From 98cb5dab1f5a1138032dea2dd9319374b39f851a Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Thu, 7 Aug 2025 14:29:47 -0400 Subject: [PATCH 001/720] Implementation of the win32 API for regular windows --- examples/multi_window_ref_app/.gitignore | 45 ++ examples/multi_window_ref_app/.metadata | 30 ++ .../multi_window_ref_app/.vscode/launch.json | 35 ++ examples/multi_window_ref_app/README.md | 5 + .../analysis_options.yaml | 28 + .../lib/app/main_window.dart | 271 ++++++++++ .../lib/app/regular_window_content.dart | 225 ++++++++ .../lib/app/regular_window_edit_dialog.dart | 190 +++++++ .../lib/app/window_controller_render.dart | 43 ++ .../lib/app/window_manager_model.dart | 59 +++ .../lib/app/window_settings.dart | 25 + .../lib/app/window_settings_dialog.dart | 104 ++++ examples/multi_window_ref_app/lib/main.dart | 33 ++ examples/multi_window_ref_app/pubspec.yaml | 48 ++ .../test/widget_test.dart | 9 + .../multi_window_ref_app/windows/.gitignore | 17 + .../windows/CMakeLists.txt | 108 ++++ .../windows/flutter/CMakeLists.txt | 109 ++++ .../windows/runner/CMakeLists.txt | 37 ++ .../windows/runner/Runner.rc | 111 ++++ .../windows/runner/main.cpp | 41 ++ .../windows/runner/resource.h | 19 + .../windows/runner/runner.exe.manifest | 14 + .../windows/runner/utils.cpp | 68 +++ .../windows/runner/utils.h | 23 + packages/flutter/lib/src/widgets/_window.dart | 13 +- .../lib/src/widgets/_window_win32.dart | 494 ++++++++++++++++++ packages/flutter/pubspec.yaml | 1 + 28 files changed, 2198 insertions(+), 7 deletions(-) create mode 100644 examples/multi_window_ref_app/.gitignore create mode 100644 examples/multi_window_ref_app/.metadata create mode 100644 examples/multi_window_ref_app/.vscode/launch.json create mode 100644 examples/multi_window_ref_app/README.md create mode 100644 examples/multi_window_ref_app/analysis_options.yaml create mode 100644 examples/multi_window_ref_app/lib/app/main_window.dart create mode 100644 examples/multi_window_ref_app/lib/app/regular_window_content.dart create mode 100644 examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart create mode 100644 examples/multi_window_ref_app/lib/app/window_controller_render.dart create mode 100644 examples/multi_window_ref_app/lib/app/window_manager_model.dart create mode 100644 examples/multi_window_ref_app/lib/app/window_settings.dart create mode 100644 examples/multi_window_ref_app/lib/app/window_settings_dialog.dart create mode 100644 examples/multi_window_ref_app/lib/main.dart create mode 100644 examples/multi_window_ref_app/pubspec.yaml create mode 100644 examples/multi_window_ref_app/test/widget_test.dart create mode 100644 examples/multi_window_ref_app/windows/.gitignore create mode 100644 examples/multi_window_ref_app/windows/CMakeLists.txt create mode 100644 examples/multi_window_ref_app/windows/flutter/CMakeLists.txt create mode 100644 examples/multi_window_ref_app/windows/runner/CMakeLists.txt create mode 100644 examples/multi_window_ref_app/windows/runner/Runner.rc create mode 100644 examples/multi_window_ref_app/windows/runner/main.cpp create mode 100644 examples/multi_window_ref_app/windows/runner/resource.h create mode 100644 examples/multi_window_ref_app/windows/runner/runner.exe.manifest create mode 100644 examples/multi_window_ref_app/windows/runner/utils.cpp create mode 100644 examples/multi_window_ref_app/windows/runner/utils.h create mode 100644 packages/flutter/lib/src/widgets/_window_win32.dart diff --git a/examples/multi_window_ref_app/.gitignore b/examples/multi_window_ref_app/.gitignore new file mode 100644 index 0000000000000..79c113f9b5017 --- /dev/null +++ b/examples/multi_window_ref_app/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/examples/multi_window_ref_app/.metadata b/examples/multi_window_ref_app/.metadata new file mode 100644 index 0000000000000..c9660e951c922 --- /dev/null +++ b/examples/multi_window_ref_app/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "f07dbe9f9b40ecc5557632d6feb70a198dab5668" + channel: "[user-branch]" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668 + base_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668 + - platform: macos + create_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668 + base_revision: f07dbe9f9b40ecc5557632d6feb70a198dab5668 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/multi_window_ref_app/.vscode/launch.json b/examples/multi_window_ref_app/.vscode/launch.json new file mode 100644 index 0000000000000..c846434f923e5 --- /dev/null +++ b/examples/multi_window_ref_app/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "multi_window_ref_app", + "request": "launch", + "type": "dart", + "program": "lib/main.dart", + "toolArgs": [ + "--local-engine=host_debug_unopt_arm64", + "--local-engine-host=host_debug_unopt_arm64", + ] + }, + { + "name": "multi_window_ref_app (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "multi_window_ref_app (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, + { + "name": "(Windows) Attach", + "type": "cppvsdbg", + "request": "attach", + } + ] +} \ No newline at end of file diff --git a/examples/multi_window_ref_app/README.md b/examples/multi_window_ref_app/README.md new file mode 100644 index 0000000000000..39b39942c528b --- /dev/null +++ b/examples/multi_window_ref_app/README.md @@ -0,0 +1,5 @@ +# multi_window_ref_app + +A reference application demonstrating multi-window support for Flutter using a +rich semantics windowing API. At the moment, only the Windows platform is +supported. \ No newline at end of file diff --git a/examples/multi_window_ref_app/analysis_options.yaml b/examples/multi_window_ref_app/analysis_options.yaml new file mode 100644 index 0000000000000..0d2902135caec --- /dev/null +++ b/examples/multi_window_ref_app/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/examples/multi_window_ref_app/lib/app/main_window.dart b/examples/multi_window_ref_app/lib/app/main_window.dart new file mode 100644 index 0000000000000..3a17f7e73b9c3 --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/main_window.dart @@ -0,0 +1,271 @@ +// 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. + +// ignore_for_file: invalid_use_of_internal_member + +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/_window.dart'; + +import 'window_controller_render.dart'; + +import 'regular_window_content.dart'; +import 'window_settings.dart'; +import 'window_settings_dialog.dart'; +import 'window_manager_model.dart'; +import 'regular_window_edit_dialog.dart'; + +class MainWindow extends StatelessWidget { + MainWindow({super.key, required BaseWindowController mainController}) { + windowManagerModel.add( + KeyedWindowController( + isMainWindow: true, + key: UniqueKey(), + controller: mainController, + ), + ); + } + + final WindowManagerModel windowManagerModel = WindowManagerModel(); + final WindowSettings settings = WindowSettings(); + + @override + Widget build(BuildContext context) { + return ViewAnchor( + view: ListenableBuilder( + listenable: windowManagerModel, + builder: (BuildContext context, Widget? _) { + final List childViews = []; + for (final KeyedWindowController controller + in windowManagerModel.windows) { + if (controller.parent == null && !controller.isMainWindow) { + childViews.add( + WindowControllerRender( + controller: controller.controller, + key: controller.key, + windowSettings: settings, + windowManagerModel: windowManagerModel, + onDestroyed: () => windowManagerModel.remove(controller.key), + onError: () => windowManagerModel.remove(controller.key), + ), + ); + } + } + + return ViewCollection(views: childViews); + }, + ), + child: Scaffold( + appBar: AppBar(title: const Text('Multi Window Reference App')), + body: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 60, + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: _WindowsTable( + windowManagerModel: windowManagerModel, + ), + ), + ), + Expanded( + flex: 40, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListenableBuilder( + listenable: windowManagerModel, + builder: (BuildContext context, Widget? child) { + return _WindowCreatorCard( + selectedWindow: windowManagerModel.selected, + windowManagerModel: windowManagerModel, + windowSettings: settings, + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _WindowsTable extends StatelessWidget { + const _WindowsTable({required this.windowManagerModel}); + + final WindowManagerModel windowManagerModel; + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: windowManagerModel, + builder: (BuildContext context, Widget? widget) { + return DataTable( + showBottomBorder: true, + onSelectAll: (selected) { + windowManagerModel.select(null); + }, + columns: const [ + DataColumn( + label: SizedBox( + width: 20, + child: Text('ID', style: TextStyle(fontSize: 16)), + ), + ), + DataColumn( + label: SizedBox( + width: 120, + child: Text('Type', style: TextStyle(fontSize: 16)), + ), + ), + DataColumn( + label: SizedBox(width: 20, child: Text('')), + numeric: true, + ), + ], + rows: (windowManagerModel.windows).map(( + KeyedWindowController controller, + ) { + return DataRow( + key: controller.key, + color: WidgetStateColor.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return Theme.of(context).colorScheme.primary.withAlpha(20); + } + return Colors.transparent; + }), + selected: controller.controller == windowManagerModel.selected, + onSelectChanged: (bool? selected) { + if (selected != null) { + windowManagerModel.select( + selected ? controller.controller.rootView.viewId : null, + ); + } + }, + cells: [ + DataCell(Text('${controller.controller.rootView.viewId}')), + DataCell(Text(_getWindowTypeName(controller.controller))), + DataCell( + Row( + children: [ + IconButton( + icon: const Icon(Icons.edit_outlined), + onPressed: () => + _showWindowEditDialog(controller, context), + ), + IconButton( + icon: const Icon(Icons.delete_outlined), + onPressed: () async { + controller.controller.destroy(); + }, + ), + if (controller.controller is RegularWindowController) + IconButton( + icon: const Icon(Icons.fullscreen), + onPressed: () async { + final RegularWindowController regular = controller + .controller as RegularWindowController; + regular.setFullscreen(!regular.isFullscreen); + }, + ), + ], + ), + ), + ], + ); + }).toList(), + ); + }, + ); + } + + void _showWindowEditDialog( + KeyedWindowController controller, + BuildContext context, + ) { + return switch (controller.controller) { + final RegularWindowController regular => showRegularWindowEditDialog( + context: context, + controller: regular, + ), + }; + } + + static String _getWindowTypeName(BaseWindowController controller) { + return switch (controller) { + RegularWindowController() => "Regular", + }; + } +} + +class _WindowCreatorCard extends StatelessWidget { + const _WindowCreatorCard({ + required this.selectedWindow, + required this.windowManagerModel, + required this.windowSettings, + }); + + final BaseWindowController? selectedWindow; + final WindowManagerModel windowManagerModel; + final WindowSettings windowSettings; + + @override + Widget build(BuildContext context) { + return Card.outlined( + margin: const EdgeInsets.symmetric(horizontal: 25), + child: Padding( + padding: const EdgeInsets.fromLTRB(25, 0, 25, 5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.only(top: 10, bottom: 10), + child: Text( + 'New Window', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + OutlinedButton( + onPressed: () async { + final UniqueKey key = UniqueKey(); + windowManagerModel.add( + KeyedWindowController( + key: key, + controller: RegularWindowController( + delegate: CallbackRegularWindowControllerDelegate( + onDestroyed: () => windowManagerModel.remove(key), + ), + title: "Regular", + preferredSize: windowSettings.regularSize, + ), + ), + ); + }, + child: const Text('Regular'), + ), + const SizedBox(height: 8), + Container( + alignment: Alignment.bottomRight, + child: TextButton( + child: const Text('SETTINGS'), + onPressed: () { + windowSettingsDialog(context, windowSettings); + }, + ), + ), + const SizedBox(width: 8), + ], + ), + ], + ), + ), + ); + } +} diff --git a/examples/multi_window_ref_app/lib/app/regular_window_content.dart b/examples/multi_window_ref_app/lib/app/regular_window_content.dart new file mode 100644 index 0000000000000..2c42e7c09c6b6 --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/regular_window_content.dart @@ -0,0 +1,225 @@ +// 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. + +// ignore_for_file: invalid_use_of_internal_member + +import 'package:flutter/material.dart'; +import 'window_controller_render.dart'; +import 'window_manager_model.dart'; +import 'window_settings.dart'; +import 'dart:math'; +import 'package:vector_math/vector_math_64.dart'; +import 'package:flutter/src/widgets/_window.dart'; + +class RegularWindowContent extends StatefulWidget { + const RegularWindowContent({ + super.key, + required this.window, + required this.windowSettings, + required this.windowManagerModel, + }); + + final RegularWindowController window; + final WindowSettings windowSettings; + final WindowManagerModel windowManagerModel; + + @override + State createState() => _RegularWindowContentState(); +} + +class CallbackRegularWindowControllerDelegate + extends RegularWindowControllerDelegate { + CallbackRegularWindowControllerDelegate({required this.onDestroyed}); + + @override + void onWindowDestroyed() { + onDestroyed(); + super.onWindowDestroyed(); + } + + final VoidCallback onDestroyed; +} + +class _RegularWindowContentState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _animation; + late final Color cubeColor; + + @override + void initState() { + super.initState(); + _animation = AnimationController( + vsync: this, + lowerBound: 0, + upperBound: 2 * pi, + duration: const Duration(seconds: 15), + )..repeat(); + cubeColor = _generateRandomDarkColor(); + } + + @override + void dispose() { + _animation.dispose(); + super.dispose(); + } + + Color _generateRandomDarkColor() { + final random = Random(); + const int lowerBound = 32; + const int span = 160; + int red = lowerBound + random.nextInt(span); + int green = lowerBound + random.nextInt(span); + int blue = lowerBound + random.nextInt(span); + return Color.fromARGB(255, red, green, blue); + } + + @override + Widget build(BuildContext context) { + final dpr = MediaQuery.of(context).devicePixelRatio; + final windowSize = WindowScope.contentSizeOf(context); + + final child = Scaffold( + appBar: AppBar(title: Text('Regular Window')), + body: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedBuilder( + animation: _animation, + builder: (context, child) { + return CustomPaint( + size: const Size(200, 200), + painter: _RotatedWireCube( + angle: _animation.value, + color: cubeColor, + ), + ); + }, + ), + ], + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + final UniqueKey key = UniqueKey(); + widget.windowManagerModel.add( + KeyedWindowController( + key: key, + controller: RegularWindowController( + preferredSize: widget.windowSettings.regularSize, + delegate: CallbackRegularWindowControllerDelegate( + onDestroyed: () => + widget.windowManagerModel.remove(key), + ), + title: "Regular", + ), + ), + ); + }, + child: const Text('Create Regular Window'), + ), + const SizedBox(height: 20), + Text( + 'View #${widget.window.rootView.viewId}\n' + 'Size: ${(windowSize.width).toStringAsFixed(1)}\u00D7${(windowSize.height).toStringAsFixed(1)}\n' + 'Device Pixel Ratio: $dpr', + textAlign: TextAlign.center, + ), + ], + ), + ], + ), + ), + ); + + return ViewAnchor( + view: ListenableBuilder( + listenable: widget.windowManagerModel, + builder: (BuildContext context, Widget? _) { + final List childViews = []; + for (final KeyedWindowController controller + in widget.windowManagerModel.windows) { + if (controller.parent == widget.window) { + childViews.add( + WindowControllerRender( + controller: controller.controller, + key: controller.key, + windowSettings: widget.windowSettings, + windowManagerModel: widget.windowManagerModel, + onDestroyed: () => + widget.windowManagerModel.remove(controller.key), + onError: () => + widget.windowManagerModel.remove(controller.key), + ), + ); + } + } + + return ViewCollection(views: childViews); + }, + ), + child: child, + ); + } +} + +class _RotatedWireCube extends CustomPainter { + static List vertices = [ + Vector3(-0.5, -0.5, -0.5), + Vector3(0.5, -0.5, -0.5), + Vector3(0.5, 0.5, -0.5), + Vector3(-0.5, 0.5, -0.5), + Vector3(-0.5, -0.5, 0.5), + Vector3(0.5, -0.5, 0.5), + Vector3(0.5, 0.5, 0.5), + Vector3(-0.5, 0.5, 0.5), + ]; + + static const List> edges = [ + [0, 1], [1, 2], [2, 3], [3, 0], // Front face + [4, 5], [5, 6], [6, 7], [7, 4], // Back face + [0, 4], [1, 5], [2, 6], [3, 7], // Connecting front and back + ]; + + final double angle; + final Color color; + + _RotatedWireCube({required this.angle, required this.color}); + + Offset scaleAndCenter(Vector3 point, double size, Offset center) { + final scale = size / 2; + return Offset(center.dx + point.x * scale, center.dy - point.y * scale); + } + + @override + void paint(Canvas canvas, Size size) { + final rotatedVertices = vertices + .map((vertex) => Matrix4.rotationX(angle).transformed3(vertex)) + .map((vertex) => Matrix4.rotationY(angle).transformed3(vertex)) + .map((vertex) => Matrix4.rotationZ(angle).transformed3(vertex)) + .toList(); + + final center = Offset(size.width / 2, size.height / 2); + + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + for (var edge in edges) { + final p1 = scaleAndCenter(rotatedVertices[edge[0]], size.width, center); + final p2 = scaleAndCenter(rotatedVertices[edge[1]], size.width, center); + canvas.drawLine(p1, p2, paint); + } + } + + @override + bool shouldRepaint(_RotatedWireCube oldDelegate) => true; +} diff --git a/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart b/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart new file mode 100644 index 0000000000000..3dee198a07464 --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart @@ -0,0 +1,190 @@ +// 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. + +// ignore_for_file: invalid_use_of_internal_member + +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/_window.dart'; + +void showRegularWindowEditDialog({ + required BuildContext context, + required RegularWindowController controller, +}) { + showDialog( + context: context, + builder: (context) => _RegularWindowEditDialog( + controller: controller, + onClose: () => Navigator.pop(context), + ), + ); +} + +class _RegularWindowEditDialog extends StatefulWidget { + const _RegularWindowEditDialog({ + required this.controller, + required this.onClose, + }); + + final RegularWindowController controller; + final VoidCallback onClose; + + @override + State createState() => _RegularWindowEditDialogState(); +} + +class _RegularWindowEditDialogState extends State<_RegularWindowEditDialog> { + late Size initialSize; + late String initialTitle; + late bool initialFullscreen; + late bool initialMaximized; + late bool initialMinimized; + + late final TextEditingController widthController; + late final TextEditingController heightController; + late final TextEditingController titleController; + + bool? nextIsFullscreen; + bool? nextIsMaximized; + bool? nextIsMinized; + + @override + void initState() { + super.initState(); + initialSize = widget.controller.contentSize; + initialTitle = widget.controller.title; + initialFullscreen = widget.controller.isFullscreen; + initialMaximized = widget.controller.isMaximized; + initialMinimized = widget.controller.isMinimized; + + widthController = TextEditingController(text: initialSize.width.toString()); + heightController = TextEditingController( + text: initialSize.height.toString(), + ); + titleController = TextEditingController(text: initialTitle); + + // TODO: Re-add listeners (somehow?) + // widget.controller.addListener(_onNotification); + } + + void _onNotification() { + // We listen on the state of the controller. If a value that the user + // can edit changes from what it was initially set to, we invalidate + // their current change and store the new "initial" value. + if (widget.controller.contentSize != initialSize) { + initialSize = widget.controller.contentSize; + widthController.text = widget.controller.contentSize.width.toString(); + heightController.text = widget.controller.contentSize.height.toString(); + } + if (widget.controller.isFullscreen != initialFullscreen) { + setState(() { + initialFullscreen = widget.controller.isFullscreen; + nextIsFullscreen = null; + }); + } + if (widget.controller.isMaximized != initialMaximized) { + setState(() { + initialMaximized = widget.controller.isMaximized; + nextIsMaximized = null; + }); + } + if (widget.controller.isMinimized != initialMinimized) { + setState(() { + initialMinimized = widget.controller.isMinimized; + nextIsMinized = null; + }); + } + } + + @override + void dispose() { + super.dispose(); + // widget.controller.removeListener(_onNotification); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("Edit Window Properties"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: widthController, + keyboardType: TextInputType.number, + decoration: InputDecoration(labelText: "Width"), + ), + TextField( + controller: heightController, + keyboardType: TextInputType.number, + decoration: InputDecoration(labelText: "Height"), + ), + TextField( + controller: titleController, + decoration: InputDecoration(labelText: "Title"), + ), + CheckboxListTile( + title: const Text('Fullscreen'), + value: nextIsFullscreen ?? initialFullscreen, + onChanged: (bool? value) { + if (value != null) { + setState(() => nextIsFullscreen = value); + } + }, + ), + CheckboxListTile( + title: const Text('Maximized'), + value: nextIsMaximized ?? initialMaximized, + onChanged: (bool? value) { + if (value != null) { + setState(() => nextIsMaximized = value); + } + }, + ), + CheckboxListTile( + title: const Text('Minimized'), + value: nextIsMinized ?? initialMinimized, + onChanged: (bool? value) { + if (value != null) { + setState(() => nextIsMinized = value); + } + }, + ), + ], + ), + actions: [ + TextButton(onPressed: () => widget.onClose(), child: Text("Cancel")), + TextButton(onPressed: () => _onSave(), child: Text("Save")), + ], + ); + } + + void _onSave() { + double? width = double.tryParse(widthController.text); + double? height = double.tryParse(heightController.text); + String? title = titleController.text.isEmpty ? null : titleController.text; + if (width != null && height != null) { + widget.controller.setSize(Size(width, height)); + } + if (title != null) { + widget.controller.setTitle(title); + } + if (nextIsFullscreen != null) { + if (widget.controller.isFullscreen != nextIsFullscreen) { + widget.controller.setFullscreen(nextIsFullscreen!); + } + } + if (nextIsMaximized != null) { + if (widget.controller.isMaximized != nextIsMaximized) { + widget.controller.setMaximized(nextIsMaximized!); + } + } + if (nextIsMinized != null) { + if (widget.controller.isMinimized != nextIsMinized) { + widget.controller.setMinimized(nextIsMinized!); + } + } + + widget.onClose(); + } +} diff --git a/examples/multi_window_ref_app/lib/app/window_controller_render.dart b/examples/multi_window_ref_app/lib/app/window_controller_render.dart new file mode 100644 index 0000000000000..80036d7b8f74e --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/window_controller_render.dart @@ -0,0 +1,43 @@ +// 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. + +// ignore_for_file: invalid_use_of_internal_member + +import 'package:flutter/material.dart'; +import 'regular_window_content.dart'; +import 'window_manager_model.dart'; +import 'window_settings.dart'; +import 'package:flutter/src/widgets/_window.dart'; + +class WindowControllerRender extends StatelessWidget { + const WindowControllerRender({ + required this.controller, + required this.onDestroyed, + required this.onError, + required this.windowSettings, + required this.windowManagerModel, + required super.key, + }); + + final BaseWindowController controller; + final VoidCallback onDestroyed; + final VoidCallback onError; + final WindowSettings windowSettings; + final WindowManagerModel windowManagerModel; + + @override + Widget build(BuildContext context) { + return switch (controller) { + final RegularWindowController regular => RegularWindow( + key: key, + controller: regular, + child: RegularWindowContent( + window: regular, + windowSettings: windowSettings, + windowManagerModel: windowManagerModel, + ), + ), + }; + } +} diff --git a/examples/multi_window_ref_app/lib/app/window_manager_model.dart b/examples/multi_window_ref_app/lib/app/window_manager_model.dart new file mode 100644 index 0000000000000..2e5920f53115a --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/window_manager_model.dart @@ -0,0 +1,59 @@ +// 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. + +// ignore_for_file: invalid_use_of_internal_member + +import 'package:flutter/widgets.dart'; +import 'package:flutter/src/widgets/_window.dart'; + +class KeyedWindowController { + KeyedWindowController({ + this.parent, + this.isMainWindow = false, + required this.key, + required this.controller, + }); + + final BaseWindowController? parent; + final bool isMainWindow; + final UniqueKey key; + final BaseWindowController controller; +} + +/// Manages a flat list of all of the [WindowController]s that have been +/// created by the application as well as which controller is currently +/// selected by the UI. +class WindowManagerModel extends ChangeNotifier { + final List _windows = []; + List get windows => _windows; + int? _selectedViewId; + BaseWindowController? get selected { + if (_selectedViewId == null) { + return null; + } + + for (final KeyedWindowController controller in _windows) { + if (controller.controller.rootView.viewId == _selectedViewId) { + return controller.controller; + } + } + + return null; + } + + void add(KeyedWindowController window) { + _windows.add(window); + notifyListeners(); + } + + void remove(UniqueKey key) { + _windows.removeWhere((KeyedWindowController window) => window.key == key); + notifyListeners(); + } + + void select(int? viewId) { + _selectedViewId = viewId; + notifyListeners(); + } +} diff --git a/examples/multi_window_ref_app/lib/app/window_settings.dart b/examples/multi_window_ref_app/lib/app/window_settings.dart new file mode 100644 index 0000000000000..157e78c30e1be --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/window_settings.dart @@ -0,0 +1,25 @@ +// 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'; + +class WindowSettings extends ChangeNotifier { + WindowSettings({Size regularSize = const Size(400, 300)}) + : _regularSize = regularSize; + + WindowSettings.clone(WindowSettings other) + : this(regularSize: other.regularSize); + + Size _regularSize; + Size get regularSize => _regularSize; + set regularSize(Size value) { + _regularSize = value; + notifyListeners(); + } + + void from(WindowSettings settings) { + _regularSize = settings.regularSize; + notifyListeners(); + } +} diff --git a/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart b/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart new file mode 100644 index 0000000000000..a3d637a82d6c4 --- /dev/null +++ b/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart @@ -0,0 +1,104 @@ +// 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 'window_settings.dart'; + +Future windowSettingsDialog( + BuildContext context, + WindowSettings settings, +) async { + return await showDialog( + barrierDismissible: true, + context: context, + builder: (BuildContext ctx) { + return _WindowSettingsEditor( + settings: WindowSettings.clone(settings), + onClose: (WindowSettings newSettings) { + settings.from(newSettings); + Navigator.of(context, rootNavigator: true).pop(); + }); + }, + ); +} + +class _WindowSettingsEditor extends StatelessWidget { + const _WindowSettingsEditor({required this.settings, required this.onClose}); + + final WindowSettings settings; + final void Function(WindowSettings) onClose; + + @override + Widget build(BuildContext context) { + return SimpleDialog( + contentPadding: const EdgeInsets.all(4), + titlePadding: const EdgeInsets.fromLTRB(24, 10, 24, 0), + title: const Center(child: Text('Window Settings')), + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: ListTile( + title: const Text('Regular'), + subtitle: ListenableBuilder( + listenable: settings, + builder: (BuildContext ctx, Widget? _) { + return Row( + children: [ + Expanded( + child: TextFormField( + initialValue: + settings.regularSize.width.toString(), + decoration: const InputDecoration( + labelText: 'Initial width', + ), + onChanged: (String value) => + settings.regularSize = Size( + double.tryParse(value) ?? 0, + settings.regularSize.height, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: TextFormField( + initialValue: + settings.regularSize.height.toString(), + decoration: const InputDecoration( + labelText: 'Initial height', + ), + onChanged: (String value) => + settings.regularSize = Size( + settings.regularSize.width, + double.tryParse(value) ?? 0, + ), + ), + ), + ], + ); + }, + ), + ), + ), + const SizedBox(width: 10), + ], + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextButton( + onPressed: () { + onClose(settings); + }, + child: const Text('Apply'), + ), + ) + ], + ); + } +} diff --git a/examples/multi_window_ref_app/lib/main.dart b/examples/multi_window_ref_app/lib/main.dart new file mode 100644 index 0000000000000..0f60fcaacee73 --- /dev/null +++ b/examples/multi_window_ref_app/lib/main.dart @@ -0,0 +1,33 @@ +// 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. + +// ignore_for_file: invalid_use_of_internal_member + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'app/main_window.dart'; +import 'package:flutter/src/widgets/_window.dart'; + +class MainControllerWindowDelegate extends RegularWindowControllerDelegate { + @override + void onWindowDestroyed() { + super.onWindowDestroyed(); + exit(0); + } +} + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + final RegularWindowController controller = RegularWindowController( + preferredSize: const Size(800, 600), + title: "Multi-Window Reference Application", + delegate: MainControllerWindowDelegate()); + runWidget( + RegularWindow( + controller: controller, + child: MaterialApp(home: MainWindow(mainController: controller)), + ), + ); +} diff --git a/examples/multi_window_ref_app/pubspec.yaml b/examples/multi_window_ref_app/pubspec.yaml new file mode 100644 index 0000000000000..eb5f7859c7404 --- /dev/null +++ b/examples/multi_window_ref_app/pubspec.yaml @@ -0,0 +1,48 @@ +name: multi_window_ref_app +description: "Reference app for the multi-view windowing API." +version: 1.0.0+1 + +environment: + sdk: '>=3.5.0-180.0.dev <4.0.0' + +dependencies: + flutter: + sdk: flutter + stack_trace: 1.12.1 + vector_math: 2.2.0 + + characters: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + collection: 1.19.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + ffi: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + material_color_utilities: 0.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.17.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path: 1.9.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: 5.0.0 + + async: 2.13.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + boolean_selector: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + clock: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + crypto: 3.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + file: 7.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + intl: 0.20.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + matcher: 0.12.17 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + platform: 3.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + process: 5.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_span: 1.10.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + string_scanner: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + sync_http: 0.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + term_glyph: 1.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.7.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + typed_data: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vm_service: 15.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + webdriver: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +flutter: + uses-material-design: true + +# PUBSPEC CHECKSUM: 78a6 diff --git a/examples/multi_window_ref_app/test/widget_test.dart b/examples/multi_window_ref_app/test/widget_test.dart new file mode 100644 index 0000000000000..31030bc0daaa8 --- /dev/null +++ b/examples/multi_window_ref_app/test/widget_test.dart @@ -0,0 +1,9 @@ +// 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'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async {}); +} diff --git a/examples/multi_window_ref_app/windows/.gitignore b/examples/multi_window_ref_app/windows/.gitignore new file mode 100644 index 0000000000000..d492d0d98c8fd --- /dev/null +++ b/examples/multi_window_ref_app/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/examples/multi_window_ref_app/windows/CMakeLists.txt b/examples/multi_window_ref_app/windows/CMakeLists.txt new file mode 100644 index 0000000000000..4450980427578 --- /dev/null +++ b/examples/multi_window_ref_app/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(multi_window_ref_app LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "multi_window_ref_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt b/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000000000..a71c6e2c5e4f3 --- /dev/null +++ b/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") +set(CMAKE_CXX_STANDARD 20) + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/examples/multi_window_ref_app/windows/runner/CMakeLists.txt b/examples/multi_window_ref_app/windows/runner/CMakeLists.txt new file mode 100644 index 0000000000000..697f43451ac08 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "main.cpp" + "utils.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/examples/multi_window_ref_app/windows/runner/Runner.rc b/examples/multi_window_ref_app/windows/runner/Runner.rc new file mode 100644 index 0000000000000..909820ff45c09 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/Runner.rc @@ -0,0 +1,111 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "The Flutter Authors" "\0" + VALUE "FileDescription", "A reference application demonstrating Flutter's multi-window API." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "Flutter Multi-Window Reference App" "\0" + VALUE "LegalCopyright", "Copyright 2014 The Flutter Authors. All rights reserved." "\0" + VALUE "OriginalFilename", "multi_window_ref_app.exe" "\0" + VALUE "ProductName", "Flutter Multi-Window Reference App" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/examples/multi_window_ref_app/windows/runner/main.cpp b/examples/multi_window_ref_app/windows/runner/main.cpp new file mode 100644 index 0000000000000..11a76c8ceffa2 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/main.cpp @@ -0,0 +1,41 @@ +// 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. + +#include +#include +#include + +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t* command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + auto command_line_arguments{GetCommandLineArguments()}; + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + auto const engine{std::make_shared(project)}; + RegisterPlugins(engine.get()); + engine->Run(); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/examples/multi_window_ref_app/windows/runner/resource.h b/examples/multi_window_ref_app/windows/runner/resource.h new file mode 100644 index 0000000000000..69cacf3cead96 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/resource.h @@ -0,0 +1,19 @@ +// 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. + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/examples/multi_window_ref_app/windows/runner/runner.exe.manifest b/examples/multi_window_ref_app/windows/runner/runner.exe.manifest new file mode 100644 index 0000000000000..153653e8d67f8 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/examples/multi_window_ref_app/windows/runner/utils.cpp b/examples/multi_window_ref_app/windows/runner/utils.cpp new file mode 100644 index 0000000000000..6abcd65042070 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/utils.cpp @@ -0,0 +1,68 @@ +// 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. + +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/examples/multi_window_ref_app/windows/runner/utils.h b/examples/multi_window_ref_app/windows/runner/utils.h new file mode 100644 index 0000000000000..54414c989ba71 --- /dev/null +++ b/examples/multi_window_ref_app/windows/runner/utils.h @@ -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. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/flutter/lib/src/widgets/_window.dart b/packages/flutter/lib/src/widgets/_window.dart index 9aa703a83e86d..27de4f3fb72d3 100644 --- a/packages/flutter/lib/src/widgets/_window.dart +++ b/packages/flutter/lib/src/widgets/_window.dart @@ -14,6 +14,7 @@ // // See: https://github.com/flutter/flutter/issues/30701. +import 'dart:io'; import 'dart:ui' show Display, FlutterView; import 'package:flutter/foundation.dart'; @@ -24,6 +25,7 @@ import 'binding.dart'; import 'framework.dart'; import 'inherited_model.dart'; import 'view.dart'; +import '_window_win32.dart'; const String _kWindowingDisabledErrorMessage = ''' Windowing APIs are not enabled. @@ -137,12 +139,6 @@ mixin class RegularWindowControllerDelegate { if (!isWindowingEnabled) { throw UnsupportedError(_kWindowingDisabledErrorMessage); } - - final WindowingOwner owner = WidgetsBinding.instance.windowingOwner; - if (!owner.hasTopLevelWindows()) { - // TODO(mattkae): close the application if this is the last window - // via ServicesBinding.instance.exitApplication(AppExitType.cancelable); - } } } @@ -408,7 +404,10 @@ abstract class WindowingOwner { return _WindowingOwnerUnsupported(errorMessage: _kWindowingDisabledErrorMessage); } - // TODO(mattkae): Implement windowing owners for desktop platforms. + if (Platform.isWindows) { + return WindowingOwnerWin32(); + } + return _WindowingOwnerUnsupported(errorMessage: 'Windowing is unsupported on this platform.'); } } diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart new file mode 100644 index 0000000000000..731f9e4ba37a2 --- /dev/null +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -0,0 +1,494 @@ +// 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:ffi' hide Size; +import 'dart:io'; +import 'dart:ui' show Display, FlutterView; +import 'package:ffi/ffi.dart' as ffi; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import '_window.dart'; +import 'binding.dart'; + +/// A Windows window handle. +typedef HWND = Pointer; + +const int _WM_SIZE = 0x0005; +const int _WM_CLOSE = 0x0010; + +const int _SW_RESTORE = 9; +const int _SW_MAXIMIZE = 3; +const int _SW_MINIMIZE = 6; + +/// Abstract handler class for Windows messages. +abstract class WindowsMessageHandler { + /// Handles a window message. Returned value, if not null will be + /// returned to the system as LRESULT and will stop all other + /// handlers from being called. + int? handleWindowsMessage( + FlutterView view, + HWND windowHandle, + int message, + int wParam, + int lParam, + ); +} + +/// Windowing owner implementation for Windows. +/// +/// This class will only be successfully instantiated on the Win32 platform. +/// If the platform is not on Windows, the constructor with thrown an +/// [UnsupportedError]. +class WindowingOwnerWin32 extends WindowingOwner { + /// Creates a new [WindowingOwnerWin32] instance. + WindowingOwnerWin32() { + if (!Platform.isWindows) { + UnsupportedError('Only available on the Win32 platform'); + } + + final Pointer<_WindowingInitRequest> request = ffi.calloc<_WindowingInitRequest>() + ..ref.onMessage = NativeCallable)>.isolateLocal( + _onMessage, + ).nativeFunction; + _initializeWindowing(PlatformDispatcher.instance.engineId!, request); + ffi.calloc.free(request); + } + + @override + RegularWindowController createRegularWindowController({ + Size? preferredSize, + BoxConstraints? preferredConstraints, + String? title, + required RegularWindowControllerDelegate delegate, + }) { + return RegularWindowControllerWin32( + owner: this, + delegate: delegate, + preferredSize: preferredSize, + preferredConstraints: preferredConstraints, + title: title, + ); + } + + /// Register a new message handler. + /// + /// The handler will be triggered for unhandled messages for all top level + /// windows. + /// + /// Adding a handler multiple times has no effect. + /// + /// Handlers are called in the order that they are added. + void addMessageHandler(WindowsMessageHandler handler) { + if (_messageHandlers.contains(handler)) { + return; + } + + _messageHandlers.add(handler); + } + + /// Unregister a message handler. + /// + /// If the handler does not exist, this method has no effect. + void removeMessageHandler(WindowsMessageHandler handler) { + _messageHandlers.remove(handler); + } + + final List _messageHandlers = []; + + void _onMessage(Pointer<_WindowsMessage> message) { + final List handlers = List.from(_messageHandlers); + final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( + (FlutterView view) => view.viewId == message.ref.viewId, + ); + for (final WindowsMessageHandler handler in handlers) { + final int? result = handler.handleWindowsMessage( + flutterView, + message.ref.windowHandle, + message.ref.message, + message.ref.wParam, + message.ref.lParam, + ); + if (result != null) { + message.ref.handled = true; + message.ref.lResult = result; + return; + } + } + } + + @override + bool hasTopLevelWindows() { + return _hasTopLevelWindows(PlatformDispatcher.instance.engineId!); + } + + @Native(symbol: 'InternalFlutterWindows_WindowManager_HasTopLevelWindows') + external static bool _hasTopLevelWindows(int engineId); + + @Native)>( + symbol: 'InternalFlutterWindows_WindowManager_Initialize', + ) + external static void _initializeWindowing(int engineId, Pointer<_WindowingInitRequest> request); +} + +/// The Win32 implementation of the regular window controller. +class RegularWindowControllerWin32 extends RegularWindowController + implements WindowsMessageHandler { + /// Creates a new regular window controller for Win32. When this constructor + /// completes the FlutterView is created and framework is aware of it. + RegularWindowControllerWin32({ + required WindowingOwnerWin32 owner, + required RegularWindowControllerDelegate delegate, + Size? preferredSize, + BoxConstraints? preferredConstraints, + String? title, + }) : _owner = owner, + _delegate = delegate, + super.empty() { + owner.addMessageHandler(this); + final Pointer<_WindowCreationRequest> request = ffi.calloc<_WindowCreationRequest>() + ..ref.preferredSize.from(preferredSize) + ..ref.preferredConstraints.from(preferredConstraints) + ..ref.title = (title ?? 'Regular window').toNativeUtf16(); + final int viewId = _createWindow(PlatformDispatcher.instance.engineId!, request); + ffi.calloc.free(request); + final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( + (FlutterView view) => view.viewId == viewId, + ); + rootView = flutterView; + } + + @override + Size get contentSize { + _ensureNotDestroyed(); + final _ActualWindowSize size = _getWindowContentSize(getWindowHandle()); + final Size result = Size(size.width, size.height); + return result; + } + + @override + String get title { + final int length = _getWindowTextLength(getWindowHandle()); + if (length == 0) { + return ''; + } + + final Pointer data = ffi.calloc(length + 1); + try { + final Pointer buffer = data.cast(); + _getWindowText(getWindowHandle(), buffer, length + 1); + return buffer.toDartString(); + } finally { + ffi.calloc.free(data); + } + } + + @override + bool get isActivated { + return _getForegroundWindow() == getWindowHandle(); + } + + @override + bool get isMaximized { + _ensureNotDestroyed(); + return _isZoomed(getWindowHandle()) != 0; + } + + @override + bool get isMinimized { + _ensureNotDestroyed(); + return _isIconic(getWindowHandle()) != 0; + } + + @override + bool get isFullscreen { + return _getFullscreen(getWindowHandle()); + } + + @override + void setSize(Size? size) { + final Pointer<_WindowSizeRequest> request = ffi.calloc<_WindowSizeRequest>(); + request.ref.hasSize = size != null; + request.ref.width = size?.width ?? 0; + request.ref.height = size?.height ?? 0; + _setWindowContentSize(getWindowHandle(), request); + ffi.calloc.free(request); + } + + @override + void setConstraints(BoxConstraints constraints) { + final Pointer<_WindowConstraints> request = ffi.calloc<_WindowConstraints>(); + request.ref.from(constraints); + _setWindowConstraints(getWindowHandle(), request); + ffi.calloc.free(request); + } + + @override + void setTitle(String title) { + _ensureNotDestroyed(); + final Pointer titlePointer = title.toNativeUtf16(); + _setWindowTitle(getWindowHandle(), titlePointer); + ffi.calloc.free(titlePointer); + } + + @override + void activate() { + _ensureNotDestroyed(); + _showWindow(getWindowHandle(), _SW_RESTORE); + } + + @override + void setMaximized(bool maximized) { + _ensureNotDestroyed(); + if (maximized) { + _showWindow(getWindowHandle(), _SW_MAXIMIZE); + } else { + _showWindow(getWindowHandle(), _SW_RESTORE); + } + } + + @override + void setMinimized(bool minimized) { + _ensureNotDestroyed(); + if (minimized) { + _showWindow(getWindowHandle(), _SW_MINIMIZE); + } else { + _showWindow(getWindowHandle(), _SW_RESTORE); + } + } + + @override + void setFullscreen(bool fullscreen, {Display? display}) { + final Pointer<_FullscreenRequest> request = ffi.calloc<_FullscreenRequest>(); + request.ref.hasDisplayId = false; + request.ref.displayId = display?.id ?? 0; + request.ref.fullscreen = fullscreen; + _setFullscreen(getWindowHandle(), request); + ffi.calloc.free(request); + } + + /// Returns HWND pointer to the top level window. + Pointer getWindowHandle() { + _ensureNotDestroyed(); + return _getWindowHandle(PlatformDispatcher.instance.engineId!, rootView.viewId); + } + + void _ensureNotDestroyed() { + if (_destroyed) { + throw StateError('Window has been destroyed.'); + } + } + + final RegularWindowControllerDelegate _delegate; + bool _destroyed = false; + + @override + void destroy() { + if (_destroyed) { + return; + } + _destroyWindow(getWindowHandle()); + _destroyed = true; + _delegate.onWindowDestroyed(); + _owner.removeMessageHandler(this); + } + + @override + int? handleWindowsMessage( + FlutterView view, + HWND windowHandle, + int message, + int wParam, + int lParam, + ) { + if (view.viewId != rootView.viewId) { + return null; + } + + if (message == _WM_CLOSE) { + _delegate.onWindowCloseRequested(this); + return 0; + } else if (message == _WM_SIZE) { + // TODO: notify context of size change + } + return null; + } + + final WindowingOwnerWin32 _owner; + + @Native)>( + symbol: 'InternalFlutterWindows_WindowManager_CreateRegularWindow', + ) + external static int _createWindow(int engineId, Pointer<_WindowCreationRequest> request); + + @Native Function(Int64, Int64)>( + symbol: 'InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle', + ) + external static Pointer _getWindowHandle(int engineId, int viewId); + + @Native)>(symbol: 'DestroyWindow') + external static void _destroyWindow(Pointer windowHandle); + + @Native<_ActualWindowSize Function(Pointer)>( + symbol: 'InternalFlutterWindows_WindowManager_GetWindowContentSize', + ) + external static _ActualWindowSize _getWindowContentSize(Pointer windowHandle); + + @Native, Pointer, Int32)>(symbol: 'GetWindowTextW') + external static void _getWindowTitle( + Pointer windowHandle, + Pointer title, + int maxLength, + ); + + @Native, Pointer)>(symbol: 'SetWindowTextW') + external static void _setWindowTitle(Pointer windowHandle, Pointer title); + + @Native, Pointer<_WindowSizeRequest>)>( + symbol: 'InternalFlutterWindows_WindowManager_SetWindowSize', + ) + external static void _setWindowContentSize( + Pointer windowHandle, + Pointer<_WindowSizeRequest> size, + ); + + @Native, Pointer<_WindowConstraints>)>( + symbol: 'InternalFlutterWindows_WindowManager_SetWindowConstraints', + ) + external static void _setWindowConstraints( + Pointer windowHandle, + Pointer<_WindowConstraints> constraints, + ); + + @Native, Int32)>(symbol: 'ShowWindow') + external static void _showWindow(Pointer windowHandle, int command); + + @Native)>(symbol: 'IsIconic') + external static int _isIconic(Pointer windowHandle); + + @Native)>(symbol: 'IsZoomed') + external static int _isZoomed(Pointer windowHandle); + + @Native, Pointer<_FullscreenRequest>)>( + symbol: 'InternalFlutterWindows_WindowManager_SetFullscreen', + ) + external static void _setFullscreen( + Pointer windowHandle, + Pointer<_FullscreenRequest> request, + ); + + @Native)>( + symbol: 'InternalFlutterWindows_WindowManager_GetFullscreen', + ) + external static bool _getFullscreen(Pointer windowHandle); + + @Native)>(symbol: 'GetWindowTextLengthW') + external static int _getWindowTextLength(Pointer windowHandle); + + @Native, Pointer, Int32)>(symbol: 'GetWindowTextW') + external static int _getWindowText( + Pointer windowHandle, + Pointer lpString, + int maxLength, + ); + + @Native Function()>(symbol: 'GetForegroundWindow') + external static Pointer _getForegroundWindow(); +} + +/// Request to initialize windowing system. +final class _WindowingInitRequest extends Struct { + external Pointer)>> onMessage; +} + +final class _WindowSizeRequest extends Struct { + @Bool() + external bool hasSize; + + @Double() + external double width; + + @Double() + external double height; + + void from(Size? size) { + hasSize = size != null; + width = size?.width ?? 0; + height = size?.height ?? 0; + } +} + +final class _WindowConstraints extends Struct { + @Bool() + external bool hasConstraints; + + @Double() + external double minWidth; + + @Double() + external double minHeight; + + @Double() + external double maxWidth; + + @Double() + external double maxHeight; + + void from(BoxConstraints? constraints) { + hasConstraints = constraints != null; + minWidth = constraints?.minWidth ?? 0; + minHeight = constraints?.minHeight ?? 0; + maxWidth = constraints?.maxWidth ?? double.maxFinite; + maxHeight = constraints?.maxHeight ?? double.maxFinite; + } +} + +final class _WindowCreationRequest extends Struct { + external _WindowSizeRequest preferredSize; + external _WindowConstraints preferredConstraints; + external Pointer title; +} + +/// Windows message received for all top level windows (regardless whether +/// they are created using a windowing controller). +final class _WindowsMessage extends Struct { + @Int64() + external int viewId; + + external Pointer windowHandle; + + @Int32() + external int message; + + @Int64() + external int wParam; + + @Int64() + external int lParam; + + @Int64() + external int lResult; + + @Bool() + external bool handled; +} + +final class _ActualWindowSize extends Struct { + @Double() + external double width; + + @Double() + external double height; +} + +final class _FullscreenRequest extends Struct { + @Bool() + external bool fullscreen; + + @Bool() + external bool hasDisplayId; + + @Uint64() + external int displayId; +} diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index c8fdce53648b3..f08d6f9e77519 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: vector_math: 2.2.0 sky_engine: sdk: flutter + ffi: ^2.1.4 dev_dependencies: flutter_driver: From 4a5bfd998661371406344fb01c02b6cc4169a546 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 7 Aug 2025 15:20:07 -0400 Subject: [PATCH 002/720] Formatting + comments --- .../lib/src/widgets/_window_win32.dart | 74 ++++++++++++++----- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index 731f9e4ba37a2..ffab48019e953 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -23,6 +23,15 @@ const int _SW_MAXIMIZE = 3; const int _SW_MINIMIZE = 6; /// Abstract handler class for Windows messages. +/// +/// Implementations of this class should register with +/// [WindowingOwnerWin32.addMessageHandler] to begin receiving messages. +/// When finished handling messages, implementations should deregister +/// themselves with [WindowingOwnerWIn32.removeMessageHandler]. +/// +/// See also: +/// +/// * [WindowingOwnerWin32], the class that manages these handlers. abstract class WindowsMessageHandler { /// Handles a window message. Returned value, if not null will be /// returned to the system as LRESULT and will stop all other @@ -36,13 +45,19 @@ abstract class WindowsMessageHandler { ); } -/// Windowing owner implementation for Windows. +/// [WindowingOwner] implementation for Windows. /// -/// This class will only be successfully instantiated on the Win32 platform. -/// If the platform is not on Windows, the constructor with thrown an +/// If [Platform.isWindows] is false, then the constructor will throw an /// [UnsupportedError]. +/// +/// See also: +/// +/// * [WindowingOwner], the abstract class that manages native windows. class WindowingOwnerWin32 extends WindowingOwner { /// Creates a new [WindowingOwnerWin32] instance. + /// + /// If [Platform.isWindows] is false, then this constructor will throw an + /// [UnsupportedError]. WindowingOwnerWin32() { if (!Platform.isWindows) { UnsupportedError('Only available on the Win32 platform'); @@ -56,6 +71,8 @@ class WindowingOwnerWin32 extends WindowingOwner { ffi.calloc.free(request); } + final List _messageHandlers = []; + @override RegularWindowController createRegularWindowController({ Size? preferredSize, @@ -72,7 +89,7 @@ class WindowingOwnerWin32 extends WindowingOwner { ); } - /// Register a new message handler. + /// Register a new [WindowsMessageHandler]. /// /// The handler will be triggered for unhandled messages for all top level /// windows. @@ -80,6 +97,14 @@ class WindowingOwnerWin32 extends WindowingOwner { /// Adding a handler multiple times has no effect. /// /// Handlers are called in the order that they are added. + /// + /// Callers must remove their message handlers using + /// [WindowingOwnerWin32.removeMessageHandler]. + /// + /// See also: + /// + /// * [WindowsMessageHandler], the interface for message handlers. + /// * [WindowingOwnerWin32.removeMessageHandler], to remove message handlers. void addMessageHandler(WindowsMessageHandler handler) { if (_messageHandlers.contains(handler)) { return; @@ -88,15 +113,18 @@ class WindowingOwnerWin32 extends WindowingOwner { _messageHandlers.add(handler); } - /// Unregister a message handler. + /// Unregister a [WindowsMessageHandler]. + /// + /// If the handler has not been registered, this method has no effect. + /// + /// See also: /// - /// If the handler does not exist, this method has no effect. + /// * [WindowsMessageHandler], the interface for message handlers. + /// * [WindowingOwnerWin32.addMessageHandler], to register message handlers. void removeMessageHandler(WindowsMessageHandler handler) { _messageHandlers.remove(handler); } - final List _messageHandlers = []; - void _onMessage(Pointer<_WindowsMessage> message) { final List handlers = List.from(_messageHandlers); final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( @@ -132,11 +160,19 @@ class WindowingOwnerWin32 extends WindowingOwner { external static void _initializeWindowing(int engineId, Pointer<_WindowingInitRequest> request); } -/// The Win32 implementation of the regular window controller. +/// Implementation of [RegularWindowController] for the Windows platform. +/// +/// If [Platform.isWindows] is false, then the constructor will throw an +/// [UnsupportedError]. class RegularWindowControllerWin32 extends RegularWindowController implements WindowsMessageHandler { - /// Creates a new regular window controller for Win32. When this constructor - /// completes the FlutterView is created and framework is aware of it. + /// Creates a new regular window controller for Win32. + /// + /// If [Platform.isWindows] is false, then this constructor will throw an + /// [UnsupportedError]. + /// + /// When this constructor completes the native window has been created and + /// has a view associated with it. RegularWindowControllerWin32({ required WindowingOwnerWin32 owner, required RegularWindowControllerDelegate delegate, @@ -159,6 +195,10 @@ class RegularWindowControllerWin32 extends RegularWindowController rootView = flutterView; } + final WindowingOwnerWin32 _owner; + final RegularWindowControllerDelegate _delegate; + bool _destroyed = false; + @override Size get contentSize { _ensureNotDestroyed(); @@ -169,6 +209,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @override String get title { + _ensureNotDestroyed(); final int length = _getWindowTextLength(getWindowHandle()); if (length == 0) { return ''; @@ -186,6 +227,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @override bool get isActivated { + _ensureNotDestroyed(); return _getForegroundWindow() == getWindowHandle(); } @@ -203,11 +245,13 @@ class RegularWindowControllerWin32 extends RegularWindowController @override bool get isFullscreen { + _ensureNotDestroyed(); return _getFullscreen(getWindowHandle()); } @override void setSize(Size? size) { + _ensureNotDestroyed(); final Pointer<_WindowSizeRequest> request = ffi.calloc<_WindowSizeRequest>(); request.ref.hasSize = size != null; request.ref.width = size?.width ?? 0; @@ -218,6 +262,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @override void setConstraints(BoxConstraints constraints) { + _ensureNotDestroyed(); final Pointer<_WindowConstraints> request = ffi.calloc<_WindowConstraints>(); request.ref.from(constraints); _setWindowConstraints(getWindowHandle(), request); @@ -269,7 +314,7 @@ class RegularWindowControllerWin32 extends RegularWindowController } /// Returns HWND pointer to the top level window. - Pointer getWindowHandle() { + HWND getWindowHandle() { _ensureNotDestroyed(); return _getWindowHandle(PlatformDispatcher.instance.engineId!, rootView.viewId); } @@ -280,9 +325,6 @@ class RegularWindowControllerWin32 extends RegularWindowController } } - final RegularWindowControllerDelegate _delegate; - bool _destroyed = false; - @override void destroy() { if (_destroyed) { @@ -315,8 +357,6 @@ class RegularWindowControllerWin32 extends RegularWindowController return null; } - final WindowingOwnerWin32 _owner; - @Native)>( symbol: 'InternalFlutterWindows_WindowManager_CreateRegularWindow', ) From bcdd3c5a777185559c2fbcebfca03bd421fb1c40 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 7 Aug 2025 15:42:18 -0400 Subject: [PATCH 003/720] Making the BaseWindowController be listenable so that updates come through --- packages/flutter/lib/src/widgets/_window.dart | 13 ++-- .../lib/src/widgets/_window_win32.dart | 60 +++++++++++++++---- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/packages/flutter/lib/src/widgets/_window.dart b/packages/flutter/lib/src/widgets/_window.dart index 27de4f3fb72d3..ff78d3a21337f 100644 --- a/packages/flutter/lib/src/widgets/_window.dart +++ b/packages/flutter/lib/src/widgets/_window.dart @@ -19,6 +19,7 @@ import 'dart:ui' show Display, FlutterView; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; import '../foundation/_features.dart'; import 'binding.dart'; @@ -62,7 +63,7 @@ See: https://github.com/flutter/flutter/issues/30701. /// /// * [RegularWindowController], the controller for regular top-level windows. @internal -sealed class BaseWindowController { +sealed class BaseWindowController extends ChangeNotifier { /// The current size of the drawable area of the window. /// /// This might differ from the requested size. @@ -506,10 +507,14 @@ class RegularWindow extends StatelessWidget { @internal @override Widget build(BuildContext context) { - return WindowScope( - controller: controller, - child: View(view: controller.rootView, child: child), + return ListenableBuilder( + listenable: controller, + builder: (BuildContext context, Widget? _) => WindowScope( + controller: controller, + child: View(view: controller.rootView, child: child), + ), ); + ; } } diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index ffab48019e953..22e7e82f30b0a 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -16,6 +16,8 @@ import 'binding.dart'; typedef HWND = Pointer; const int _WM_SIZE = 0x0005; +const int _WM_FOCUS = 0x0007; +const int _WM_KILLFOCUS = 0x0008; const int _WM_CLOSE = 0x0010; const int _SW_RESTORE = 9; @@ -29,13 +31,20 @@ const int _SW_MINIMIZE = 6; /// When finished handling messages, implementations should deregister /// themselves with [WindowingOwnerWIn32.removeMessageHandler]. /// +/// {@macro flutter.widgets.windowing.experimental} +/// /// See also: /// /// * [WindowingOwnerWin32], the class that manages these handlers. +@internal abstract class WindowsMessageHandler { - /// Handles a window message. Returned value, if not null will be - /// returned to the system as LRESULT and will stop all other - /// handlers from being called. + /// Handles a window message. + /// + /// Returned value, if not null will be returned to the system as LRESULT + /// and will stop all other handlers from being called. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal int? handleWindowsMessage( FlutterView view, HWND windowHandle, @@ -50,14 +59,24 @@ abstract class WindowsMessageHandler { /// If [Platform.isWindows] is false, then the constructor will throw an /// [UnsupportedError]. /// +/// {@macro flutter.widgets.windowing.experimental} +/// /// See also: /// /// * [WindowingOwner], the abstract class that manages native windows. +@internal class WindowingOwnerWin32 extends WindowingOwner { /// Creates a new [WindowingOwnerWin32] instance. /// /// If [Platform.isWindows] is false, then this constructor will throw an /// [UnsupportedError]. + /// + /// {@macro flutter.widgets.windowing.experimental} + /// + /// See also: + /// + /// * [WindowingOwner], the abstract class that manages native windows. + @internal WindowingOwnerWin32() { if (!Platform.isWindows) { UnsupportedError('Only available on the Win32 platform'); @@ -73,6 +92,7 @@ class WindowingOwnerWin32 extends WindowingOwner { final List _messageHandlers = []; + @internal @override RegularWindowController createRegularWindowController({ Size? preferredSize, @@ -101,10 +121,13 @@ class WindowingOwnerWin32 extends WindowingOwner { /// Callers must remove their message handlers using /// [WindowingOwnerWin32.removeMessageHandler]. /// + /// {@macro flutter.widgets.windowing.experimental} + /// /// See also: /// /// * [WindowsMessageHandler], the interface for message handlers. /// * [WindowingOwnerWin32.removeMessageHandler], to remove message handlers. + @internal void addMessageHandler(WindowsMessageHandler handler) { if (_messageHandlers.contains(handler)) { return; @@ -117,10 +140,13 @@ class WindowingOwnerWin32 extends WindowingOwner { /// /// If the handler has not been registered, this method has no effect. /// + /// {@macro flutter.widgets.windowing.experimental} + /// /// See also: /// /// * [WindowsMessageHandler], the interface for message handlers. /// * [WindowingOwnerWin32.addMessageHandler], to register message handlers. + @internal void removeMessageHandler(WindowsMessageHandler handler) { _messageHandlers.remove(handler); } @@ -146,6 +172,7 @@ class WindowingOwnerWin32 extends WindowingOwner { } } + @internal @override bool hasTopLevelWindows() { return _hasTopLevelWindows(PlatformDispatcher.instance.engineId!); @@ -164,6 +191,12 @@ class WindowingOwnerWin32 extends WindowingOwner { /// /// If [Platform.isWindows] is false, then the constructor will throw an /// [UnsupportedError]. +/// +/// {@macro flutter.widgets.windowing.experimental} +/// +/// See also: +/// +/// * [RegularWindowController], the base class for regular windows. class RegularWindowControllerWin32 extends RegularWindowController implements WindowsMessageHandler { /// Creates a new regular window controller for Win32. @@ -173,6 +206,12 @@ class RegularWindowControllerWin32 extends RegularWindowController /// /// When this constructor completes the native window has been created and /// has a view associated with it. + /// {@macro flutter.widgets.windowing.experimental} + /// + /// See also: + /// + /// * [RegularWindowController], the base class for regular windows. + @internal RegularWindowControllerWin32({ required WindowingOwnerWin32 owner, required RegularWindowControllerDelegate delegate, @@ -267,6 +306,8 @@ class RegularWindowControllerWin32 extends RegularWindowController request.ref.from(constraints); _setWindowConstraints(getWindowHandle(), request); ffi.calloc.free(request); + + notifyListeners(); } @override @@ -275,6 +316,8 @@ class RegularWindowControllerWin32 extends RegularWindowController final Pointer titlePointer = title.toNativeUtf16(); _setWindowTitle(getWindowHandle(), titlePointer); ffi.calloc.free(titlePointer); + + notifyListeners(); } @override @@ -351,8 +394,8 @@ class RegularWindowControllerWin32 extends RegularWindowController if (message == _WM_CLOSE) { _delegate.onWindowCloseRequested(this); return 0; - } else if (message == _WM_SIZE) { - // TODO: notify context of size change + } else if (message == _WM_SIZE || message == _WM_FOCUS || message == _WM_KILLFOCUS) { + notifyListeners(); } return null; } @@ -375,13 +418,6 @@ class RegularWindowControllerWin32 extends RegularWindowController ) external static _ActualWindowSize _getWindowContentSize(Pointer windowHandle); - @Native, Pointer, Int32)>(symbol: 'GetWindowTextW') - external static void _getWindowTitle( - Pointer windowHandle, - Pointer title, - int maxLength, - ); - @Native, Pointer)>(symbol: 'SetWindowTextW') external static void _setWindowTitle(Pointer windowHandle, Pointer title); From 31bc08b822023f9a509bfbf3b2ac3ade55d43d35 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Thu, 7 Aug 2025 15:50:47 -0400 Subject: [PATCH 004/720] Using notifiers in the example application --- examples/multi_window_ref_app/lib/app/main_window.dart | 1 + .../lib/app/regular_window_content.dart | 1 + .../lib/app/regular_window_edit_dialog.dart | 6 +++--- .../lib/app/window_controller_render.dart | 1 + .../multi_window_ref_app/lib/app/window_manager_model.dart | 1 + examples/multi_window_ref_app/lib/main.dart | 1 + packages/flutter/lib/src/widgets/_window_win32.dart | 5 ++--- 7 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/multi_window_ref_app/lib/app/main_window.dart b/examples/multi_window_ref_app/lib/app/main_window.dart index 3a17f7e73b9c3..f045a49f71f5a 100644 --- a/examples/multi_window_ref_app/lib/app/main_window.dart +++ b/examples/multi_window_ref_app/lib/app/main_window.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: implementation_imports import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/_window.dart'; diff --git a/examples/multi_window_ref_app/lib/app/regular_window_content.dart b/examples/multi_window_ref_app/lib/app/regular_window_content.dart index 2c42e7c09c6b6..351439ccf2ec8 100644 --- a/examples/multi_window_ref_app/lib/app/regular_window_content.dart +++ b/examples/multi_window_ref_app/lib/app/regular_window_content.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: implementation_imports import 'package:flutter/material.dart'; import 'window_controller_render.dart'; diff --git a/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart b/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart index 3dee198a07464..3ce900d42f22b 100644 --- a/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart +++ b/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: implementation_imports import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/_window.dart'; @@ -63,8 +64,7 @@ class _RegularWindowEditDialogState extends State<_RegularWindowEditDialog> { ); titleController = TextEditingController(text: initialTitle); - // TODO: Re-add listeners (somehow?) - // widget.controller.addListener(_onNotification); + widget.controller.addListener(_onNotification); } void _onNotification() { @@ -99,7 +99,7 @@ class _RegularWindowEditDialogState extends State<_RegularWindowEditDialog> { @override void dispose() { super.dispose(); - // widget.controller.removeListener(_onNotification); + widget.controller.removeListener(_onNotification); } @override diff --git a/examples/multi_window_ref_app/lib/app/window_controller_render.dart b/examples/multi_window_ref_app/lib/app/window_controller_render.dart index 80036d7b8f74e..ba6aba8e66a58 100644 --- a/examples/multi_window_ref_app/lib/app/window_controller_render.dart +++ b/examples/multi_window_ref_app/lib/app/window_controller_render.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: implementation_imports import 'package:flutter/material.dart'; import 'regular_window_content.dart'; diff --git a/examples/multi_window_ref_app/lib/app/window_manager_model.dart b/examples/multi_window_ref_app/lib/app/window_manager_model.dart index 2e5920f53115a..ccddb3ff6142e 100644 --- a/examples/multi_window_ref_app/lib/app/window_manager_model.dart +++ b/examples/multi_window_ref_app/lib/app/window_manager_model.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: implementation_imports import 'package:flutter/widgets.dart'; import 'package:flutter/src/widgets/_window.dart'; diff --git a/examples/multi_window_ref_app/lib/main.dart b/examples/multi_window_ref_app/lib/main.dart index 0f60fcaacee73..d671b9d36b71d 100644 --- a/examples/multi_window_ref_app/lib/main.dart +++ b/examples/multi_window_ref_app/lib/main.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. // ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: implementation_imports import 'dart:io'; diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index 22e7e82f30b0a..d48e652d42b8f 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -16,8 +16,7 @@ import 'binding.dart'; typedef HWND = Pointer; const int _WM_SIZE = 0x0005; -const int _WM_FOCUS = 0x0007; -const int _WM_KILLFOCUS = 0x0008; +const int _WM_ACTIVATE = 0x0006; const int _WM_CLOSE = 0x0010; const int _SW_RESTORE = 9; @@ -394,7 +393,7 @@ class RegularWindowControllerWin32 extends RegularWindowController if (message == _WM_CLOSE) { _delegate.onWindowCloseRequested(this); return 0; - } else if (message == _WM_SIZE || message == _WM_FOCUS || message == _WM_KILLFOCUS) { + } else if (message == _WM_SIZE || message == _WM_ACTIVATE) { notifyListeners(); } return null; From 04c662a89427cb6f85bc43544d1c3ccc375abaf3 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 7 Aug 2025 16:57:28 -0400 Subject: [PATCH 005/720] Abstracting away the win32 interface for testing purposes --- .../lib/src/widgets/_window_win32.dart | 299 ++++++++++++++---- 1 file changed, 237 insertions(+), 62 deletions(-) diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index d48e652d42b8f..c880995bc8fe3 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -81,15 +81,25 @@ class WindowingOwnerWin32 extends WindowingOwner { UnsupportedError('Only available on the Win32 platform'); } - final Pointer<_WindowingInitRequest> request = ffi.calloc<_WindowingInitRequest>() - ..ref.onMessage = NativeCallable)>.isolateLocal( + final Pointer request = ffi.calloc() + ..ref.onMessage = NativeCallable)>.isolateLocal( _onMessage, ).nativeFunction; - _initializeWindowing(PlatformDispatcher.instance.engineId!, request); + win32platforminterface.initialize(PlatformDispatcher.instance.engineId!, request); ffi.calloc.free(request); } final List _messageHandlers = []; + final Win32PlatformInterface _win32platforminterface = _NativeWin32PlatformInterface(); + + /// Getter for the [Win32PlatformInterface]. + /// + /// Overriding this is only useful for testing. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + @protected + Win32PlatformInterface get win32platforminterface => _win32platforminterface; @internal @override @@ -150,7 +160,7 @@ class WindowingOwnerWin32 extends WindowingOwner { _messageHandlers.remove(handler); } - void _onMessage(Pointer<_WindowsMessage> message) { + void _onMessage(Pointer message) { final List handlers = List.from(_messageHandlers); final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( (FlutterView view) => view.viewId == message.ref.viewId, @@ -174,16 +184,8 @@ class WindowingOwnerWin32 extends WindowingOwner { @internal @override bool hasTopLevelWindows() { - return _hasTopLevelWindows(PlatformDispatcher.instance.engineId!); + return win32platforminterface.hasTopLevelWindows(PlatformDispatcher.instance.engineId!); } - - @Native(symbol: 'InternalFlutterWindows_WindowManager_HasTopLevelWindows') - external static bool _hasTopLevelWindows(int engineId); - - @Native)>( - symbol: 'InternalFlutterWindows_WindowManager_Initialize', - ) - external static void _initializeWindowing(int engineId, Pointer<_WindowingInitRequest> request); } /// Implementation of [RegularWindowController] for the Windows platform. @@ -205,6 +207,7 @@ class RegularWindowControllerWin32 extends RegularWindowController /// /// When this constructor completes the native window has been created and /// has a view associated with it. + /// /// {@macro flutter.widgets.windowing.experimental} /// /// See also: @@ -221,11 +224,14 @@ class RegularWindowControllerWin32 extends RegularWindowController _delegate = delegate, super.empty() { owner.addMessageHandler(this); - final Pointer<_WindowCreationRequest> request = ffi.calloc<_WindowCreationRequest>() + final Pointer request = ffi.calloc() ..ref.preferredSize.from(preferredSize) ..ref.preferredConstraints.from(preferredConstraints) ..ref.title = (title ?? 'Regular window').toNativeUtf16(); - final int viewId = _createWindow(PlatformDispatcher.instance.engineId!, request); + final int viewId = _owner.win32platforminterface.createWindow( + PlatformDispatcher.instance.engineId!, + request, + ); ffi.calloc.free(request); final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( (FlutterView view) => view.viewId == viewId, @@ -240,7 +246,9 @@ class RegularWindowControllerWin32 extends RegularWindowController @override Size get contentSize { _ensureNotDestroyed(); - final _ActualWindowSize size = _getWindowContentSize(getWindowHandle()); + final ActualContentSize size = _owner.win32platforminterface.getWindowContentSize( + getWindowHandle(), + ); final Size result = Size(size.width, size.height); return result; } @@ -248,7 +256,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @override String get title { _ensureNotDestroyed(); - final int length = _getWindowTextLength(getWindowHandle()); + final int length = _owner.win32platforminterface.getWindowTextLength(getWindowHandle()); if (length == 0) { return ''; } @@ -256,7 +264,7 @@ class RegularWindowControllerWin32 extends RegularWindowController final Pointer data = ffi.calloc(length + 1); try { final Pointer buffer = data.cast(); - _getWindowText(getWindowHandle(), buffer, length + 1); + _owner.win32platforminterface.getWindowText(getWindowHandle(), buffer, length + 1); return buffer.toDartString(); } finally { ffi.calloc.free(data); @@ -266,44 +274,44 @@ class RegularWindowControllerWin32 extends RegularWindowController @override bool get isActivated { _ensureNotDestroyed(); - return _getForegroundWindow() == getWindowHandle(); + return _owner.win32platforminterface.getForegroundWindow() == getWindowHandle(); } @override bool get isMaximized { _ensureNotDestroyed(); - return _isZoomed(getWindowHandle()) != 0; + return _owner.win32platforminterface.isZoomed(getWindowHandle()) != 0; } @override bool get isMinimized { _ensureNotDestroyed(); - return _isIconic(getWindowHandle()) != 0; + return _owner.win32platforminterface.isIconic(getWindowHandle()) != 0; } @override bool get isFullscreen { _ensureNotDestroyed(); - return _getFullscreen(getWindowHandle()); + return _owner.win32platforminterface.getFullscreen(getWindowHandle()); } @override void setSize(Size? size) { _ensureNotDestroyed(); - final Pointer<_WindowSizeRequest> request = ffi.calloc<_WindowSizeRequest>(); + final Pointer request = ffi.calloc(); request.ref.hasSize = size != null; request.ref.width = size?.width ?? 0; request.ref.height = size?.height ?? 0; - _setWindowContentSize(getWindowHandle(), request); + _owner.win32platforminterface.setWindowContentSize(getWindowHandle(), request); ffi.calloc.free(request); } @override void setConstraints(BoxConstraints constraints) { _ensureNotDestroyed(); - final Pointer<_WindowConstraints> request = ffi.calloc<_WindowConstraints>(); + final Pointer request = ffi.calloc(); request.ref.from(constraints); - _setWindowConstraints(getWindowHandle(), request); + _owner.win32platforminterface.setWindowConstraints(getWindowHandle(), request); ffi.calloc.free(request); notifyListeners(); @@ -313,7 +321,7 @@ class RegularWindowControllerWin32 extends RegularWindowController void setTitle(String title) { _ensureNotDestroyed(); final Pointer titlePointer = title.toNativeUtf16(); - _setWindowTitle(getWindowHandle(), titlePointer); + _owner.win32platforminterface.setWindowTitle(getWindowHandle(), titlePointer); ffi.calloc.free(titlePointer); notifyListeners(); @@ -322,16 +330,16 @@ class RegularWindowControllerWin32 extends RegularWindowController @override void activate() { _ensureNotDestroyed(); - _showWindow(getWindowHandle(), _SW_RESTORE); + _owner.win32platforminterface.showWindow(getWindowHandle(), _SW_RESTORE); } @override void setMaximized(bool maximized) { _ensureNotDestroyed(); if (maximized) { - _showWindow(getWindowHandle(), _SW_MAXIMIZE); + _owner.win32platforminterface.showWindow(getWindowHandle(), _SW_MAXIMIZE); } else { - _showWindow(getWindowHandle(), _SW_RESTORE); + _owner.win32platforminterface.showWindow(getWindowHandle(), _SW_RESTORE); } } @@ -339,26 +347,29 @@ class RegularWindowControllerWin32 extends RegularWindowController void setMinimized(bool minimized) { _ensureNotDestroyed(); if (minimized) { - _showWindow(getWindowHandle(), _SW_MINIMIZE); + _owner.win32platforminterface.showWindow(getWindowHandle(), _SW_MINIMIZE); } else { - _showWindow(getWindowHandle(), _SW_RESTORE); + _owner.win32platforminterface.showWindow(getWindowHandle(), _SW_RESTORE); } } @override void setFullscreen(bool fullscreen, {Display? display}) { - final Pointer<_FullscreenRequest> request = ffi.calloc<_FullscreenRequest>(); + final Pointer request = ffi.calloc(); request.ref.hasDisplayId = false; request.ref.displayId = display?.id ?? 0; request.ref.fullscreen = fullscreen; - _setFullscreen(getWindowHandle(), request); + _owner.win32platforminterface.setFullscreen(getWindowHandle(), request); ffi.calloc.free(request); } /// Returns HWND pointer to the top level window. HWND getWindowHandle() { _ensureNotDestroyed(); - return _getWindowHandle(PlatformDispatcher.instance.engineId!, rootView.viewId); + return _owner.win32platforminterface.getWindowHandle( + PlatformDispatcher.instance.engineId!, + rootView.viewId, + ); } void _ensureNotDestroyed() { @@ -372,7 +383,7 @@ class RegularWindowControllerWin32 extends RegularWindowController if (_destroyed) { return; } - _destroyWindow(getWindowHandle()); + _owner.win32platforminterface.destroyWindow(getWindowHandle()); _destroyed = true; _delegate.onWindowDestroyed(); _owner.removeMessageHandler(this); @@ -398,11 +409,139 @@ class RegularWindowControllerWin32 extends RegularWindowController } return null; } +} + +/// Abstract class that wraps native access to the win32 API via FFI. +/// +/// Used by [WindowingOwnerWin32]. +/// +/// Overriding this is only useful for testing purposes. +/// +/// {@macro flutter.widgets.windowing.experimental} +/// +/// See also: +/// +/// * [WindowingOwnerWin32], the user of this interface. +@visibleForTesting +@internal +abstract class Win32PlatformInterface { + bool hasTopLevelWindows(int engineId); + void initialize(int engineId, Pointer request); + int createWindow(int engineId, Pointer request); + HWND getWindowHandle(int engineId, int viewId); + void destroyWindow(HWND windowHandle); + ActualContentSize getWindowContentSize(HWND windowHandle); + void setWindowTitle(HWND windowHandle, Pointer title); + void setWindowContentSize(HWND windowHandle, Pointer size); + void setWindowConstraints(HWND windowHandle, Pointer constraints); + void showWindow(HWND windowHandle, int command); + int isIconic(HWND windowHandle); + int isZoomed(HWND windowHandle); + void setFullscreen(HWND windowHandle, Pointer request); + bool getFullscreen(HWND windowHandle); + int getWindowTextLength(HWND windowHandle); + int getWindowText(HWND windowHandle, Pointer lpString, int maxLength); + HWND getForegroundWindow(); +} + +class _NativeWin32PlatformInterface extends Win32PlatformInterface { + @override + bool hasTopLevelWindows(int engineId) { + return _hasTopLevelWindows(engineId); + } + + @override + void initialize(int engineId, Pointer request) { + _initializeWindowing(engineId, request); + } + + @override + int createWindow(int engineId, Pointer request) { + return _createWindow(engineId, request); + } + + @override + HWND getWindowHandle(int engineId, int viewId) { + return _getWindowHandle(engineId, viewId); + } + + @override + void destroyWindow(HWND windowHandle) { + return _destroyWindow(windowHandle); + } + + @override + ActualContentSize getWindowContentSize(HWND windowHandle) { + return _getWindowContentSize(windowHandle); + } + + @override + void setWindowTitle(HWND windowHandle, Pointer title) { + _setWindowTitle(windowHandle, title); + } + + @override + void setWindowContentSize(HWND windowHandle, Pointer size) { + _setWindowContentSize(windowHandle, size); + } + + @override + void setWindowConstraints(HWND windowHandle, Pointer constraints) { + _setWindowConstraints(windowHandle, constraints); + } + + @override + void showWindow(HWND windowHandle, int command) { + _showWindow(windowHandle, command); + } + + @override + int isIconic(HWND windowHandle) { + return _isIconic(windowHandle); + } + + @override + int isZoomed(HWND windowHandle) { + return _isZoomed(windowHandle); + } + + @override + void setFullscreen(HWND windowHandle, Pointer request) { + _setFullscreen(windowHandle, request); + } + + @override + bool getFullscreen(HWND windowHandle) { + return _getFullscreen(windowHandle); + } + + @override + int getWindowTextLength(HWND windowHandle) { + return _getWindowTextLength(windowHandle); + } + + @override + int getWindowText(HWND windowHandle, Pointer lpString, int maxLength) { + return _getWindowText(windowHandle, lpString, maxLength); + } + + @override + HWND getForegroundWindow() { + return _getForegroundWindow(); + } + + @Native(symbol: 'InternalFlutterWindows_WindowManager_HasTopLevelWindows') + external static bool _hasTopLevelWindows(int engineId); + + @Native)>( + symbol: 'InternalFlutterWindows_WindowManager_Initialize', + ) + external static void _initializeWindowing(int engineId, Pointer request); - @Native)>( + @Native)>( symbol: 'InternalFlutterWindows_WindowManager_CreateRegularWindow', ) - external static int _createWindow(int engineId, Pointer<_WindowCreationRequest> request); + external static int _createWindow(int engineId, Pointer request); @Native Function(Int64, Int64)>( symbol: 'InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle', @@ -412,28 +551,28 @@ class RegularWindowControllerWin32 extends RegularWindowController @Native)>(symbol: 'DestroyWindow') external static void _destroyWindow(Pointer windowHandle); - @Native<_ActualWindowSize Function(Pointer)>( + @Native)>( symbol: 'InternalFlutterWindows_WindowManager_GetWindowContentSize', ) - external static _ActualWindowSize _getWindowContentSize(Pointer windowHandle); + external static ActualContentSize _getWindowContentSize(HWND windowHandle); @Native, Pointer)>(symbol: 'SetWindowTextW') external static void _setWindowTitle(Pointer windowHandle, Pointer title); - @Native, Pointer<_WindowSizeRequest>)>( + @Native, Pointer)>( symbol: 'InternalFlutterWindows_WindowManager_SetWindowSize', ) external static void _setWindowContentSize( Pointer windowHandle, - Pointer<_WindowSizeRequest> size, + Pointer size, ); - @Native, Pointer<_WindowConstraints>)>( + @Native, Pointer)>( symbol: 'InternalFlutterWindows_WindowManager_SetWindowConstraints', ) external static void _setWindowConstraints( Pointer windowHandle, - Pointer<_WindowConstraints> constraints, + Pointer constraints, ); @Native, Int32)>(symbol: 'ShowWindow') @@ -445,12 +584,12 @@ class RegularWindowControllerWin32 extends RegularWindowController @Native)>(symbol: 'IsZoomed') external static int _isZoomed(Pointer windowHandle); - @Native, Pointer<_FullscreenRequest>)>( + @Native, Pointer)>( symbol: 'InternalFlutterWindows_WindowManager_SetFullscreen', ) external static void _setFullscreen( Pointer windowHandle, - Pointer<_FullscreenRequest> request, + Pointer request, ); @Native)>( @@ -472,12 +611,34 @@ class RegularWindowControllerWin32 extends RegularWindowController external static Pointer _getForegroundWindow(); } -/// Request to initialize windowing system. -final class _WindowingInitRequest extends Struct { - external Pointer)>> onMessage; +/// Payload for the creation method used by [Win32PlatformInterface.createWindow]. +/// +/// {@macro flutter.widgets.windowing.experimental} +@visibleForTesting +@internal +final class WindowCreationRequest extends Struct { + external WindowSizeRequest preferredSize; + external WindowConstraintsRequest preferredConstraints; + external Pointer title; +} + +/// Payload for the initialization request for the windowing subsystem used +/// by the constructor for [WindowingOwnerWin32]. +/// +/// {@macro flutter.widgets.windowing.experimental} +@visibleForTesting +@internal +final class WindowingInitRequest extends Struct { + external Pointer)>> onMessage; } -final class _WindowSizeRequest extends Struct { +/// Payload for the size of a window used by [WindowCreationRequest] and +/// [Win32PlatformInterface.setWindowContentSize]. +/// +/// {@macro flutter.widgets.windowing.experimental} +@visibleForTesting +@internal +final class WindowSizeRequest extends Struct { @Bool() external bool hasSize; @@ -494,7 +655,13 @@ final class _WindowSizeRequest extends Struct { } } -final class _WindowConstraints extends Struct { +/// Payload for the constraints of a window used by [WindowCreationRequest] and +/// [Win32PlatformInterface.setWindowConstraints]. +/// +/// {@macro flutter.widgets.windowing.experimental} +@visibleForTesting +@internal +final class WindowConstraintsRequest extends Struct { @Bool() external bool hasConstraints; @@ -519,15 +686,12 @@ final class _WindowConstraints extends Struct { } } -final class _WindowCreationRequest extends Struct { - external _WindowSizeRequest preferredSize; - external _WindowConstraints preferredConstraints; - external Pointer title; -} - -/// Windows message received for all top level windows (regardless whether -/// they are created using a windowing controller). -final class _WindowsMessage extends Struct { +/// A message received for all toplevel windows, used by [WindowingInitRequest]. +/// +/// {@macro flutter.widgets.windowing.experimental} +@visibleForTesting +@internal +final class WindowsMessage extends Struct { @Int64() external int viewId; @@ -549,7 +713,13 @@ final class _WindowsMessage extends Struct { external bool handled; } -final class _ActualWindowSize extends Struct { +/// Holds the real size of a window as retrieved from +/// [Win32PlatformInterface.getWindowContentSize]. +/// +/// {@macro flutter.widgets.windowing.experimental} +@visibleForTesting +@internal +final class ActualContentSize extends Struct { @Double() external double width; @@ -557,7 +727,12 @@ final class _ActualWindowSize extends Struct { external double height; } -final class _FullscreenRequest extends Struct { +/// Payload for the [Win32PlatformInterface.setFullscreen] request. +/// +/// {@macro flutter.widgets.windowing.experimental} +@visibleForTesting +@internal +final class WindowFullscreenRequest extends Struct { @Bool() external bool fullscreen; From 57bf59bc3131ebba20bbeb6b5e84ac7303223880 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Fri, 8 Aug 2025 13:54:16 -0400 Subject: [PATCH 006/720] Testing apparatus for the win32 platform --- .../lib/src/widgets/_window_win32.dart | 90 +++++---- .../test/widgets/window_win32_test.dart | 172 ++++++++++++++++++ packages/flutter_test/lib/src/window.dart | 3 + 3 files changed, 232 insertions(+), 33 deletions(-) create mode 100644 packages/flutter/test/widgets/window_win32_test.dart diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index c880995bc8fe3..b5640b5ee863d 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -68,7 +68,8 @@ class WindowingOwnerWin32 extends WindowingOwner { /// Creates a new [WindowingOwnerWin32] instance. /// /// If [Platform.isWindows] is false, then this constructor will throw an - /// [UnsupportedError]. + /// [UnsupportedError] + /// /// /// {@macro flutter.widgets.windowing.experimental} /// @@ -76,30 +77,53 @@ class WindowingOwnerWin32 extends WindowingOwner { /// /// * [WindowingOwner], the abstract class that manages native windows. @internal - WindowingOwnerWin32() { + WindowingOwnerWin32() + : win32PlatformInterface = _NativeWin32PlatformInterface(), + platformDispatcher = PlatformDispatcher.instance { if (!Platform.isWindows) { - UnsupportedError('Only available on the Win32 platform'); + throw UnsupportedError('Only available on the Win32 platform'); } final Pointer request = ffi.calloc() ..ref.onMessage = NativeCallable)>.isolateLocal( _onMessage, ).nativeFunction; - win32platforminterface.initialize(PlatformDispatcher.instance.engineId!, request); + win32PlatformInterface.initialize(platformDispatcher.engineId!, request); + ffi.calloc.free(request); + } + + /// Creates a new [WindowingOwnerWin32] instance for testing purposes. + /// + /// This constructor will not throw when we are not on the win32 platform. + /// + /// This constructor takes a [win32PlatformInterface], which is most likely + /// a mock interface in addition to a custom [platformDispatcher] so that + /// [PlatformDispatcher.engineId] can successfully be mocked. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + @visibleForTesting + WindowingOwnerWin32.test({ + required this.win32PlatformInterface, + required this.platformDispatcher, + }) { + final Pointer request = ffi.calloc() + ..ref.onMessage = NativeCallable)>.isolateLocal( + _onMessage, + ).nativeFunction; + win32PlatformInterface.initialize(platformDispatcher.engineId!, request); ffi.calloc.free(request); } final List _messageHandlers = []; - final Win32PlatformInterface _win32platforminterface = _NativeWin32PlatformInterface(); - /// Getter for the [Win32PlatformInterface]. - /// - /// Overriding this is only useful for testing. + /// Provides access to the native win32 backend. /// /// {@macro flutter.widgets.windowing.experimental} @internal - @protected - Win32PlatformInterface get win32platforminterface => _win32platforminterface; + final Win32PlatformInterface win32PlatformInterface; + + final PlatformDispatcher platformDispatcher; @internal @override @@ -184,7 +208,7 @@ class WindowingOwnerWin32 extends WindowingOwner { @internal @override bool hasTopLevelWindows() { - return win32platforminterface.hasTopLevelWindows(PlatformDispatcher.instance.engineId!); + return win32PlatformInterface.hasTopLevelWindows(platformDispatcher.engineId!); } } @@ -228,8 +252,8 @@ class RegularWindowControllerWin32 extends RegularWindowController ..ref.preferredSize.from(preferredSize) ..ref.preferredConstraints.from(preferredConstraints) ..ref.title = (title ?? 'Regular window').toNativeUtf16(); - final int viewId = _owner.win32platforminterface.createWindow( - PlatformDispatcher.instance.engineId!, + final int viewId = _owner.win32PlatformInterface.createWindow( + _owner.platformDispatcher.engineId!, request, ); ffi.calloc.free(request); @@ -246,7 +270,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @override Size get contentSize { _ensureNotDestroyed(); - final ActualContentSize size = _owner.win32platforminterface.getWindowContentSize( + final ActualContentSize size = _owner.win32PlatformInterface.getWindowContentSize( getWindowHandle(), ); final Size result = Size(size.width, size.height); @@ -256,7 +280,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @override String get title { _ensureNotDestroyed(); - final int length = _owner.win32platforminterface.getWindowTextLength(getWindowHandle()); + final int length = _owner.win32PlatformInterface.getWindowTextLength(getWindowHandle()); if (length == 0) { return ''; } @@ -264,7 +288,7 @@ class RegularWindowControllerWin32 extends RegularWindowController final Pointer data = ffi.calloc(length + 1); try { final Pointer buffer = data.cast(); - _owner.win32platforminterface.getWindowText(getWindowHandle(), buffer, length + 1); + _owner.win32PlatformInterface.getWindowText(getWindowHandle(), buffer, length + 1); return buffer.toDartString(); } finally { ffi.calloc.free(data); @@ -274,25 +298,25 @@ class RegularWindowControllerWin32 extends RegularWindowController @override bool get isActivated { _ensureNotDestroyed(); - return _owner.win32platforminterface.getForegroundWindow() == getWindowHandle(); + return _owner.win32PlatformInterface.getForegroundWindow() == getWindowHandle(); } @override bool get isMaximized { _ensureNotDestroyed(); - return _owner.win32platforminterface.isZoomed(getWindowHandle()) != 0; + return _owner.win32PlatformInterface.isZoomed(getWindowHandle()) != 0; } @override bool get isMinimized { _ensureNotDestroyed(); - return _owner.win32platforminterface.isIconic(getWindowHandle()) != 0; + return _owner.win32PlatformInterface.isIconic(getWindowHandle()) != 0; } @override bool get isFullscreen { _ensureNotDestroyed(); - return _owner.win32platforminterface.getFullscreen(getWindowHandle()); + return _owner.win32PlatformInterface.getFullscreen(getWindowHandle()); } @override @@ -302,7 +326,7 @@ class RegularWindowControllerWin32 extends RegularWindowController request.ref.hasSize = size != null; request.ref.width = size?.width ?? 0; request.ref.height = size?.height ?? 0; - _owner.win32platforminterface.setWindowContentSize(getWindowHandle(), request); + _owner.win32PlatformInterface.setWindowContentSize(getWindowHandle(), request); ffi.calloc.free(request); } @@ -311,7 +335,7 @@ class RegularWindowControllerWin32 extends RegularWindowController _ensureNotDestroyed(); final Pointer request = ffi.calloc(); request.ref.from(constraints); - _owner.win32platforminterface.setWindowConstraints(getWindowHandle(), request); + _owner.win32PlatformInterface.setWindowConstraints(getWindowHandle(), request); ffi.calloc.free(request); notifyListeners(); @@ -321,7 +345,7 @@ class RegularWindowControllerWin32 extends RegularWindowController void setTitle(String title) { _ensureNotDestroyed(); final Pointer titlePointer = title.toNativeUtf16(); - _owner.win32platforminterface.setWindowTitle(getWindowHandle(), titlePointer); + _owner.win32PlatformInterface.setWindowTitle(getWindowHandle(), titlePointer); ffi.calloc.free(titlePointer); notifyListeners(); @@ -330,16 +354,16 @@ class RegularWindowControllerWin32 extends RegularWindowController @override void activate() { _ensureNotDestroyed(); - _owner.win32platforminterface.showWindow(getWindowHandle(), _SW_RESTORE); + _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE); } @override void setMaximized(bool maximized) { _ensureNotDestroyed(); if (maximized) { - _owner.win32platforminterface.showWindow(getWindowHandle(), _SW_MAXIMIZE); + _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_MAXIMIZE); } else { - _owner.win32platforminterface.showWindow(getWindowHandle(), _SW_RESTORE); + _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE); } } @@ -347,9 +371,9 @@ class RegularWindowControllerWin32 extends RegularWindowController void setMinimized(bool minimized) { _ensureNotDestroyed(); if (minimized) { - _owner.win32platforminterface.showWindow(getWindowHandle(), _SW_MINIMIZE); + _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_MINIMIZE); } else { - _owner.win32platforminterface.showWindow(getWindowHandle(), _SW_RESTORE); + _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE); } } @@ -359,15 +383,15 @@ class RegularWindowControllerWin32 extends RegularWindowController request.ref.hasDisplayId = false; request.ref.displayId = display?.id ?? 0; request.ref.fullscreen = fullscreen; - _owner.win32platforminterface.setFullscreen(getWindowHandle(), request); + _owner.win32PlatformInterface.setFullscreen(getWindowHandle(), request); ffi.calloc.free(request); } /// Returns HWND pointer to the top level window. HWND getWindowHandle() { _ensureNotDestroyed(); - return _owner.win32platforminterface.getWindowHandle( - PlatformDispatcher.instance.engineId!, + return _owner.win32PlatformInterface.getWindowHandle( + _owner.platformDispatcher.engineId!, rootView.viewId, ); } @@ -383,7 +407,7 @@ class RegularWindowControllerWin32 extends RegularWindowController if (_destroyed) { return; } - _owner.win32platforminterface.destroyWindow(getWindowHandle()); + _owner.win32PlatformInterface.destroyWindow(getWindowHandle()); _destroyed = true; _delegate.onWindowDestroyed(); _owner.removeMessageHandler(this); @@ -467,7 +491,7 @@ class _NativeWin32PlatformInterface extends Win32PlatformInterface { @override void destroyWindow(HWND windowHandle) { - return _destroyWindow(windowHandle); + _destroyWindow(windowHandle); } @override diff --git a/packages/flutter/test/widgets/window_win32_test.dart b/packages/flutter/test/widgets/window_win32_test.dart new file mode 100644 index 0000000000000..e35f7a12ccacf --- /dev/null +++ b/packages/flutter/test/widgets/window_win32_test.dart @@ -0,0 +1,172 @@ +// 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:ffi'; +import 'dart:io'; +import 'package:ffi/ffi.dart' as ffi; +import 'package:flutter/semantics.dart'; +import 'package:flutter/src/foundation/binding.dart'; + +import 'package:flutter/src/widgets/_window_win32.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Win32 window test', () { + group('Platform.isWindows is false', () { + setUp(() { + Platform.isWindows = false; + }); + + test('WindowingOwnerWin32 constructor throws when not on windows', () { + expect(() => WindowingOwnerWin32(), throwsUnsupportedError); + }); + }); + + testWidgets('WindowingOwner32 constructor does NOT throw when on windows', ( + WidgetTester tester, + ) async { + WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface(), + platformDispatcher: tester.platformDispatcher, + ); + }); + }); +} + +class _MockWin32PlatformInterface extends Win32PlatformInterface { + _MockWin32PlatformInterface({ + this.onInitialize, + this.onCreateWindow, + this.onDestroyWindow, + this.onGetContentSize, + this.onSetTitle, + this.onSetContentSize, + this.onSetConstraints, + this.onShowWindow, + this.onIsIconic, + this.onIsZoomed, + this.onSetFullscreen, + this.onGetFullscreen, + this.onGetWindowTextLength, + this.onGetWindowText, + this.onGetForegroundWindow, + }); + + final int viewId = 1; + final HWND hwnd = Pointer.fromAddress(0x8000); + final bool _hasToplevelWindows = true; + Pointer? size; + + final void Function(WindowingInitRequest)? onInitialize; + final void Function(int, WindowCreationRequest)? onCreateWindow; + final void Function(HWND)? onDestroyWindow; + final void Function(HWND)? onGetContentSize; + final void Function(HWND, Pointer)? onSetTitle; + final void Function(HWND, WindowSizeRequest)? onSetContentSize; + final void Function(HWND, WindowConstraintsRequest)? onSetConstraints; + final void Function(HWND, int)? onShowWindow; + final void Function(HWND)? onIsIconic; + final void Function(HWND)? onIsZoomed; + final void Function(HWND, WindowFullscreenRequest)? onSetFullscreen; + final void Function(HWND)? onGetFullscreen; + final void Function(HWND)? onGetWindowTextLength; + final void Function(HWND, Pointer, int)? onGetWindowText; + final VoidCallback? onGetForegroundWindow; + + @override + bool hasTopLevelWindows(int engineId) { + return _hasToplevelWindows; + } + + @override + void initialize(int engineId, Pointer request) { + onInitialize?.call(request.ref); + } + + @override + int createWindow(int engineId, Pointer request) { + onCreateWindow?.call(engineId, request.ref); + return viewId; + } + + @override + HWND getWindowHandle(int engineId, int viewId) { + return hwnd; + } + + @override + void destroyWindow(HWND windowHandle) { + onDestroyWindow?.call(windowHandle); + } + + @override + ActualContentSize getWindowContentSize(HWND windowHandle) { + onGetContentSize?.call(windowHandle); + size = ffi.calloc(); + size!.ref.width = 800; + size!.ref.height = 600; + return size!.ref; + } + + @override + void setWindowTitle(HWND windowHandle, Pointer title) { + onSetTitle?.call(windowHandle, title); + } + + @override + void setWindowContentSize(HWND windowHandle, Pointer size) { + onSetContentSize?.call(windowHandle, size.ref); + } + + @override + void setWindowConstraints(HWND windowHandle, Pointer constraints) { + onSetConstraints?.call(windowHandle, constraints.ref); + } + + @override + void showWindow(HWND windowHandle, int command) { + onShowWindow?.call(windowHandle, command); + } + + @override + int isIconic(HWND windowHandle) { + onIsIconic?.call(windowHandle); + return 0; + } + + @override + int isZoomed(HWND windowHandle) { + onIsZoomed?.call(windowHandle); + return 0; + } + + @override + void setFullscreen(HWND windowHandle, Pointer request) { + onSetFullscreen?.call(windowHandle, request.ref); + } + + @override + bool getFullscreen(HWND windowHandle) { + onGetFullscreen?.call(windowHandle); + return false; + } + + @override + int getWindowTextLength(HWND windowHandle) { + onGetWindowTextLength?.call(windowHandle); + return 0; + } + + @override + int getWindowText(HWND windowHandle, Pointer lpString, int maxLength) { + onGetWindowText?.call(windowHandle, lpString, maxLength); + return 0; + } + + @override + HWND getForegroundWindow() { + onGetForegroundWindow?.call(); + return hwnd; + } +} diff --git a/packages/flutter_test/lib/src/window.dart b/packages/flutter_test/lib/src/window.dart index 514894229475e..78e7af53eb5dc 100644 --- a/packages/flutter_test/lib/src/window.dart +++ b/packages/flutter_test/lib/src/window.dart @@ -175,6 +175,9 @@ class TestPlatformDispatcher implements PlatformDispatcher { : null; } + @override + int? get engineId => 1; + final Map _testViews = {}; final Map _testDisplays = {}; From 09e5c7b4c29a174d7c25e21fd87d8514a02b76e7 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Fri, 8 Aug 2025 14:53:09 -0400 Subject: [PATCH 007/720] Finishing off the tests for the win32 windowing owner --- .../test/widgets/window_win32_test.dart | 390 +++++++++++++++++- 1 file changed, 380 insertions(+), 10 deletions(-) diff --git a/packages/flutter/test/widgets/window_win32_test.dart b/packages/flutter/test/widgets/window_win32_test.dart index e35f7a12ccacf..bfaf38f98160d 100644 --- a/packages/flutter/test/widgets/window_win32_test.dart +++ b/packages/flutter/test/widgets/window_win32_test.dart @@ -2,17 +2,22 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ffi'; +import 'dart:ffi' hide Size; import 'dart:io'; import 'package:ffi/ffi.dart' as ffi; -import 'package:flutter/semantics.dart'; -import 'package:flutter/src/foundation/binding.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/src/foundation/_features.dart'; +import 'package:flutter/src/widgets/_window.dart'; import 'package:flutter/src/widgets/_window_win32.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group('Win32 window test', () { + setUp(() { + isWindowingEnabled = true; + }); + group('Platform.isWindows is false', () { setUp(() { Platform.isWindows = false; @@ -23,13 +28,378 @@ void main() { }); }); - testWidgets('WindowingOwner32 constructor does NOT throw when on windows', ( - WidgetTester tester, - ) async { + testWidgets('WindowingOwner32 constructor initializes', (WidgetTester tester) async { + bool isInitialized = false; WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface(), + win32PlatformInterface: _MockWin32PlatformInterface( + onInitialize: (WindowingInitRequest request) => isInitialized = true, + ), + platformDispatcher: tester.platformDispatcher, + ); + + expect(isInitialized, true); + }); + + testWidgets('WindowingOwner32 can create a regular window', (WidgetTester tester) async { + bool hasCreated = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onCreateWindow: (int engineId, WindowCreationRequest request) { + expect(engineId, tester.platformDispatcher.engineId); + expect(request.preferredSize.hasSize, true); + expect(request.preferredSize.width, 400); + expect(request.preferredSize.height, 300); + + expect(request.preferredConstraints.hasConstraints, true); + expect(request.preferredConstraints.minWidth, 100); + expect(request.preferredConstraints.minHeight, 101); + expect(request.preferredConstraints.maxWidth, 500); + expect(request.preferredConstraints.maxHeight, 501); + hasCreated = true; + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + owner.createRegularWindowController( + preferredSize: const Size(400, 300), + preferredConstraints: const BoxConstraints( + minWidth: 100, + minHeight: 101, + maxWidth: 500, + maxHeight: 501, + ), + delegate: RegularWindowControllerDelegate(), + ); + + expect(hasCreated, true); + }); + + testWidgets('WindowingOwner32 can destroy a regular window', (WidgetTester tester) async { + bool hasDestroyed = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onDestroyWindow: (HWND hwnd) { + hasDestroyed = true; + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.destroy(); + expect(hasDestroyed, true); + }); + + testWidgets('WindowingOwner32 can get content size', (WidgetTester tester) async { + bool hasGottenContentSize = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onGetContentSize: (HWND hwnd) { + hasGottenContentSize = true; + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.contentSize; + expect(hasGottenContentSize, true); + }); + + testWidgets('WindowingOwner32 can set title', (WidgetTester tester) async { + bool hasSetTitle = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onSetTitle: (HWND hwnd, Pointer title) { + hasSetTitle = true; + expect(title.toDartString(), 'Hello world'); + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.setTitle('Hello world'); + expect(hasSetTitle, true); + }); + + testWidgets('WindowingOwner32 can set content size', (WidgetTester tester) async { + bool hasSetSize = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onSetContentSize: (HWND hwnd, WindowSizeRequest request) { + hasSetSize = true; + expect(request.hasSize, true); + expect(request.width, 800); + expect(request.height, 600); + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.setSize(const Size(800, 600)); + expect(hasSetSize, true); + }); + + testWidgets('WindowingOwner32 can set constraints', (WidgetTester tester) async { + bool hasSetConstraints = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onSetConstraints: (HWND hwnd, WindowConstraintsRequest request) { + hasSetConstraints = true; + expect(request.hasConstraints, true); + expect(request.minWidth, 100); + expect(request.minHeight, 101); + expect(request.maxWidth, 500); + expect(request.maxHeight, 501); + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.setConstraints( + const BoxConstraints(minWidth: 100, minHeight: 101, maxWidth: 500, maxHeight: 501), + ); + expect(hasSetConstraints, true); + }); + + testWidgets('WindowingOwner32 can activate', (WidgetTester tester) async { + bool hasShown = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onShowWindow: (HWND hwnd, int sw) { + hasShown = true; + expect(sw, 9); // SW_RESTORE + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.activate(); + expect(hasShown, true); + }); + + testWidgets('WindowingOwner32 can maximize', (WidgetTester tester) async { + bool hasMaximized = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onShowWindow: (HWND hwnd, int sw) { + hasMaximized = true; + expect(sw, 3); // SW_MAXIMIZE + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.setMaximized(true); + expect(hasMaximized, true); + }); + + testWidgets('WindowingOwner32 can unmaximize', (WidgetTester tester) async { + bool hasUnmaximized = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onShowWindow: (HWND hwnd, int sw) { + hasUnmaximized = true; + expect(sw, 9); // SW_RESTORE + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.setMaximized(false); + expect(hasUnmaximized, true); + }); + + testWidgets('WindowingOwner32 can minimize', (WidgetTester tester) async { + bool hasMinimized = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onShowWindow: (HWND hwnd, int sw) { + hasMinimized = true; + expect(sw, 6); // SW_MINIMIZE + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.setMinimized(true); + expect(hasMinimized, true); + }); + + testWidgets('WindowingOwner32 can unmaximize', (WidgetTester tester) async { + bool hasUnminimized = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onShowWindow: (HWND hwnd, int sw) { + hasUnminimized = true; + expect(sw, 9); // SW_RESTORE + }, + ), platformDispatcher: tester.platformDispatcher, ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.setMinimized(false); + expect(hasUnminimized, true); + }); + + testWidgets('WindowingOwner32 can set fullscreen', (WidgetTester tester) async { + bool hasFullscreen = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onSetFullscreen: (HWND hwnd, WindowFullscreenRequest request) { + hasFullscreen = true; + expect(request.fullscreen, true); + expect(request.hasDisplayId, false); + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.setFullscreen(true); + expect(hasFullscreen, true); + }); + + testWidgets('WindowingOwner32 can get isMinimized', (WidgetTester tester) async { + bool hasCalledIsMinimized = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onIsIconic: (HWND hwnd) { + hasCalledIsMinimized = true; + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.isMinimized; + expect(hasCalledIsMinimized, true); + }); + + testWidgets('WindowingOwner32 can get isMaximized', (WidgetTester tester) async { + bool hasCalledIsMaximized = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onIsZoomed: (HWND hwnd) { + hasCalledIsMaximized = true; + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.isMaximized; + expect(hasCalledIsMaximized, true); + }); + + testWidgets('WindowingOwner32 can get isFullscreen', (WidgetTester tester) async { + bool hasCalledIsFullscreen = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onGetFullscreen: (HWND hwnd) { + hasCalledIsFullscreen = true; + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.isFullscreen; + expect(hasCalledIsFullscreen, true); + }); + + testWidgets('WindowingOwner32 can get title', (WidgetTester tester) async { + bool hasCalledTextLengthGetter = false; + bool hasCalledTextGetter = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onGetWindowTextLength: (HWND hwnd) { + hasCalledTextLengthGetter = true; + }, + onGetWindowText: (HWND hwnd, Pointer title, int length) { + hasCalledTextGetter = true; + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.title; + expect(hasCalledTextLengthGetter, true); + expect(hasCalledTextGetter, true); + }); + + testWidgets('WindowingOwner32 can get isActivated', (WidgetTester tester) async { + bool hasCalledIsActivated = false; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onGetForegroundWindow: () { + hasCalledIsActivated = true; + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + controller.isActivated; + expect(hasCalledIsActivated, true); }); }); } @@ -53,7 +423,7 @@ class _MockWin32PlatformInterface extends Win32PlatformInterface { this.onGetForegroundWindow, }); - final int viewId = 1; + final int viewId = 0; final HWND hwnd = Pointer.fromAddress(0x8000); final bool _hasToplevelWindows = true; Pointer? size; @@ -155,13 +525,13 @@ class _MockWin32PlatformInterface extends Win32PlatformInterface { @override int getWindowTextLength(HWND windowHandle) { onGetWindowTextLength?.call(windowHandle); - return 0; + return 10; } @override int getWindowText(HWND windowHandle, Pointer lpString, int maxLength) { onGetWindowText?.call(windowHandle, lpString, maxLength); - return 0; + return 10; } @override From e043b72bd7076081487d3ed40ee7ea1b516ca93a Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Mon, 11 Aug 2025 13:59:08 -0400 Subject: [PATCH 008/720] Prefer HWND typedef over Pointer --- .../lib/src/widgets/_window_win32.dart | 68 ++++++++----------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index b5640b5ee863d..5a4a8ee3df37c 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -567,72 +567,60 @@ class _NativeWin32PlatformInterface extends Win32PlatformInterface { ) external static int _createWindow(int engineId, Pointer request); - @Native Function(Int64, Int64)>( + @Native( symbol: 'InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle', ) - external static Pointer _getWindowHandle(int engineId, int viewId); + external static HWND _getWindowHandle(int engineId, int viewId); - @Native)>(symbol: 'DestroyWindow') - external static void _destroyWindow(Pointer windowHandle); + @Native(symbol: 'DestroyWindow') + external static void _destroyWindow(HWND windowHandle); - @Native)>( + @Native( symbol: 'InternalFlutterWindows_WindowManager_GetWindowContentSize', ) external static ActualContentSize _getWindowContentSize(HWND windowHandle); - @Native, Pointer)>(symbol: 'SetWindowTextW') - external static void _setWindowTitle(Pointer windowHandle, Pointer title); + @Native)>(symbol: 'SetWindowTextW') + external static void _setWindowTitle(HWND windowHandle, Pointer title); - @Native, Pointer)>( + @Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetWindowSize', ) - external static void _setWindowContentSize( - Pointer windowHandle, - Pointer size, - ); + external static void _setWindowContentSize(HWND windowHandle, Pointer size); - @Native, Pointer)>( + @Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetWindowConstraints', ) external static void _setWindowConstraints( - Pointer windowHandle, + HWND windowHandle, Pointer constraints, ); - @Native, Int32)>(symbol: 'ShowWindow') - external static void _showWindow(Pointer windowHandle, int command); + @Native(symbol: 'ShowWindow') + external static void _showWindow(HWND windowHandle, int command); - @Native)>(symbol: 'IsIconic') - external static int _isIconic(Pointer windowHandle); + @Native(symbol: 'IsIconic') + external static int _isIconic(HWND windowHandle); - @Native)>(symbol: 'IsZoomed') - external static int _isZoomed(Pointer windowHandle); + @Native(symbol: 'IsZoomed') + external static int _isZoomed(HWND windowHandle); - @Native, Pointer)>( + @Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetFullscreen', ) - external static void _setFullscreen( - Pointer windowHandle, - Pointer request, - ); + external static void _setFullscreen(HWND windowHandle, Pointer request); - @Native)>( - symbol: 'InternalFlutterWindows_WindowManager_GetFullscreen', - ) - external static bool _getFullscreen(Pointer windowHandle); + @Native(symbol: 'InternalFlutterWindows_WindowManager_GetFullscreen') + external static bool _getFullscreen(HWND windowHandle); - @Native)>(symbol: 'GetWindowTextLengthW') - external static int _getWindowTextLength(Pointer windowHandle); + @Native(symbol: 'GetWindowTextLengthW') + external static int _getWindowTextLength(HWND windowHandle); - @Native, Pointer, Int32)>(symbol: 'GetWindowTextW') - external static int _getWindowText( - Pointer windowHandle, - Pointer lpString, - int maxLength, - ); + @Native, Int32)>(symbol: 'GetWindowTextW') + external static int _getWindowText(HWND windowHandle, Pointer lpString, int maxLength); - @Native Function()>(symbol: 'GetForegroundWindow') - external static Pointer _getForegroundWindow(); + @Native(symbol: 'GetForegroundWindow') + external static HWND _getForegroundWindow(); } /// Payload for the creation method used by [Win32PlatformInterface.createWindow]. @@ -719,7 +707,7 @@ final class WindowsMessage extends Struct { @Int64() external int viewId; - external Pointer windowHandle; + external HWND windowHandle; @Int32() external int message; From 9536a81d158c175c9729f6cf2521aa9dd576ec98 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Mon, 11 Aug 2025 14:18:53 -0400 Subject: [PATCH 009/720] Adding comments and simplifying constructor for common use cases --- packages/flutter/lib/src/widgets/_window.dart | 17 +-- .../lib/src/widgets/_window_win32.dart | 142 +++++++++++++++++- 2 files changed, 148 insertions(+), 11 deletions(-) diff --git a/packages/flutter/lib/src/widgets/_window.dart b/packages/flutter/lib/src/widgets/_window.dart index ff78d3a21337f..f38e022d2f260 100644 --- a/packages/flutter/lib/src/widgets/_window.dart +++ b/packages/flutter/lib/src/widgets/_window.dart @@ -18,14 +18,9 @@ import 'dart:io'; import 'dart:ui' show Display, FlutterView; import 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import '../foundation/_features.dart'; -import 'binding.dart'; -import 'framework.dart'; -import 'inherited_model.dart'; -import 'view.dart'; import '_window_win32.dart'; const String _kWindowingDisabledErrorMessage = ''' @@ -243,12 +238,17 @@ abstract class RegularWindowController extends BaseWindowController { ); } - /// Creates an empty [RegularWindowController] for testing purposes. + /// Creates an empty [RegularWindowController]. + /// + /// This method is only intended to be used by subclasses of the + /// [RegularWindowController]. + /// + /// Users who want to instantiate a new [RegularWindowController] should + /// always use the factory method to create a controller that is valid + /// for their particular platform. /// /// {@macro flutter.widgets.windowing.experimental} @internal - @protected - @visibleForTesting RegularWindowController.empty(); /// The current title of the window. @@ -514,7 +514,6 @@ class RegularWindow extends StatelessWidget { child: View(view: controller.rootView, child: child), ), ); - ; } } diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index 5a4a8ee3df37c..393881634d33d 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -2,6 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// Do not import this file in production applications or packages published +// to pub.dev. Flutter will make breaking changes to this file, even in patch +// versions. +// +// All APIs in this file must be private or must: +// +// 1. Have the `@internal` attribute. +// 2. Throw an `UnsupportedError` if `isWindowingEnabled` +// is `false. +// +// See: https://github.com/flutter/flutter/issues/30701. + import 'dart:ffi' hide Size; import 'dart:io'; import 'dart:ui' show Display, FlutterView; @@ -12,7 +24,10 @@ import 'package:flutter/rendering.dart'; import '_window.dart'; import 'binding.dart'; -/// A Windows window handle. +/// A Win32 window handle. +/// +/// {@macro flutter.widgets.windowing.experimental} +@internal typedef HWND = Pointer; const int _WM_SIZE = 0x0005; @@ -28,7 +43,7 @@ const int _SW_MINIMIZE = 6; /// Implementations of this class should register with /// [WindowingOwnerWin32.addMessageHandler] to begin receiving messages. /// When finished handling messages, implementations should deregister -/// themselves with [WindowingOwnerWIn32.removeMessageHandler]. +/// themselves with [WindowingOwnerWin32.removeMessageHandler]. /// /// {@macro flutter.widgets.windowing.experimental} /// @@ -123,6 +138,12 @@ class WindowingOwnerWin32 extends WindowingOwner { @internal final Win32PlatformInterface win32PlatformInterface; + /// The [PlatformDispatcher]. + /// + /// This will differ from [PlatformDispatcher.instance] during testing. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal final PlatformDispatcher platformDispatcher; @internal @@ -268,6 +289,7 @@ class RegularWindowControllerWin32 extends RegularWindowController bool _destroyed = false; @override + @internal Size get contentSize { _ensureNotDestroyed(); final ActualContentSize size = _owner.win32PlatformInterface.getWindowContentSize( @@ -278,6 +300,7 @@ class RegularWindowControllerWin32 extends RegularWindowController } @override + @internal String get title { _ensureNotDestroyed(); final int length = _owner.win32PlatformInterface.getWindowTextLength(getWindowHandle()); @@ -296,30 +319,35 @@ class RegularWindowControllerWin32 extends RegularWindowController } @override + @internal bool get isActivated { _ensureNotDestroyed(); return _owner.win32PlatformInterface.getForegroundWindow() == getWindowHandle(); } @override + @internal bool get isMaximized { _ensureNotDestroyed(); return _owner.win32PlatformInterface.isZoomed(getWindowHandle()) != 0; } @override + @internal bool get isMinimized { _ensureNotDestroyed(); return _owner.win32PlatformInterface.isIconic(getWindowHandle()) != 0; } @override + @internal bool get isFullscreen { _ensureNotDestroyed(); return _owner.win32PlatformInterface.getFullscreen(getWindowHandle()); } @override + @internal void setSize(Size? size) { _ensureNotDestroyed(); final Pointer request = ffi.calloc(); @@ -331,6 +359,7 @@ class RegularWindowControllerWin32 extends RegularWindowController } @override + @internal void setConstraints(BoxConstraints constraints) { _ensureNotDestroyed(); final Pointer request = ffi.calloc(); @@ -342,6 +371,7 @@ class RegularWindowControllerWin32 extends RegularWindowController } @override + @internal void setTitle(String title) { _ensureNotDestroyed(); final Pointer titlePointer = title.toNativeUtf16(); @@ -352,12 +382,14 @@ class RegularWindowControllerWin32 extends RegularWindowController } @override + @internal void activate() { _ensureNotDestroyed(); _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE); } @override + @internal void setMaximized(bool maximized) { _ensureNotDestroyed(); if (maximized) { @@ -368,6 +400,7 @@ class RegularWindowControllerWin32 extends RegularWindowController } @override + @internal void setMinimized(bool minimized) { _ensureNotDestroyed(); if (minimized) { @@ -378,6 +411,7 @@ class RegularWindowControllerWin32 extends RegularWindowController } @override + @internal void setFullscreen(bool fullscreen, {Display? display}) { final Pointer request = ffi.calloc(); request.ref.hasDisplayId = false; @@ -388,6 +422,7 @@ class RegularWindowControllerWin32 extends RegularWindowController } /// Returns HWND pointer to the top level window. + @internal HWND getWindowHandle() { _ensureNotDestroyed(); return _owner.win32PlatformInterface.getWindowHandle( @@ -414,6 +449,7 @@ class RegularWindowControllerWin32 extends RegularWindowController } @override + @internal int? handleWindowsMessage( FlutterView view, HWND windowHandle, @@ -449,22 +485,124 @@ class RegularWindowControllerWin32 extends RegularWindowController @visibleForTesting @internal abstract class Win32PlatformInterface { + /// Checks if the engine specified by [engineId] has any top level + /// windows created on it. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal bool hasTopLevelWindows(int engineId); + + /// Initialize the window subsystem for the provided engine. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal void initialize(int engineId, Pointer request); + + /// Create a regular window on the provided engine. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal int createWindow(int engineId, Pointer request); + + /// Retrieve the window handle associated with the provided engine and view ids. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal HWND getWindowHandle(int engineId, int viewId); + + /// Destroy a window given its window handle. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal void destroyWindow(HWND windowHandle); + + /// Retrieve the current content size of a window given its handle. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal ActualContentSize getWindowContentSize(HWND windowHandle); + + /// Set the title of a window. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal void setWindowTitle(HWND windowHandle, Pointer title); + + /// Set the content size of the window. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal void setWindowContentSize(HWND windowHandle, Pointer size); + + /// Set the constraints of the window. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal void setWindowConstraints(HWND windowHandle, Pointer constraints); + + /// Show the window. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal void showWindow(HWND windowHandle, int command); + + /// Check if the window is minimized. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal int isIconic(HWND windowHandle); + + /// Check if the window is maximized. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal int isZoomed(HWND windowHandle); + + /// Request that the window change its fullscreen status. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal void setFullscreen(HWND windowHandle, Pointer request); + + /// Retrieve the fullscreen status of the window. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal bool getFullscreen(HWND windowHandle); + + /// Retrieve the text length of the title of the window. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal int getWindowTextLength(HWND windowHandle); + + /// Retrieve the title of the window. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal int getWindowText(HWND windowHandle, Pointer lpString, int maxLength); + + /// Retrieve the currently focused window handle. + /// + /// {@macro flutter.widgets.windowing.experimental} + @visibleForTesting + @internal HWND getForegroundWindow(); } From 6d7f227cec85864476eb7efc4d1734ae29b97388 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Mon, 11 Aug 2025 14:21:33 -0400 Subject: [PATCH 010/720] Further pull request feedback --- packages/flutter/lib/src/widgets/_window_win32.dart | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index 393881634d33d..89c059280571f 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -70,7 +70,7 @@ abstract class WindowsMessageHandler { /// [WindowingOwner] implementation for Windows. /// -/// If [Platform.isWindows] is false, then the constructor will throw an +/// If [Platform.isWindows] is false, then the constructor will throw an /// [UnsupportedError]. /// /// {@macro flutter.widgets.windowing.experimental} @@ -235,9 +235,6 @@ class WindowingOwnerWin32 extends WindowingOwner { /// Implementation of [RegularWindowController] for the Windows platform. /// -/// If [Platform.isWindows] is false, then the constructor will throw an -/// [UnsupportedError]. -/// /// {@macro flutter.widgets.windowing.experimental} /// /// See also: @@ -247,9 +244,6 @@ class RegularWindowControllerWin32 extends RegularWindowController implements WindowsMessageHandler { /// Creates a new regular window controller for Win32. /// - /// If [Platform.isWindows] is false, then this constructor will throw an - /// [UnsupportedError]. - /// /// When this constructor completes the native window has been created and /// has a view associated with it. /// From 6e78c79f798bcb07ec636315ada3fddda448d8d7 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Mon, 11 Aug 2025 14:56:25 -0400 Subject: [PATCH 011/720] Adding further tests --- .../test/widgets/window_win32_test.dart | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/packages/flutter/test/widgets/window_win32_test.dart b/packages/flutter/test/widgets/window_win32_test.dart index bfaf38f98160d..ef7d0c7fc7efd 100644 --- a/packages/flutter/test/widgets/window_win32_test.dart +++ b/packages/flutter/test/widgets/window_win32_test.dart @@ -75,6 +75,90 @@ void main() { expect(hasCreated, true); }); + testWidgets('Sending WM_SIZE to WindowingOwner32 notifies listeners', ( + WidgetTester tester, + ) async { + const int WM_SIZE = 0x0005; + late void Function(Pointer) messageFunc; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onInitialize: (WindowingInitRequest request) { + messageFunc = request.onMessage.asFunction(); + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + bool listenerTriggered = false; + controller.addListener(() => listenerTriggered = true); + final Pointer message = ffi.calloc(); + message.ref.viewId = 0; + message.ref.message = WM_SIZE; + messageFunc(message); + + expect(listenerTriggered, true); + }); + + testWidgets('Sending WM_ACTIVATE to WindowingOwner32 notifies listeners', ( + WidgetTester tester, + ) async { + const int WM_ACTIVATE = 0x0006; + late void Function(Pointer) messageFunc; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onInitialize: (WindowingInitRequest request) { + messageFunc = request.onMessage.asFunction(); + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + final RegularWindowController controller = owner.createRegularWindowController( + delegate: RegularWindowControllerDelegate(), + ); + + bool listenerTriggered = false; + controller.addListener(() => listenerTriggered = true); + final Pointer message = ffi.calloc(); + message.ref.viewId = 0; + message.ref.message = WM_ACTIVATE; + messageFunc(message); + + expect(listenerTriggered, true); + }); + + testWidgets('Sending WM_CLOSE message to WindowingOwner32 results in window being destroyed', ( + WidgetTester tester, + ) async { + const int WM_CLOSE = 0x0010; + bool hasDestroyed = false; + late void Function(Pointer) messageFunc; + final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( + win32PlatformInterface: _MockWin32PlatformInterface( + onInitialize: (WindowingInitRequest request) { + messageFunc = request.onMessage.asFunction(); + }, + onDestroyWindow: (HWND hwnd) { + hasDestroyed = true; + }, + ), + platformDispatcher: tester.platformDispatcher, + ); + + owner.createRegularWindowController(delegate: RegularWindowControllerDelegate()); + + final Pointer message = ffi.calloc(); + message.ref.viewId = 0; + message.ref.message = WM_CLOSE; + messageFunc(message); + + expect(hasDestroyed, true); + }); + testWidgets('WindowingOwner32 can destroy a regular window', (WidgetTester tester) async { bool hasDestroyed = false; final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( From 9cbd55ceb8ab9fd577c643bbe82f06f6e7076462 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 13 Aug 2025 10:31:46 -0400 Subject: [PATCH 012/720] Roll Dart SDK from a098cb676fd6 to 73153bdc1459 (1 revision) (#173708) https://dart.googlesource.com/sdk.git/+log/a098cb676fd6..73153bdc1459 2025-08-13 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-93.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 chinmaygarde@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 d81233c644a8b..df70a545faa3e 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': 'a098cb676fd60bdd71bdb76430ba2c0436dd2a0e', + 'dart_revision': '73153bdc1459d34da816c13ece0c6812b97caf92', # 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 738ec1a4b9409d60ac196c0c0a62681a49b5878e Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Wed, 13 Aug 2025 11:00:48 -0400 Subject: [PATCH 013/720] Regular windows win32 engine (#173424) ## What's new? This pull request implements the remaining bits necessary for landing regular windows on win32 for the engine only. - Updated `InternalFlutterWindows_WindowManager_CreateRegularWindow` - Separated constraints and size to two different fields - Added title - Updated `InternalFlutterWindows_WindowManager_SetWindowSize` - Added `InternalFlutterWindows_WindowManager_SetWindowConstraints` - Added `InternalFlutterWindows_WindowManager_SetFullscreen` - Added `InternalFlutterWindows_WindowManager_GetFullscreen` - Added integration tests, but let me know if you think more tests would help! I felt that the integration tests showed off the functionality very well. https://github.com/canonical/flutter/pull/64 will follow with the framework side of things :rocket: ## 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 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]. **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. --- .../flutter/shell/platform/windows/BUILD.gn | 1 + .../shell/platform/windows/host_window.cc | 329 ++++++++++++++++-- .../shell/platform/windows/host_window.h | 50 ++- .../shell/platform/windows/rect_helper.h | 25 ++ .../shell/platform/windows/window_manager.cc | 55 ++- .../shell/platform/windows/window_manager.h | 72 ++-- .../windows/window_manager_unittests.cc | 173 ++++++++- 7 files changed, 626 insertions(+), 79 deletions(-) create mode 100644 engine/src/flutter/shell/platform/windows/rect_helper.h diff --git a/engine/src/flutter/shell/platform/windows/BUILD.gn b/engine/src/flutter/shell/platform/windows/BUILD.gn index 036106dfe1626..97c25996f1604 100644 --- a/engine/src/flutter/shell/platform/windows/BUILD.gn +++ b/engine/src/flutter/shell/platform/windows/BUILD.gn @@ -110,6 +110,7 @@ source_set("flutter_windows_source") { "platform_view_manager.h", "platform_view_plugin.cc", "platform_view_plugin.h", + "rect_helper.h", "sequential_id_generator.cc", "sequential_id_generator.h", "settings_plugin.cc", diff --git a/engine/src/flutter/shell/platform/windows/host_window.cc b/engine/src/flutter/shell/platform/windows/host_window.cc index 2723bb99857f9..b901700e9a7c9 100644 --- a/engine/src/flutter/shell/platform/windows/host_window.cc +++ b/engine/src/flutter/shell/platform/windows/host_window.cc @@ -6,9 +6,11 @@ #include +#include "flutter/shell/platform/windows/display_monitor.h" #include "flutter/shell/platform/windows/dpi_utils.h" #include "flutter/shell/platform/windows/flutter_window.h" #include "flutter/shell/platform/windows/flutter_windows_view_controller.h" +#include "flutter/shell/platform/windows/rect_helper.h" #include "flutter/shell/platform/windows/window_manager.h" namespace { @@ -193,6 +195,60 @@ void SetChildContent(HWND content, HWND window) { client_rect.bottom - client_rect.top, true); } +// Adjusts a 1D segment (defined by origin and size) to fit entirely within +// a destination segment. If the segment is larger than the destination, it is +// first shrunk to fit. Then, it's shifted to be within the bounds. +// +// Let the destination be "{...}" and the segment to adjust be "[...]". +// +// Case 1: The segment sticks out to the right. +// +// Before: {------[----}------] +// After: {------[----]} +// +// Case 2: The segment sticks out to the left. +// +// Before: [------{----]------} +// After: {[----]------} +void AdjustAlongAxis(LONG dst_origin, LONG dst_size, LONG* origin, LONG* size) { + *size = std::min(dst_size, *size); + if (*origin < dst_origin) + *origin = dst_origin; + else + *origin = std::min(dst_origin + dst_size, *origin + *size) - *size; +} + +RECT AdjustToFit(const RECT& parent, const RECT& child) { + auto new_x = child.left; + auto new_y = child.top; + auto new_width = flutter::RectWidth(child); + auto new_height = flutter::RectHeight(child); + AdjustAlongAxis(parent.left, flutter::RectWidth(parent), &new_x, &new_width); + AdjustAlongAxis(parent.top, flutter::RectHeight(parent), &new_y, &new_height); + RECT result; + result.left = new_x; + result.right = new_x + new_width; + result.top = new_y; + result.bottom = new_y + new_height; + return result; +} + +flutter::BoxConstraints FromWindowConstraints( + const flutter::WindowConstraints& preferred_constraints) { + std::optional smallest, biggest; + if (preferred_constraints.has_view_constraints) { + smallest = flutter::Size(preferred_constraints.view_min_width, + preferred_constraints.view_min_height); + if (preferred_constraints.view_max_width > 0 && + preferred_constraints.view_max_height > 0) { + biggest = flutter::Size(preferred_constraints.view_max_width, + preferred_constraints.view_max_height); + } + } + + return flutter::BoxConstraints(smallest, biggest); +} + } // namespace namespace flutter { @@ -200,21 +256,15 @@ namespace flutter { std::unique_ptr HostWindow::CreateRegularWindow( WindowManager* window_manager, FlutterWindowsEngine* engine, - const WindowSizing& content_size) { + const WindowSizeRequest& preferred_size, + const WindowConstraints& preferred_constraints, + LPCWSTR title) { DWORD window_style = WS_OVERLAPPEDWINDOW; DWORD extended_window_style = 0; - std::optional smallest = std::nullopt; - std::optional biggest = std::nullopt; - - if (content_size.has_view_constraints) { - smallest = Size(content_size.view_min_width, content_size.view_min_height); - if (content_size.view_max_width > 0 && content_size.view_max_height > 0) { - biggest = Size(content_size.view_max_width, content_size.view_max_height); - } - } + auto const box_constraints = FromWindowConstraints(preferred_constraints); // TODO(knopp): What about windows sized to content? - FML_CHECK(content_size.has_preferred_view_size); + FML_CHECK(preferred_size.has_preferred_view_size); // Calculate the screen space window rectangle for the new window. // Default positioning values (CW_USEDEFAULT) are used @@ -222,9 +272,10 @@ std::unique_ptr HostWindow::CreateRegularWindow( Rect const initial_window_rect = [&]() -> Rect { std::optional const window_size = GetWindowSizeForClientSize( *engine->windows_proc_table(), - Size(content_size.preferred_view_width, - content_size.preferred_view_height), - smallest, biggest, window_style, extended_window_style, nullptr); + Size(preferred_size.preferred_view_width, + preferred_size.preferred_view_height), + box_constraints.smallest(), box_constraints.biggest(), window_style, + extended_window_style, nullptr); return {{CW_USEDEFAULT, CW_USEDEFAULT}, window_size ? *window_size : Size{CW_USEDEFAULT, CW_USEDEFAULT}}; }(); @@ -274,7 +325,7 @@ std::unique_ptr HostWindow::CreateRegularWindow( // Create the native window. HWND hwnd = CreateWindowEx( - extended_window_style, kWindowClassName, L"", window_style, + extended_window_style, kWindowClassName, title, window_style, initial_window_rect.left(), initial_window_rect.top(), initial_window_rect.width(), initial_window_rect.height(), nullptr, nullptr, GetModuleHandle(nullptr), engine->windows_proc_table().get()); @@ -308,9 +359,9 @@ std::unique_ptr HostWindow::CreateRegularWindow( // multiple next frame callbacks. If multiple windows are created, only the // last one will be shown. ShowWindow(hwnd, SW_SHOWNORMAL); - return std::unique_ptr(new HostWindow( - window_manager, engine, WindowArchetype::kRegular, - std::move(view_controller), BoxConstraints(smallest, biggest), hwnd)); + return std::unique_ptr( + new HostWindow(window_manager, engine, WindowArchetype::kRegular, + std::move(view_controller), box_constraints, hwnd)); } HostWindow::HostWindow( @@ -463,28 +514,77 @@ LRESULT HostWindow::HandleMessage(HWND hwnd, return DefWindowProc(hwnd, message, wparam, lparam); } -void HostWindow::SetContentSize(const WindowSizing& size) { - WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)}; - GetWindowInfo(window_handle_, &window_info); +void HostWindow::SetContentSize(const WindowSizeRequest& size) { + if (!size.has_preferred_view_size) { + return; + } - std::optional smallest, biggest; - if (size.has_view_constraints) { - smallest = Size(size.view_min_width, size.view_min_height); - if (size.view_max_width > 0 && size.view_max_height > 0) { - biggest = Size(size.view_max_width, size.view_max_height); + if (GetFullscreen()) { + std::optional const window_size = GetWindowSizeForClientSize( + *engine_->windows_proc_table(), + Size(size.preferred_view_width, size.preferred_view_height), + box_constraints_.smallest(), box_constraints_.biggest(), + saved_window_info_.style, saved_window_info_.ex_style, nullptr); + if (!window_size) { + return; } - } - box_constraints_ = BoxConstraints(smallest, biggest); + saved_window_info_.client_size = + ActualWindowSize{.width = size.preferred_view_width, + .height = size.preferred_view_height}; + saved_window_info_.rect.right = + saved_window_info_.rect.left + static_cast(window_size->width()); + saved_window_info_.rect.bottom = + saved_window_info_.rect.top + static_cast(window_size->height()); + } else { + WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)}; + GetWindowInfo(window_handle_, &window_info); - if (size.has_preferred_view_size) { std::optional const window_size = GetWindowSizeForClientSize( *engine_->windows_proc_table(), Size(size.preferred_view_width, size.preferred_view_height), box_constraints_.smallest(), box_constraints_.biggest(), window_info.dwStyle, window_info.dwExStyle, nullptr); - if (window_size) { + if (!window_size) { + return; + } + + SetWindowPos(window_handle_, NULL, 0, 0, window_size->width(), + window_size->height(), + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } +} + +void HostWindow::SetConstraints(const WindowConstraints& constraints) { + box_constraints_ = FromWindowConstraints(constraints); + + if (GetFullscreen()) { + std::optional const window_size = GetWindowSizeForClientSize( + *engine_->windows_proc_table(), + Size(saved_window_info_.client_size.width, + saved_window_info_.client_size.height), + box_constraints_.smallest(), box_constraints_.biggest(), + saved_window_info_.style, saved_window_info_.ex_style, nullptr); + if (!window_size) { + return; + } + + saved_window_info_.rect.right = + saved_window_info_.rect.left + static_cast(window_size->width()); + saved_window_info_.rect.bottom = + saved_window_info_.rect.top + static_cast(window_size->height()); + } else { + auto const client_size = GetWindowContentSize(window_handle_); + auto const current_size = Size(client_size.width, client_size.height); + WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)}; + GetWindowInfo(window_handle_, &window_info); + std::optional const window_size = GetWindowSizeForClientSize( + *engine_->windows_proc_table(), current_size, + box_constraints_.smallest(), box_constraints_.biggest(), + window_info.dwStyle, window_info.dwExStyle, nullptr); + + if (window_size && current_size != window_size) { SetWindowPos(window_handle_, NULL, 0, 0, window_size->width(), window_size->height(), SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); @@ -492,4 +592,173 @@ void HostWindow::SetContentSize(const WindowSizing& size) { } } +// The fullscreen method is largely adapted from the method found in chromium: +// See: +// +// * https://chromium.googlesource.com/chromium/src/+/refs/heads/main/ui/views/win/fullscreen_handler.h +// * https://chromium.googlesource.com/chromium/src/+/refs/heads/main/ui/views/win/fullscreen_handler.cc +void HostWindow::SetFullscreen( + bool fullscreen, + std::optional display_id) { + if (fullscreen == GetFullscreen()) { + return; + } + + if (fullscreen) { + WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)}; + GetWindowInfo(window_handle_, &window_info); + saved_window_info_.style = window_info.dwStyle; + saved_window_info_.ex_style = window_info.dwExStyle; + // Store the original window rect, DPI, and monitor info to detect changes + // and more accurately restore window placements when exiting fullscreen. + ::GetWindowRect(window_handle_, &saved_window_info_.rect); + saved_window_info_.client_size = GetWindowContentSize(window_handle_); + saved_window_info_.dpi = GetDpiForHWND(window_handle_); + saved_window_info_.monitor = + MonitorFromWindow(window_handle_, MONITOR_DEFAULTTONEAREST); + saved_window_info_.monitor_info.cbSize = + sizeof(saved_window_info_.monitor_info); + GetMonitorInfo(saved_window_info_.monitor, + &saved_window_info_.monitor_info); + } + + if (fullscreen) { + // Next, get the raw HMONITOR that we want to be fullscreened on + HMONITOR monitor = + MonitorFromWindow(window_handle_, MONITOR_DEFAULTTONEAREST); + if (display_id) { + for (auto const& display : engine_->display_monitor()->GetDisplays()) { + if (display.display_id == display_id) { + monitor = reinterpret_cast(display.display_id); + break; + } + } + } + + MONITORINFO monitor_info; + monitor_info.cbSize = sizeof(monitor_info); + if (!GetMonitorInfo(monitor, &monitor_info)) { + FML_LOG(ERROR) << "Cannot set window fullscreen because the monitor info " + "was not found"; + } + + auto const width = RectWidth(monitor_info.rcMonitor); + auto const height = RectHeight(monitor_info.rcMonitor); + WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)}; + GetWindowInfo(window_handle_, &window_info); + + // Set new window style and size. + SetWindowLong(window_handle_, GWL_STYLE, + saved_window_info_.style & ~(WS_CAPTION | WS_THICKFRAME)); + SetWindowLong( + window_handle_, GWL_EXSTYLE, + saved_window_info_.ex_style & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | + WS_EX_CLIENTEDGE | WS_EX_STATICEDGE)); + + // We call SetWindowPos first to set the window flags immediately. This + // makes it so that the WM_GETMINMAXINFO gets called with the correct window + // and content sizes. + SetWindowPos(window_handle_, NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + + SetWindowPos(window_handle_, nullptr, monitor_info.rcMonitor.left, + monitor_info.rcMonitor.top, width, height, + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } else { + // Restore the window style and bounds saved prior to entering fullscreen. + // Use WS_VISIBLE for windows shown after SetFullscreen: crbug.com/1062251. + // Making multiple window adjustments here is ugly, but if SetWindowPos() + // doesn't redraw, the taskbar won't be repainted. + SetWindowLong(window_handle_, GWL_STYLE, + saved_window_info_.style | WS_VISIBLE); + SetWindowLong(window_handle_, GWL_EXSTYLE, saved_window_info_.ex_style); + + // We call SetWindowPos first to set the window flags immediately. This + // makes it so that the WM_GETMINMAXINFO gets called with the correct window + // and content sizes. + SetWindowPos(window_handle_, NULL, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); + + HMONITOR monitor = + MonitorFromRect(&saved_window_info_.rect, MONITOR_DEFAULTTONEAREST); + MONITORINFO monitor_info; + monitor_info.cbSize = sizeof(monitor_info); + GetMonitorInfo(monitor, &monitor_info); + + auto window_rect = saved_window_info_.rect; + + // Adjust the window bounds to restore, if displays were disconnected, + // virtually rearranged, or otherwise changed metrics during fullscreen. + if (monitor != saved_window_info_.monitor || + !AreRectsEqual(saved_window_info_.monitor_info.rcWork, + monitor_info.rcWork)) { + window_rect = AdjustToFit(monitor_info.rcWork, window_rect); + } + + auto const fullscreen_dpi = GetDpiForHWND(window_handle_); + SetWindowPos(window_handle_, nullptr, window_rect.left, window_rect.top, + RectWidth(window_rect), RectHeight(window_rect), + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + auto const final_dpi = GetDpiForHWND(window_handle_); + if (final_dpi != saved_window_info_.dpi || final_dpi != fullscreen_dpi) { + // Reissue SetWindowPos if the DPI changed from saved or fullscreen DPIs. + // The first call may misinterpret bounds spanning displays, if the + // fullscreen display's DPI does not match the target display's DPI. + // + // Scale and clamp the bounds if the final DPI changed from the saved DPI. + // This more accurately matches the original placement, while avoiding + // unexpected offscreen placement in a recongifured multi-screen space. + if (final_dpi != saved_window_info_.dpi) { + auto const scale = + final_dpi / static_cast(saved_window_info_.dpi); + auto const width = static_cast(scale * RectWidth(window_rect)); + auto const height = static_cast(scale * RectHeight(window_rect)); + window_rect.right = window_rect.left + width; + window_rect.bottom = window_rect.top + height; + window_rect = AdjustToFit(monitor_info.rcWork, window_rect); + } + + SetWindowPos(window_handle_, nullptr, window_rect.left, window_rect.top, + RectWidth(window_rect), RectHeight(window_rect), + SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); + } + } + + if (!task_bar_list_) { + HRESULT hr = + ::CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, + IID_PPV_ARGS(&task_bar_list_)); + if (SUCCEEDED(hr) && FAILED(task_bar_list_->HrInit())) { + task_bar_list_ = nullptr; + } + } + + // As per MSDN marking the window as fullscreen should ensure that the + // taskbar is moved to the bottom of the Z-order when the fullscreen window + // is activated. If the window is not fullscreen, the Shell falls back to + // heuristics to determine how the window should be treated, which means + // that it could still consider the window as fullscreen. :( + if (task_bar_list_) { + task_bar_list_->MarkFullscreenWindow(window_handle_, !!fullscreen); + } + + is_fullscreen_ = fullscreen; +} + +bool HostWindow::GetFullscreen() const { + return is_fullscreen_; +} + +ActualWindowSize HostWindow::GetWindowContentSize(HWND hwnd) { + RECT rect; + GetClientRect(hwnd, &rect); + double const dpr = FlutterDesktopGetDpiForHWND(hwnd) / + static_cast(USER_DEFAULT_SCREEN_DPI); + double const width = rect.right / dpr; + double const height = rect.bottom / dpr; + return { + .width = rect.right / dpr, + .height = rect.bottom / dpr, + }; +} } // namespace flutter diff --git a/engine/src/flutter/shell/platform/windows/host_window.h b/engine/src/flutter/shell/platform/windows/host_window.h index 8a6875e83f45a..e3773d65ec5de 100644 --- a/engine/src/flutter/shell/platform/windows/host_window.h +++ b/engine/src/flutter/shell/platform/windows/host_window.h @@ -5,7 +5,9 @@ #ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_H_ #define FLUTTER_SHELL_PLATFORM_WINDOWS_HOST_WINDOW_H_ +#include #include +#include #include #include @@ -34,7 +36,9 @@ class HostWindow { static std::unique_ptr CreateRegularWindow( WindowManager* controller, FlutterWindowsEngine* engine, - const WindowSizing& content_size); + const WindowSizeRequest& preferred_size, + const WindowConstraints& preferred_constraints, + LPCWSTR title); // Returns the instance pointer for |hwnd| or nullptr if invalid. static HostWindow* GetThisFromHandle(HWND hwnd); @@ -44,12 +48,43 @@ class HostWindow { HWND GetWindowHandle() const; // Resizes the window to accommodate a client area of the given - // |size|. - void SetContentSize(const WindowSizing& size); + // |size|. If the size does not satisfy the constraints, the window will be + // resized to the minimum or maximum size as appropriate. + void SetContentSize(const WindowSizeRequest& size); + + // Sets the constaints on the client area of the window. + // If the current window size does not satisfy the new constraints, + // the window will be resized to satisy thew new constraints. + void SetConstraints(const WindowConstraints& constraints); + + // Set the fullscreen state. |display_id| indicates the display where + // the window should be shown fullscreen; std::nullopt indicates + // that no display was specified, so the current display may be used. + void SetFullscreen(bool fullscreen, + std::optional display_id); + + // Returns |true| if this window is fullscreen, otherwise |false|. + bool GetFullscreen() const; + + // Given a window identifier, returns the window content size of the + // window. + static ActualWindowSize GetWindowContentSize(HWND hwnd); private: friend WindowManager; + // Information saved before going into fullscreen mode, used to restore the + // window afterwards. + struct SavedWindowInfo { + LONG style; + LONG ex_style; + RECT rect; + ActualWindowSize client_size; + int dpi; + HMONITOR monitor; + MONITORINFO monitor_info; + }; + HostWindow(WindowManager* controller, FlutterWindowsEngine* engine, WindowArchetype archetype, @@ -91,6 +126,15 @@ class HostWindow { // The constraints on the window's client area. BoxConstraints box_constraints_; + // Whether or not the window is currently in a fullscreen state. + bool is_fullscreen_ = false; + + // Saved window information from before entering fullscreen mode. + SavedWindowInfo saved_window_info_; + + // Used to mark a window as fullscreen. + Microsoft::WRL::ComPtr task_bar_list_; + FML_DISALLOW_COPY_AND_ASSIGN(HostWindow); }; diff --git a/engine/src/flutter/shell/platform/windows/rect_helper.h b/engine/src/flutter/shell/platform/windows/rect_helper.h new file mode 100644 index 0000000000000..feb1ce74cc8dc --- /dev/null +++ b/engine/src/flutter/shell/platform/windows/rect_helper.h @@ -0,0 +1,25 @@ +// 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 "Windows.h" + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_RECT_HELPER_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_RECT_HELPER_H_ + +namespace flutter { +LONG RectWidth(const RECT& r) { + return r.right - r.left; +} + +LONG RectHeight(const RECT& r) { + return r.bottom - r.top; +} + +bool AreRectsEqual(const RECT& a, const RECT& b) { + return a.left == b.left && a.top == b.top && a.right == b.right && + a.bottom == b.bottom; +} +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_RECT_HELPER_H_ diff --git a/engine/src/flutter/shell/platform/windows/window_manager.cc b/engine/src/flutter/shell/platform/windows/window_manager.cc index 11ff976bc3ba6..012e8894d17ab 100644 --- a/engine/src/flutter/shell/platform/windows/window_manager.cc +++ b/engine/src/flutter/shell/platform/windows/window_manager.cc @@ -33,8 +33,9 @@ bool WindowManager::HasTopLevelWindows() const { FlutterViewId WindowManager::CreateRegularWindow( const WindowCreationRequest* request) { - auto window = - HostWindow::CreateRegularWindow(this, engine_, request->content_size); + auto window = HostWindow::CreateRegularWindow( + this, engine_, request->preferred_size, request->preferred_constraints, + request->title); if (!window || !window->GetWindowHandle()) { FML_LOG(ERROR) << "Failed to create host window"; return -1; @@ -134,25 +135,47 @@ HWND InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle( } } -FlutterWindowSize InternalFlutterWindows_WindowManager_GetWindowContentSize( - HWND hwnd) { - RECT rect; - GetClientRect(hwnd, &rect); - double const dpr = FlutterDesktopGetDpiForHWND(hwnd) / - static_cast(USER_DEFAULT_SCREEN_DPI); - double const width = rect.right / dpr; - double const height = rect.bottom / dpr; - return { - .width = rect.right / dpr, - .height = rect.bottom / dpr, - }; +flutter::ActualWindowSize +InternalFlutterWindows_WindowManager_GetWindowContentSize(HWND hwnd) { + return flutter::HostWindow::GetWindowContentSize(hwnd); } -void InternalFlutterWindows_WindowManager_SetWindowContentSize( +void InternalFlutterWindows_WindowManager_SetWindowSize( HWND hwnd, - const flutter::WindowSizing* size) { + const flutter::WindowSizeRequest* size) { flutter::HostWindow* window = flutter::HostWindow::GetThisFromHandle(hwnd); if (window) { window->SetContentSize(*size); } } + +void InternalFlutterWindows_WindowManager_SetWindowConstraints( + HWND hwnd, + const flutter::WindowConstraints* constraints) { + flutter::HostWindow* window = flutter::HostWindow::GetThisFromHandle(hwnd); + if (window) { + window->SetConstraints(*constraints); + } +} + +void InternalFlutterWindows_WindowManager_SetFullscreen( + HWND hwnd, + const flutter::FullscreenRequest* request) { + flutter::HostWindow* window = flutter::HostWindow::GetThisFromHandle(hwnd); + const std::optional display_id = + request->has_display_id + ? std::optional(request->display_id) + : std::nullopt; + if (window) { + window->SetFullscreen(request->fullscreen, display_id); + } +} + +bool InternalFlutterWindows_WindowManager_GetFullscreen(HWND hwnd) { + flutter::HostWindow* window = flutter::HostWindow::GetThisFromHandle(hwnd); + if (window) { + return window->GetFullscreen(); + } + + return false; +} diff --git a/engine/src/flutter/shell/platform/windows/window_manager.h b/engine/src/flutter/shell/platform/windows/window_manager.h index 675b470c0681f..9cad1d5847dda 100644 --- a/engine/src/flutter/shell/platform/windows/window_manager.h +++ b/engine/src/flutter/shell/platform/windows/window_manager.h @@ -21,7 +21,29 @@ namespace flutter { class FlutterWindowsEngine; class HostWindow; -struct WindowingInitRequest; + +// Specifies a preferred content size for the window. +struct WindowSizeRequest { + bool has_preferred_view_size = false; + double preferred_view_width; + double preferred_view_height; +}; + +// Specifies a preferred constraint on the window. +struct WindowConstraints { + bool has_view_constraints = false; + double view_min_width; + double view_min_height; + double view_max_width; + double view_max_height; +}; + +// Sent by the framework to request a new window be created. +struct WindowCreationRequest { + WindowSizeRequest preferred_size; + WindowConstraints preferred_constraints; + LPCWSTR title; +}; struct WindowsMessage { FlutterViewId view_id; @@ -33,23 +55,21 @@ struct WindowsMessage { bool handled; }; -struct WindowSizing { - bool has_preferred_view_size; - double preferred_view_width; - double preferred_view_height; - bool has_view_constraints; - double view_min_width; - double view_min_height; - double view_max_width; - double view_max_height; -}; - struct WindowingInitRequest { void (*on_message)(WindowsMessage*); }; -struct WindowCreationRequest { - WindowSizing content_size; +// Returned from |InternalFlutterWindows_WindowManager_GetWindowContentSize|. +// This represents the current content size of the window. +struct ActualWindowSize { + double width; + double height; +}; + +struct FullscreenRequest { + bool fullscreen; + bool has_display_id; + FlutterEngineDisplayId display_id; }; // A manager class for managing |HostWindow| instances. @@ -117,19 +137,27 @@ HWND InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle( int64_t engine_id, FlutterViewId view_id); -struct FlutterWindowSize { - double width; - double height; -}; +FLUTTER_EXPORT +flutter::ActualWindowSize +InternalFlutterWindows_WindowManager_GetWindowContentSize(HWND hwnd); FLUTTER_EXPORT -FlutterWindowSize InternalFlutterWindows_WindowManager_GetWindowContentSize( - HWND hwnd); +void InternalFlutterWindows_WindowManager_SetWindowSize( + HWND hwnd, + const flutter::WindowSizeRequest* size); FLUTTER_EXPORT -void InternalFlutterWindows_WindowManager_SetWindowContentSize( +void InternalFlutterWindows_WindowManager_SetWindowConstraints( HWND hwnd, - const flutter::WindowSizing* size); + const flutter::WindowConstraints* constraints); + +FLUTTER_EXPORT +void InternalFlutterWindows_WindowManager_SetFullscreen( + HWND hwnd, + const flutter::FullscreenRequest* request); + +FLUTTER_EXPORT +bool InternalFlutterWindows_WindowManager_GetFullscreen(HWND hwnd); } #endif // FLUTTER_SHELL_PLATFORM_WINDOWS_WINDOW_MANAGER_H_ 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 02c943b8e227f..cc0e0c9f43695 100644 --- a/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc +++ b/engine/src/flutter/shell/platform/windows/window_manager_unittests.cc @@ -21,6 +21,7 @@ class WindowManagerTest : public WindowsTest { void SetUp() override { auto& context = GetContext(); FlutterWindowsEngineBuilder builder(context); + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); engine_ = builder.Build(); ASSERT_TRUE(engine_); @@ -49,7 +50,7 @@ class WindowManagerTest : public WindowsTest { std::unique_ptr engine_; std::optional isolate_; WindowCreationRequest creation_request_{ - .content_size = + .preferred_size = { .has_preferred_view_size = true, .preferred_view_width = 800, @@ -124,12 +125,13 @@ TEST_F(WindowManagerTest, GetWindowSize) { InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), view_id); - FlutterWindowSize size = + ActualWindowSize size = InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle); - EXPECT_EQ(size.width, creation_request()->content_size.preferred_view_width); + EXPECT_EQ(size.width, + creation_request()->preferred_size.preferred_view_width); EXPECT_EQ(size.height, - creation_request()->content_size.preferred_view_height); + creation_request()->preferred_size.preferred_view_height); } TEST_F(WindowManagerTest, SetWindowSize) { @@ -142,19 +144,174 @@ TEST_F(WindowManagerTest, SetWindowSize) { InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), view_id); - WindowSizing requestedSize{ + WindowSizeRequest requestedSize{ + .has_preferred_view_size = true, .preferred_view_width = 640, .preferred_view_height = 480, }; - InternalFlutterWindows_WindowManager_SetWindowContentSize(window_handle, - &requestedSize); + InternalFlutterWindows_WindowManager_SetWindowSize(window_handle, + &requestedSize); - FlutterWindowSize actual_size = + ActualWindowSize actual_size = InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle); EXPECT_EQ(actual_size.width, 640); EXPECT_EQ(actual_size.height, 480); } +TEST_F(WindowManagerTest, CanConstrainByMinimiumSize) { + IsolateScope isolate_scope(isolate()); + + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + const HWND window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), + view_id); + WindowConstraints constraints{.has_view_constraints = true, + .view_min_width = 900, + .view_min_height = 700, + .view_max_width = 10000, + .view_max_height = 10000}; + InternalFlutterWindows_WindowManager_SetWindowConstraints(window_handle, + &constraints); + + ActualWindowSize actual_size = + InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle); + EXPECT_EQ(actual_size.width, 900); + EXPECT_EQ(actual_size.height, 700); +} + +TEST_F(WindowManagerTest, CanConstrainByMaximumSize) { + IsolateScope isolate_scope(isolate()); + + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + const HWND window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), + view_id); + WindowConstraints constraints{.has_view_constraints = true, + .view_min_width = 0, + .view_min_height = 0, + .view_max_width = 500, + .view_max_height = 500}; + InternalFlutterWindows_WindowManager_SetWindowConstraints(window_handle, + &constraints); + + ActualWindowSize actual_size = + InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle); + EXPECT_EQ(actual_size.width, 500); + EXPECT_EQ(actual_size.height, 500); +} + +TEST_F(WindowManagerTest, CanFullscreenWindow) { + IsolateScope isolate_scope(isolate()); + + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + const HWND window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), + view_id); + + FullscreenRequest request{.fullscreen = true, .has_display_id = false}; + InternalFlutterWindows_WindowManager_SetFullscreen(window_handle, &request); + + int screen_width = GetSystemMetrics(SM_CXSCREEN); + int screen_height = GetSystemMetrics(SM_CYSCREEN); + ActualWindowSize actual_size = + InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle); + EXPECT_EQ(actual_size.width, screen_width); + EXPECT_EQ(actual_size.height, screen_height); + EXPECT_TRUE( + InternalFlutterWindows_WindowManager_GetFullscreen(window_handle)); +} + +TEST_F(WindowManagerTest, CanUnfullscreenWindow) { + IsolateScope isolate_scope(isolate()); + + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + const HWND window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), + view_id); + + FullscreenRequest request{.fullscreen = true, .has_display_id = false}; + InternalFlutterWindows_WindowManager_SetFullscreen(window_handle, &request); + + request.fullscreen = false; + InternalFlutterWindows_WindowManager_SetFullscreen(window_handle, &request); + + ActualWindowSize actual_size = + InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle); + EXPECT_EQ(actual_size.width, 800); + EXPECT_EQ(actual_size.height, 600); + EXPECT_FALSE( + InternalFlutterWindows_WindowManager_GetFullscreen(window_handle)); +} + +TEST_F(WindowManagerTest, CanSetWindowSizeWhileFullscreen) { + IsolateScope isolate_scope(isolate()); + + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + const HWND window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), + view_id); + + FullscreenRequest request{.fullscreen = true, .has_display_id = false}; + InternalFlutterWindows_WindowManager_SetFullscreen(window_handle, &request); + + WindowSizeRequest requestedSize{ + + .has_preferred_view_size = true, + .preferred_view_width = 500, + .preferred_view_height = 500, + }; + InternalFlutterWindows_WindowManager_SetWindowSize(window_handle, + &requestedSize); + + request.fullscreen = false; + InternalFlutterWindows_WindowManager_SetFullscreen(window_handle, &request); + + ActualWindowSize actual_size = + InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle); + EXPECT_EQ(actual_size.width, 500); + EXPECT_EQ(actual_size.height, 500); +} + +TEST_F(WindowManagerTest, CanSetWindowConstraintsWhileFullscreen) { + IsolateScope isolate_scope(isolate()); + + const int64_t view_id = + InternalFlutterWindows_WindowManager_CreateRegularWindow( + engine_id(), creation_request()); + const HWND window_handle = + InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle(engine_id(), + view_id); + + FullscreenRequest request{.fullscreen = true, .has_display_id = false}; + InternalFlutterWindows_WindowManager_SetFullscreen(window_handle, &request); + + WindowConstraints constraints{.has_view_constraints = true, + .view_min_width = 0, + .view_min_height = 0, + .view_max_width = 500, + .view_max_height = 500}; + InternalFlutterWindows_WindowManager_SetWindowConstraints(window_handle, + &constraints); + + request.fullscreen = false; + InternalFlutterWindows_WindowManager_SetFullscreen(window_handle, &request); + + ActualWindowSize actual_size = + InternalFlutterWindows_WindowManager_GetWindowContentSize(window_handle); + EXPECT_EQ(actual_size.width, 500); + EXPECT_EQ(actual_size.height, 500); +} + } // namespace testing } // namespace flutter From ef76a61283e6cc645d11e169947389b5f767b6a9 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 13 Aug 2025 11:34:12 -0400 Subject: [PATCH 014/720] Roll Skia from 29e3e1ab7f62 to f7fdda3cd0e6 (3 revisions) (#173709) https://skia.googlesource.com/skia.git/+log/29e3e1ab7f62..f7fdda3cd0e6 2025-08-13 mike@reedtribe.org Conditionally switch technique for rect::bounds 2025-08-13 robertphillips@google.com Revert "Fix bazel release build to not compile debug code" 2025-08-13 skia-autoroll@skia-public.iam.gserviceaccount.com Roll ANGLE from 3643c21d9d0c to cfeea900811c (12 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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 df70a545faa3e..b8db7732e4676 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '29e3e1ab7f62c6dc7a71e073f108b1f13274da20', + 'skia_revision': 'f7fdda3cd0e60ae063fdc33a33175e57a87bf23c', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From f26eddba005398f1ff7edfd878f0d60d03e44346 Mon Sep 17 00:00:00 2001 From: Jamil Saadeh Date: Wed, 13 Aug 2025 18:36:14 +0300 Subject: [PATCH 015/720] Null aware elements clean-ups (#173074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converted more null checks to null aware elements. The analyzer had many false negatives and the regex became pretty wild to find many of them 😂 I've found more in the engine folder but I'll apply the changes in a separate PR Part of #172188 *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 [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]. **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 [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/framework/ab.dart | 2 +- dev/devicelab/lib/framework/runner.dart | 2 +- .../lib/src/foundation/diagnostics.dart | 4 +- .../flutter/lib/src/services/autofill.dart | 2 +- .../lib/src/services/platform_views.dart | 8 ++-- .../flutter/lib/src/services/text_input.dart | 8 +--- .../lib/src/widgets/widget_inspector.dart | 7 +--- .../lib/src/common/request_data.dart | 2 +- .../flutter_tools/lib/src/android/java.dart | 2 +- .../flutter_tools/lib/src/build_info.dart | 18 ++++---- .../lib/src/build_system/build_system.dart | 2 +- .../lib/src/commands/daemon.dart | 2 +- packages/flutter_tools/lib/src/daemon.dart | 13 ++---- .../debug_adapters/flutter_adapter_args.dart | 20 ++++----- .../lib/src/drive/web_driver_service.dart | 4 +- .../lib/src/platform_plugins.dart | 42 +++++++++---------- packages/flutter_tools/lib/src/project.dart | 4 +- .../lib/src/proxied_devices/devices.dart | 2 +- .../lib/src/reporting/usage.dart | 4 +- .../lib/src/test/flutter_tester_device.dart | 2 +- .../lib/src/test/flutter_web_platform.dart | 2 +- .../flutter_tools/lib/src/test/runner.dart | 2 +- packages/flutter_tools/lib/src/version.dart | 6 +-- packages/flutter_tools/lib/src/vmservice.dart | 2 +- .../flutter_tools/lib/src/web/compile.dart | 4 +- .../lib/src/web/compiler_config.dart | 2 +- .../lib/src/windows/build_windows.dart | 2 +- .../hermetic/build_ios_test.dart | 2 +- .../coverage_collector_test.dart | 2 +- .../test/general.shard/daemon_test.dart | 2 +- .../test/test_golden_comparator_test.dart | 2 +- .../test/general.shard/version_test.dart | 2 +- packages/flutter_tools/test/src/common.dart | 2 +- packages/flutter_tools/test/src/fakes.dart | 2 +- packages/integration_test/lib/common.dart | 2 +- 35 files changed, 85 insertions(+), 101 deletions(-) diff --git a/dev/devicelab/lib/framework/ab.dart b/dev/devicelab/lib/framework/ab.dart index 5f9c2e9639f9c..ad20fe8bb40ba 100644 --- a/dev/devicelab/lib/framework/ab.dart +++ b/dev/devicelab/lib/framework/ab.dart @@ -89,7 +89,7 @@ class ABTest { kBenchmarkTypeKeyName: kBenchmarkResultsType, kBenchmarkVersionKeyName: kBenchmarkABVersion, kLocalEngineKeyName: localEngine, - if (localEngineHost != null) kLocalEngineHostKeyName: localEngineHost, + kLocalEngineHostKeyName: ?localEngineHost, kTaskNameKeyName: taskName, kRunStartKeyName: runStart.toIso8601String(), kRunEndKeyName: runEnd!.toIso8601String(), diff --git a/dev/devicelab/lib/framework/runner.dart b/dev/devicelab/lib/framework/runner.dart index ef73d6ed0ba55..05d6e2a37f3cd 100644 --- a/dev/devicelab/lib/framework/runner.dart +++ b/dev/devicelab/lib/framework/runner.dart @@ -199,7 +199,7 @@ Future runTask( taskExecutable, ...?taskArgs, ], - environment: {if (deviceId != null) DeviceIdEnvName: deviceId}, + environment: {DeviceIdEnvName: ?deviceId}, ); bool runnerFinished = false; diff --git a/packages/flutter/lib/src/foundation/diagnostics.dart b/packages/flutter/lib/src/foundation/diagnostics.dart index 5a6158ba64eb4..d40371dfa0c60 100644 --- a/packages/flutter/lib/src/foundation/diagnostics.dart +++ b/packages/flutter/lib/src/foundation/diagnostics.dart @@ -1647,11 +1647,11 @@ abstract class DiagnosticsNode { result = { 'description': toDescription(), 'type': runtimeType.toString(), - if (name != null) 'name': name, + 'name': ?name, if (!showSeparator) 'showSeparator': showSeparator, if (level != DiagnosticLevel.info) 'level': level.name, if (!showName) 'showName': showName, - if (emptyBodyDescription != null) 'emptyBodyDescription': emptyBodyDescription, + 'emptyBodyDescription': ?emptyBodyDescription, if (style != DiagnosticsTreeStyle.sparse) 'style': style!.name, if (allowTruncate) 'allowTruncate': allowTruncate, if (hasChildren) 'hasChildren': hasChildren, diff --git a/packages/flutter/lib/src/services/autofill.dart b/packages/flutter/lib/src/services/autofill.dart index 48a8ce831a2fa..8326cbe957a85 100644 --- a/packages/flutter/lib/src/services/autofill.dart +++ b/packages/flutter/lib/src/services/autofill.dart @@ -733,7 +733,7 @@ class AutofillConfiguration { 'uniqueIdentifier': uniqueIdentifier, 'hints': autofillHints, 'editingValue': currentEditingValue.toJSON(), - if (hintText != null) 'hintText': hintText, + 'hintText': ?hintText, } : null; } diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index 8a24b22396133..3e11eb0bd5662 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -1312,11 +1312,11 @@ abstract class _AndroidViewControllerInternals { 'viewType': viewType, 'direction': AndroidViewController._getAndroidDirection(layoutDirection), if (hybrid) 'hybrid': hybrid, - if (size != null) 'width': size.width, - if (size != null) 'height': size.height, + 'width': ?size?.width, + 'height': ?size?.height, if (hybridFallback) 'hybridFallback': hybridFallback, - if (position != null) 'left': position.dx, - if (position != null) 'top': position.dy, + 'left': ?position?.dx, + 'top': ?position?.dy, }; if (creationParams != null) { final ByteData paramsByteData = creationParams.codec.encodeMessage(creationParams.data)!; diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart index 4e8a6d2dae9ea..0116bfa54686b 100644 --- a/packages/flutter/lib/src/services/text_input.dart +++ b/packages/flutter/lib/src/services/text_input.dart @@ -791,7 +791,7 @@ class TextInputConfiguration { 'keyboardAppearance': keyboardAppearance.toString(), 'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning, 'contentCommitMimeTypes': allowedMimeTypes, - if (autofill != null) 'autofill': autofill, + 'autofill': ?autofill, 'enableDeltaModel': enableDeltaModel, 'hintLocales': hintLocales?.map((Locale locale) => locale.toLanguageTag()).toList(), }; @@ -2866,11 +2866,7 @@ sealed class IOSSystemContextMenuItemData { /// Returns json for use in method channel calls, specifically /// `ContextMenu.showSystemContextMenu`. Map get _json { - return { - 'callbackId': hashCode, - if (title != null) 'title': title, - 'type': _jsonType, - }; + return {'callbackId': hashCode, 'title': ?title, 'type': _jsonType}; } @override diff --git a/packages/flutter/lib/src/widgets/widget_inspector.dart b/packages/flutter/lib/src/widgets/widget_inspector.dart index be7394b9c7cad..607f26f4031d2 100644 --- a/packages/flutter/lib/src/widgets/widget_inspector.dart +++ b/packages/flutter/lib/src/widgets/widget_inspector.dart @@ -4074,12 +4074,7 @@ class _Location { final String? name; Map toJsonMap() { - return { - 'file': file, - 'line': line, - 'column': column, - if (name != null) 'name': name, - }; + return {'file': file, 'line': line, 'column': column, 'name': ?name}; } @override diff --git a/packages/flutter_driver/lib/src/common/request_data.dart b/packages/flutter_driver/lib/src/common/request_data.dart index de4b972be3581..89871f99b9648 100644 --- a/packages/flutter_driver/lib/src/common/request_data.dart +++ b/packages/flutter_driver/lib/src/common/request_data.dart @@ -24,7 +24,7 @@ class RequestData extends Command { @override Map serialize() => - super.serialize()..addAll({if (message != null) 'message': message!}); + super.serialize()..addAll({'message': ?message}); } /// The result of the [RequestData] command. diff --git a/packages/flutter_tools/lib/src/android/java.dart b/packages/flutter_tools/lib/src/android/java.dart index 2b34b47cca11d..a741041033630 100644 --- a/packages/flutter_tools/lib/src/android/java.dart +++ b/packages/flutter_tools/lib/src/android/java.dart @@ -152,7 +152,7 @@ class Java { /// This map should be used as the environment when invoking any Java-dependent /// processes, such as Gradle or Android SDK tools (avdmanager, sdkmanager, etc.) Map get environment => { - if (javaHome != null) javaHomeEnvironmentVariable: javaHome!, + javaHomeEnvironmentVariable: ?javaHome, 'PATH': _fileSystem.path.dirname(binaryPath) + _os.pathVarSeparator + diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index b94cd02ceb673..2a5d1676cb9a1 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -313,18 +313,18 @@ class BuildInfo { kBuildMode: mode.cliName, if (dartDefines.isNotEmpty) kDartDefines: encodeDartDefines(dartDefines), kDartObfuscation: dartObfuscation.toString(), - if (frontendServerStarterPath != null) kFrontendServerStarterPath: frontendServerStarterPath!, + kFrontendServerStarterPath: ?frontendServerStarterPath, if (extraFrontEndOptions.isNotEmpty) kExtraFrontEndOptions: extraFrontEndOptions.join(','), if (extraGenSnapshotOptions.isNotEmpty) kExtraGenSnapshotOptions: extraGenSnapshotOptions.join(','), - if (splitDebugInfoPath != null) kSplitDebugInfo: splitDebugInfoPath!, + kSplitDebugInfo: ?splitDebugInfoPath, kTrackWidgetCreation: trackWidgetCreation.toString(), kIconTreeShakerFlag: treeShakeIcons.toString(), - if (codeSizeDirectory != null) kCodeSizeDirectory: codeSizeDirectory!, + kCodeSizeDirectory: ?codeSizeDirectory, if (fileSystemRoots.isNotEmpty) kFileSystemRoots: fileSystemRoots.join(','), - if (fileSystemScheme != null) kFileSystemScheme: fileSystemScheme!, - if (buildName != null) kBuildName: buildName!, - if (buildNumber != null) kBuildNumber: buildNumber!, + kFileSystemScheme: ?fileSystemScheme, + kBuildName: ?buildName, + kBuildNumber: ?buildNumber, if (useLocalCanvasKit) kUseLocalCanvasKitFlag: useLocalCanvasKit.toString(), }; } @@ -343,14 +343,14 @@ class BuildInfo { 'EXTRA_FRONT_END_OPTIONS': extraFrontEndOptions.join(','), if (extraGenSnapshotOptions.isNotEmpty) 'EXTRA_GEN_SNAPSHOT_OPTIONS': extraGenSnapshotOptions.join(','), - if (splitDebugInfoPath != null) 'SPLIT_DEBUG_INFO': splitDebugInfoPath!, + 'SPLIT_DEBUG_INFO': ?splitDebugInfoPath, 'TRACK_WIDGET_CREATION': trackWidgetCreation.toString(), 'TREE_SHAKE_ICONS': treeShakeIcons.toString(), if (performanceMeasurementFile != null) 'PERFORMANCE_MEASUREMENT_FILE': performanceMeasurementFile!, 'PACKAGE_CONFIG': packageConfigPath, - if (codeSizeDirectory != null) 'CODE_SIZE_DIRECTORY': codeSizeDirectory!, - if (flavor != null) 'FLAVOR': flavor!, + 'CODE_SIZE_DIRECTORY': ?codeSizeDirectory, + 'FLAVOR': ?flavor, }; } diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart index 7b44ee6748368..0146558ea2332 100644 --- a/packages/flutter_tools/lib/src/build_system/build_system.dart +++ b/packages/flutter_tools/lib/src/build_system/build_system.dart @@ -227,7 +227,7 @@ abstract class Target { 'dependencies': [for (final Target target in dependencies) target.name], 'inputs': [for (final File file in resolveInputs(environment).sources) file.path], 'outputs': [for (final File file in resolveOutputs(environment).sources) file.path], - if (key != null) 'buildKey': key, + 'buildKey': ?key, 'stamp': _findStampFile(environment).absolute.path, }; } diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index ba45ab2846f1b..d34f163b74c1a 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -1824,7 +1824,7 @@ final class MachineOutputLogger extends DelegatingLogger { final event = { 'id': eventId, 'progressId': eventType, - if (message != null) 'message': message, + 'message': ?message, 'finished': finished, }; diff --git a/packages/flutter_tools/lib/src/daemon.dart b/packages/flutter_tools/lib/src/daemon.dart index f11629a9a8b51..efa0f541746e8 100644 --- a/packages/flutter_tools/lib/src/daemon.dart +++ b/packages/flutter_tools/lib/src/daemon.dart @@ -282,11 +282,7 @@ class DaemonConnection { final id = '${++_outgoingRequestId}'; final completer = Completer(); _outgoingRequestCompleters[id] = completer; - final data = { - 'id': id, - 'method': method, - if (params != null) 'params': params, - }; + final data = {'id': id, 'method': method, 'params': ?params}; _logger.printTrace('-> Sending to daemon, id = $id, method = $method'); _daemonStreams.send(data, binary); return completer.future; @@ -294,7 +290,7 @@ class DaemonConnection { /// Sends a response to the other end of the connection. void sendResponse(Object id, [Object? result]) { - _daemonStreams.send({'id': id, if (result != null) 'result': result}); + _daemonStreams.send({'id': id, 'result': ?result}); } /// Sends an error response to the other end of the connection. @@ -304,10 +300,7 @@ class DaemonConnection { /// Sends an event to the client. void sendEvent(String name, [Object? params, List? binary]) { - _daemonStreams.send({ - 'event': name, - if (params != null) 'params': params, - }, binary); + _daemonStreams.send({'event': name, 'params': ?params}, binary); } /// Handles the input from the stream. 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 ff5b3776b19f8..3be9e9371295e 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 @@ -81,10 +81,10 @@ class FlutterAttachRequestArguments extends DartCommonLaunchAttachRequestArgumen @override Map toJson() => { ...super.toJson(), - if (toolArgs != null) 'toolArgs': toolArgs, - if (customTool != null) 'customTool': customTool, - if (customToolReplacesArgs != null) 'customToolReplacesArgs': customToolReplacesArgs, - if (vmServiceUri != null) 'vmServiceUri': vmServiceUri, + 'toolArgs': ?toolArgs, + 'customTool': ?customTool, + 'customToolReplacesArgs': ?customToolReplacesArgs, + 'vmServiceUri': ?vmServiceUri, }; } @@ -162,11 +162,11 @@ class FlutterLaunchRequestArguments extends DartCommonLaunchAttachRequestArgumen @override Map toJson() => { ...super.toJson(), - if (noDebug != null) 'noDebug': noDebug, - if (program != null) 'program': program, - if (args != null) 'args': args, - if (toolArgs != null) 'toolArgs': toolArgs, - if (customTool != null) 'customTool': customTool, - if (customToolReplacesArgs != null) 'customToolReplacesArgs': customToolReplacesArgs, + 'noDebug': ?noDebug, + 'program': ?program, + 'args': ?args, + 'toolArgs': ?toolArgs, + 'customTool': ?customTool, + 'customToolReplacesArgs': ?customToolReplacesArgs, }; } 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 c7d878424d7ef..75e7e088431d6 100644 --- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart +++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart @@ -352,8 +352,8 @@ Map getDesiredCapabilities( 'v8,blink.console,benchmark,blink,' 'blink.user_timing', }, - if (chromeBinary != null) 'binary': chromeBinary, - if (mobileEmulation != null) 'mobileEmulation': mobileEmulation, + 'binary': ?chromeBinary, + 'mobileEmulation': ?mobileEmulation, }, }, Browser.firefox => { diff --git a/packages/flutter_tools/lib/src/platform_plugins.dart b/packages/flutter_tools/lib/src/platform_plugins.dart index a7d066afea783..0dac5673cb6a5 100644 --- a/packages/flutter_tools/lib/src/platform_plugins.dart +++ b/packages/flutter_tools/lib/src/platform_plugins.dart @@ -166,12 +166,12 @@ class AndroidPlugin extends PluginPlatform implements NativeOrDartPlugin { Map toMap() { return { 'name': name, - if (package != null) 'package': package, - if (pluginClass != null) 'class': pluginClass, - if (dartPluginClass != null) kDartPluginClass: dartPluginClass, - if (dartFileName != null) kDartFileName: dartFileName, + 'package': ?package, + 'class': ?pluginClass, + kDartPluginClass: ?dartPluginClass, + kDartFileName: ?dartFileName, if (ffiPlugin) kFfiPlugin: true, - if (defaultPackage != null) kDefaultPackage: defaultPackage, + kDefaultPackage: ?defaultPackage, // Mustache doesn't support complex types. 'supportsEmbeddingV1': _supportedEmbeddings.contains('1'), 'supportsEmbeddingV2': _supportedEmbeddings.contains('2'), @@ -326,12 +326,12 @@ class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin, DarwinPlug return { 'name': name, 'prefix': classPrefix, - if (pluginClass != null) 'class': pluginClass, - if (dartPluginClass != null) kDartPluginClass: dartPluginClass, - if (dartFileName != null) kDartFileName: dartFileName, + 'class': ?pluginClass, + kDartPluginClass: ?dartPluginClass, + kDartFileName: ?dartFileName, if (ffiPlugin) kFfiPlugin: true, if (sharedDarwinSource) kSharedDarwinSource: true, - if (defaultPackage != null) kDefaultPackage: defaultPackage, + kDefaultPackage: ?defaultPackage, }; } } @@ -413,12 +413,12 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin, DarwinPl Map toMap() { return { 'name': name, - if (pluginClass != null) 'class': pluginClass, - if (dartPluginClass != null) kDartPluginClass: dartPluginClass, - if (dartFileName != null) kDartFileName: dartFileName, + 'class': ?pluginClass, + kDartPluginClass: ?dartPluginClass, + kDartFileName: ?dartFileName, if (ffiPlugin) kFfiPlugin: true, if (sharedDarwinSource) kSharedDarwinSource: true, - if (defaultPackage != null) kDefaultPackage: defaultPackage, + kDefaultPackage: ?defaultPackage, }; } } @@ -513,12 +513,12 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, Varian Map toMap() { return { 'name': name, - if (pluginClass != null) 'class': pluginClass, + 'class': ?pluginClass, if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!), - if (dartPluginClass != null) kDartPluginClass: dartPluginClass, - if (dartFileName != null) kDartFileName: dartFileName, + kDartPluginClass: ?dartPluginClass, + kDartFileName: ?dartFileName, if (ffiPlugin) kFfiPlugin: true, - if (defaultPackage != null) kDefaultPackage: defaultPackage, + kDefaultPackage: ?defaultPackage, }; } } @@ -596,12 +596,12 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin { Map toMap() { return { 'name': name, - if (pluginClass != null) 'class': pluginClass, + 'class': ?pluginClass, if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!), - if (dartPluginClass != null) kDartPluginClass: dartPluginClass, - if (dartFileName != null) kDartFileName: dartFileName, + kDartPluginClass: ?dartPluginClass, + kDartFileName: ?dartFileName, if (ffiPlugin) kFfiPlugin: true, - if (defaultPackage != null) kDefaultPackage: defaultPackage, + kDefaultPackage: ?defaultPackage, }; } } diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index ffefebb07eadd..c857ffe7003f5 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -414,8 +414,8 @@ class FlutterProject { final String? buildNumber = manifest.buildNumber; final versionFileJson = { 'app_name': manifest.appName, - if (buildName != null) 'version': buildName, - if (buildNumber != null) 'build_number': buildNumber, + 'version': ?buildName, + 'build_number': ?buildNumber, 'package_name': manifest.appName, }; return jsonEncode(versionFileJson); diff --git a/packages/flutter_tools/lib/src/proxied_devices/devices.dart b/packages/flutter_tools/lib/src/proxied_devices/devices.dart index 0915476983e66..658027245fffc 100644 --- a/packages/flutter_tools/lib/src/proxied_devices/devices.dart +++ b/packages/flutter_tools/lib/src/proxied_devices/devices.dart @@ -519,7 +519,7 @@ class _ProxiedLogReader extends DeviceLogReader { (String? applicationPackageId) async => _cast( await connection.sendRequest('device.logReader.start', { 'deviceId': device.id, - if (applicationPackageId != null) 'applicationPackageId': applicationPackageId, + 'applicationPackageId': ?applicationPackageId, }), ), ); diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart index 1bc0f92ce49c4..cdea29afd298d 100644 --- a/packages/flutter_tools/lib/src/reporting/usage.dart +++ b/packages/flutter_tools/lib/src/reporting/usage.dart @@ -379,8 +379,8 @@ class LogToFileAnalytics extends AnalyticsMock { final parameters = { 'variableName': variableName, 'time': '$time', - if (category != null) 'category': category, - if (label != null) 'label': label, + 'category': ?category, + 'label': ?label, }; _sendController.add(parameters); logFile.writeAsStringSync('timing $parameters\n', mode: FileMode.append); diff --git a/packages/flutter_tools/lib/src/test/flutter_tester_device.dart b/packages/flutter_tools/lib/src/test/flutter_tester_device.dart index dfc34548d6673..6986b06f7c8d2 100644 --- a/packages/flutter_tools/lib/src/test/flutter_tester_device.dart +++ b/packages/flutter_tools/lib/src/test/flutter_tester_device.dart @@ -138,7 +138,7 @@ class FlutterTesterTestDevice extends TestDevice { 'APP_NAME': flutterProject?.manifest.appName ?? '', if (debuggingOptions.enableImpeller == ImpellerStatus.enabled) 'FLUTTER_TEST_IMPELLER': 'true', - if (testAssetDirectory != null) 'UNIT_TEST_ASSETS': testAssetDirectory!, + 'UNIT_TEST_ASSETS': ?testAssetDirectory, if (platform.isWindows && nativeAssetsBuilder != null && flutterProject != null) 'PATH': '${nativeAssetsBuilder!.windowsBuildDirectory(flutterProject!)};${platform.environment['PATH']}', 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 04716575657e5..010876f96729e 100644 --- a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart +++ b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart @@ -57,7 +57,7 @@ shelf.Handler createDirectoryHandler(Directory directory, {required bool crossOr return shelf.Response.ok( file.openRead(), headers: { - if (contentType != null) 'Content-Type': contentType, + 'Content-Type': ?contentType, if (needsCrossOriginIsolated) ...kMultiThreadedHeaders, }, ); diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart index 86aa2ea08820b..d47d28f605bd9 100644 --- a/packages/flutter_tools/lib/src/test/runner.dart +++ b/packages/flutter_tools/lib/src/test/runner.dart @@ -734,7 +734,7 @@ class SpawnPlugin extends PlatformPlugin { 'FLUTTER_TEST': flutterTest, 'FONTCONFIG_FILE': FontConfigManager().fontConfigFile.path, 'APP_NAME': flutterProject.manifest.appName, - if (testAssetDirectory != null) 'UNIT_TEST_ASSETS': testAssetDirectory, + 'UNIT_TEST_ASSETS': ?testAssetDirectory, if (nativeAssetsBuilder != null && globals.platform.isWindows) 'PATH': '${nativeAssetsBuilder.windowsBuildDirectory(flutterProject)};${globals.platform.environment['PATH']}', diff --git a/packages/flutter_tools/lib/src/version.dart b/packages/flutter_tools/lib/src/version.dart index 97801031e507b..a57787931d1c2 100644 --- a/packages/flutter_tools/lib/src/version.dart +++ b/packages/flutter_tools/lib/src/version.dart @@ -271,9 +271,9 @@ abstract class FlutterVersion { 'frameworkRevision': frameworkRevision, 'frameworkCommitDate': frameworkCommitDate, 'engineRevision': engineRevision, - if (engineCommitDate != null) 'engineCommitDate': engineCommitDate!, - if (engineContentHash != null) 'engineContentHash': engineContentHash!, - if (engineBuildDate != null) 'engineBuildDate': engineBuildDate!, + 'engineCommitDate': ?engineCommitDate, + 'engineContentHash': ?engineContentHash, + 'engineBuildDate': ?engineBuildDate, 'dartSdkVersion': dartSdkVersion, 'devToolsVersion': devToolsVersion, 'flutterVersion': frameworkVersion, diff --git a/packages/flutter_tools/lib/src/vmservice.dart b/packages/flutter_tools/lib/src/vmservice.dart index 2fdf3374a4d81..21d21501f448c 100644 --- a/packages/flutter_tools/lib/src/vmservice.dart +++ b/packages/flutter_tools/lib/src/vmservice.dart @@ -789,7 +789,7 @@ class FlutterVmService { }) async { final vm_service.Response? response = await _checkedCallServiceExtension( method, - args: {if (isolateId != null) 'isolateId': isolateId, ...?args}, + args: {'isolateId': ?isolateId, ...?args}, ); return response?.json; } diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart index 1e1a72a519829..b79ee7c309976 100644 --- a/packages/flutter_tools/lib/src/web/compile.dart +++ b/packages/flutter_tools/lib/src/web/compile.dart @@ -102,8 +102,8 @@ class WebBuilder { defines: { kTargetFile: target, kHasWebPlugins: hasWebPlugins.toString(), - if (baseHref != null) kBaseHref: baseHref, - if (staticAssetsUrl != null) kStaticAssetsUrl: staticAssetsUrl, + kBaseHref: ?baseHref, + kStaticAssetsUrl: ?staticAssetsUrl, kServiceWorkerStrategy: serviceWorkerStrategy.cliName, ...buildInfo.toBuildSystemEnvironment(), }, diff --git a/packages/flutter_tools/lib/src/web/compiler_config.dart b/packages/flutter_tools/lib/src/web/compiler_config.dart index 4479e06be5ebe..aef8968a7d8ca 100644 --- a/packages/flutter_tools/lib/src/web/compiler_config.dart +++ b/packages/flutter_tools/lib/src/web/compiler_config.dart @@ -43,7 +43,7 @@ sealed class WebCompilerConfig { String get buildKey; Map get buildEventAnalyticsValues => { - if (optimizationLevel != null) 'optimizationLevel': optimizationLevel!, + 'optimizationLevel': ?optimizationLevel, }; Map get _buildKeyMap => { diff --git a/packages/flutter_tools/lib/src/windows/build_windows.dart b/packages/flutter_tools/lib/src/windows/build_windows.dart index 9eee4dfdd450e..44d8992e29e37 100644 --- a/packages/flutter_tools/lib/src/windows/build_windows.dart +++ b/packages/flutter_tools/lib/src/windows/build_windows.dart @@ -281,7 +281,7 @@ void _writeGeneratedFlutterConfig( 'FLUTTER_ROOT': Cache.flutterRoot!, 'FLUTTER_EPHEMERAL_DIR': windowsProject.ephemeralDirectory.path, 'PROJECT_DIR': windowsProject.parent.directory.path, - if (target != null) 'FLUTTER_TARGET': target, + 'FLUTTER_TARGET': ?target, ...buildInfo.toEnvironmentConfig(), }; final LocalEngineInfo? localEngineInfo = globals.artifacts?.localEngineInfo; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart index 4ab321cb85e2e..70ef5188b832c 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_ios_test.dart @@ -49,7 +49,7 @@ class FakeXcodeProjectInterpreterWithBuildSettings extends FakeXcodeProjectInter 'PRODUCT_BUNDLE_IDENTIFIER': productBundleIdentifier ?? 'io.flutter.someProject', 'TARGET_BUILD_DIR': 'build/ios/Release-iphoneos', 'WRAPPER_NAME': 'Runner.app', - if (developmentTeam != null) 'DEVELOPMENT_TEAM': developmentTeam!, + 'DEVELOPMENT_TEAM': ?developmentTeam, }; } diff --git a/packages/flutter_tools/test/general.shard/coverage_collector_test.dart b/packages/flutter_tools/test/general.shard/coverage_collector_test.dart index 082d1678bde46..0ac9783ea323d 100644 --- a/packages/flutter_tools/test/general.shard/coverage_collector_test.dart +++ b/packages/flutter_tools/test/general.shard/coverage_collector_test.dart @@ -770,7 +770,7 @@ FakeVmServiceHost createFakeVmServiceHostWithFooAndBar({ 'forceCompile': true, 'reportLines': true, 'librariesAlreadyCompiled': librariesAlreadyCompiled, - if (libraryFilters != null) 'libraryFilters': libraryFilters, + 'libraryFilters': ?libraryFilters, }, jsonResponse: SourceReport( ranges: [ diff --git a/packages/flutter_tools/test/general.shard/daemon_test.dart b/packages/flutter_tools/test/general.shard/daemon_test.dart index 59f2ea75180e1..708480a566882 100644 --- a/packages/flutter_tools/test/general.shard/daemon_test.dart +++ b/packages/flutter_tools/test/general.shard/daemon_test.dart @@ -197,7 +197,7 @@ void main() { Map testCommand(int id, [int? binarySize]) => { 'id': id, 'method': 'test', - if (binarySize != null) '_binaryLength': binarySize, + '_binaryLength': ?binarySize, }; List testCommandBinary(int id, [int? binarySize]) => utf8.encode('[${json.encode(testCommand(id, binarySize))}]\n'); diff --git a/packages/flutter_tools/test/general.shard/test/test_golden_comparator_test.dart b/packages/flutter_tools/test/general.shard/test/test_golden_comparator_test.dart index c81b4846a08d0..161ea35c6fc5b 100644 --- a/packages/flutter_tools/test/general.shard/test/test_golden_comparator_test.dart +++ b/packages/flutter_tools/test/general.shard/test/test_golden_comparator_test.dart @@ -242,7 +242,7 @@ void main() { } String _encodeStdout({required bool success, String? message}) { - return jsonEncode({'success': success, if (message != null) 'message': message}); + return jsonEncode({'success': success, 'message': ?message}); } final class _FakeTestCompiler extends Fake implements TestCompiler { diff --git a/packages/flutter_tools/test/general.shard/version_test.dart b/packages/flutter_tools/test/general.shard/version_test.dart index 84fa833a1aab3..afa9b103e10db 100644 --- a/packages/flutter_tools/test/general.shard/version_test.dart +++ b/packages/flutter_tools/test/general.shard/version_test.dart @@ -735,7 +735,7 @@ void main() { VersionCheckError? runUpstreamValidator({String? versionUpstreamUrl, String? flutterGitUrl}) { final Platform testPlatform = FakePlatform( - environment: {if (flutterGitUrl != null) 'FLUTTER_GIT_URL': flutterGitUrl}, + environment: {'FLUTTER_GIT_URL': ?flutterGitUrl}, ); return VersionUpstreamValidator( version: FakeFlutterVersion(repositoryUrl: versionUpstreamUrl), diff --git a/packages/flutter_tools/test/src/common.dart b/packages/flutter_tools/test/src/common.dart index 1154f02c49a2f..283e0fa68245d 100644 --- a/packages/flutter_tools/test/src/common.dart +++ b/packages/flutter_tools/test/src/common.dart @@ -376,7 +376,7 @@ bool analyticsTimingEventExists({ final lookup = { 'workflow': workflow, 'variableName': variableName, - if (label != null) 'label': label, + 'label': ?label, }; for (final e in sentEvents) { diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index ba3198b7da87c..267bd3ee39f80 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -726,7 +726,7 @@ class FakeJava extends Fake implements Java { }) : binaryPath = binary, version = version ?? const Version.withText(19, 0, 2, 'openjdk 19.0.2 2023-01-17'), _environment = { - if (javaHome != null) Java.javaHomeEnvironmentVariable: javaHome, + Java.javaHomeEnvironmentVariable: ?javaHome, 'PATH': '/android-studio/jbr/bin', }, _canRun = canRun; diff --git a/packages/integration_test/lib/common.dart b/packages/integration_test/lib/common.dart index 4fc134c32dee4..cc31bce1f462e 100644 --- a/packages/integration_test/lib/common.dart +++ b/packages/integration_test/lib/common.dart @@ -72,7 +72,7 @@ class Response { String toJson() => json.encode({ 'result': allTestsPassed.toString(), 'failureDetails': _failureDetailsAsString(), - if (data != null) 'data': data, + 'data': ?data, }); /// Deserializes the result from JSON. From 646eecabd14f226805988c7a39455abb3a19dcd6 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Wed, 13 Aug 2025 11:57:31 -0400 Subject: [PATCH 016/720] Update pubspec yamls --- examples/multi_window_ref_app/pubspec.yaml | 2 +- packages/flutter/pubspec.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/multi_window_ref_app/pubspec.yaml b/examples/multi_window_ref_app/pubspec.yaml index eb5f7859c7404..8a5a56d7df7a4 100644 --- a/examples/multi_window_ref_app/pubspec.yaml +++ b/examples/multi_window_ref_app/pubspec.yaml @@ -45,4 +45,4 @@ dev_dependencies: flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 78a6 +# PUBSPEC CHECKSUM: moe6md diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index f08d6f9e77519..78e514622505e 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: vector_math: 2.2.0 sky_engine: sdk: flutter - ffi: ^2.1.4 + ffi: 2.1.4 dev_dependencies: flutter_driver: @@ -41,4 +41,4 @@ dev_dependencies: path: any platform: any -# PUBSPEC CHECKSUM: spkhmh +# PUBSPEC CHECKSUM: sdkqgj From 1eb2f68da5a7d38e0c3bdef887d888a6594a6a9d Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 13 Aug 2025 12:14:22 -0400 Subject: [PATCH 017/720] [ Widget Preview ] Move `--dtd-url` from a global flag to a `widget-preview start` option (#173712) `--dtd-url` is only used by widget previews and has no current usage, so this is a safe change. --- .../flutter_tools/lib/src/commands/widget_preview.dart | 10 ++++++++-- .../lib/src/runner/flutter_command_runner.dart | 7 ------- .../test/integration.shard/widget_preview_test.dart | 3 +-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart index 5c56005518e14..32854a03b03f9 100644 --- a/packages/flutter_tools/lib/src/commands/widget_preview.dart +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -25,7 +25,6 @@ import '../isolated/resident_web_runner.dart'; import '../project.dart'; import '../resident_runner.dart'; import '../runner/flutter_command.dart'; -import '../runner/flutter_command_runner.dart'; import '../web/web_device.dart'; import '../widget_preview/analytics.dart'; import '../widget_preview/dependency_graph.dart'; @@ -139,6 +138,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C 'Serve the widget preview environment using the web-server device instead of the ' 'browser.', ) + ..addOption( + kDtdUrl, + help: + 'The address of an existing Dart Tooling Daemon instance to be used by the Flutter CLI.', + hide: !verbose, + ) ..addFlag( kLaunchPreviewer, defaultsTo: true, @@ -156,6 +161,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C ); } + static const kDtdUrl = 'dtd-url'; static const kWidgetPreviewScaffoldName = 'widget_preview_scaffold'; static const kLaunchPreviewer = 'launch-previewer'; static const kHeadless = 'headless'; @@ -342,7 +348,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C /// /// If --dtd-uri is not provided, a DTD instance managed by the tool will be started. Future configureDtd() async { - final String? existingDtdUriStr = stringArg(FlutterGlobalOptions.kDtdUrl, global: true); + final String? existingDtdUriStr = stringArg(kDtdUrl); Uri? existingDtdUri; try { if (existingDtdUriStr != null) { diff --git a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart index d5016d1450e9f..34e4469b24834 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart @@ -36,7 +36,6 @@ abstract final class FlutterGlobalOptions { static const kMachineFlag = 'machine'; static const kPackagesOption = 'packages'; static const kPrefixedErrorsFlag = 'prefixed-errors'; - static const kDtdUrl = 'dtd-url'; static const kPrintDtd = 'print-dtd'; static const kQuietFlag = 'quiet'; static const kShowTestDeviceFlag = 'show-test-device'; @@ -152,12 +151,6 @@ class FlutterCommandRunner extends CommandRunner { hide: !verboseHelp, help: 'Path to your "package_config.json" file.', ); - argParser.addOption( - FlutterGlobalOptions.kDtdUrl, - help: - 'The address of an existing Dart Tooling Daemon instance to be used by the Flutter CLI.', - hide: !verboseHelp, - ); argParser.addFlag( FlutterGlobalOptions.kPrintDtd, negatable: false, diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_test.dart index 3280411ff9748..f6865f0829ca3 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test.dart @@ -11,7 +11,6 @@ import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/commands/widget_preview.dart'; import 'package:flutter_tools/src/globals.dart' as globals; -import 'package:flutter_tools/src/runner/flutter_command_runner.dart'; import 'package:flutter_tools/src/widget_preview/dtd_services.dart'; import 'package:process/process.dart'; @@ -74,7 +73,7 @@ void main() { '--verbose', '--${WidgetPreviewStartCommand.kHeadless}', if (useWebServer) '--${WidgetPreviewStartCommand.kWebServer}', - if (dtdUri != null) '--${FlutterGlobalOptions.kDtdUrl}=$dtdUri', + if (dtdUri != null) '--${WidgetPreviewStartCommand.kDtdUrl}=$dtdUri', ], workingDirectory: tempDir.path); final completer = Completer(); From 34c2a3b158b2b395cd31351c1e8a94ed4d9892b4 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 13 Aug 2025 14:04:33 -0400 Subject: [PATCH 018/720] [ Tool ] Mark Linux_pixel_7pro linux_chrome_dev_mode as bringup (#173646) There seems to be issues with Chrome flakily either having too many open tabs or none at all. This needs to be investigated further and shouldn't block the tree. Related issue: https://github.com/flutter/flutter/issues/173696 --- .ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.ci.yaml b/.ci.yaml index 4c8a09e3a124f..fdad90adc465f 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -3066,6 +3066,7 @@ targets: task_name: large_image_changer_perf_android - name: Linux_pixel_7pro linux_chrome_dev_mode + bringup: true recipe: devicelab/devicelab_drone presubmit: false timeout: 60 From dd7b2130a1ff295ef3394a6856bc3ce058d80e45 Mon Sep 17 00:00:00 2001 From: Valentin Vignal <32538273+ValentinVignal@users.noreply.github.com> Date: Thu, 14 Aug 2025 03:06:00 +0800 Subject: [PATCH 019/720] Fix `ChipThemeData` lerp for `BorderSide` (#173160) In https://github.com/flutter/flutter/pull/171945#discussion_r2245715705 we realised that some `lerp` method for `BorderSide` had a bug. This PR - Fixes a wrong interpolation of `ChipThemeData.lerp` for `side` in the case of `b` being `null` - Do some cleaning of other `lerp` of `BorderSide?` Fixes https://github.com/flutter/flutter/issues/173161 ## 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 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]. **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 [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/material/button_style.dart | 14 +------ .../lib/src/material/checkbox_theme.dart | 18 ++++----- .../flutter/lib/src/material/chip_theme.dart | 27 ++++--------- .../lib/src/material/search_bar_theme.dart | 14 +------ .../lib/src/material/search_view_theme.dart | 12 ++++-- .../flutter/lib/src/widgets/widget_state.dart | 3 ++ .../test/material/checkbox_theme_test.dart | 40 ++++++++++++++++++- .../test/material/chip_theme_test.dart | 4 +- 8 files changed, 71 insertions(+), 61 deletions(-) diff --git a/packages/flutter/lib/src/material/button_style.dart b/packages/flutter/lib/src/material/button_style.dart index 7bcac2fa73c6a..43641d2ac8a61 100644 --- a/packages/flutter/lib/src/material/button_style.dart +++ b/packages/flutter/lib/src/material/button_style.dart @@ -776,7 +776,7 @@ class ButtonStyle with Diagnosticable { iconColor: MaterialStateProperty.lerp(a?.iconColor, b?.iconColor, t, Color.lerp), iconSize: MaterialStateProperty.lerp(a?.iconSize, b?.iconSize, t, lerpDouble), iconAlignment: t < 0.5 ? a?.iconAlignment : b?.iconAlignment, - side: _lerpSides(a?.side, b?.side, t), + side: WidgetStateBorderSide.lerp(a?.side, b?.side, t), shape: MaterialStateProperty.lerp( a?.shape, b?.shape, @@ -794,16 +794,4 @@ class ButtonStyle with Diagnosticable { foregroundBuilder: t < 0.5 ? a?.foregroundBuilder : b?.foregroundBuilder, ); } - - // Special case because BorderSide.lerp() doesn't support null arguments - static MaterialStateProperty? _lerpSides( - MaterialStateProperty? a, - MaterialStateProperty? b, - double t, - ) { - if (a == null && b == null) { - return null; - } - return MaterialStateBorderSide.lerp(a, b, t); - } } diff --git a/packages/flutter/lib/src/material/checkbox_theme.dart b/packages/flutter/lib/src/material/checkbox_theme.dart index a63c3fae8ec46..3a964f5ac5f0b 100644 --- a/packages/flutter/lib/src/material/checkbox_theme.dart +++ b/packages/flutter/lib/src/material/checkbox_theme.dart @@ -235,19 +235,19 @@ class CheckboxThemeData with Diagnosticable { // Special case because BorderSide.lerp() doesn't support null arguments static BorderSide? _lerpSides(BorderSide? a, BorderSide? b, double t) { - if (a == null || b == null) { + if (a == null && b == null) { return null; } - if (identical(a, b)) { - return a; - } - if (a is MaterialStateBorderSide) { - a = a.resolve({}); + if (a is WidgetStateBorderSide) { + a = a.resolve(const {}); } - if (b is MaterialStateBorderSide) { - b = b.resolve({}); + if (b is WidgetStateBorderSide) { + b = b.resolve(const {}); } - return BorderSide.lerp(a!, b!, t); + a ??= BorderSide(width: 0, color: b!.color.withAlpha(0)); + b ??= BorderSide(width: 0, color: a.color.withAlpha(0)); + + return BorderSide.lerp(a, b, t); } } diff --git a/packages/flutter/lib/src/material/chip_theme.dart b/packages/flutter/lib/src/material/chip_theme.dart index f2210e64f4c1b..8b582e60875f1 100644 --- a/packages/flutter/lib/src/material/chip_theme.dart +++ b/packages/flutter/lib/src/material/chip_theme.dart @@ -539,7 +539,7 @@ class ChipThemeData with Diagnosticable { labelPadding: EdgeInsetsGeometry.lerp(a?.labelPadding, b?.labelPadding, t), padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t), side: _lerpSides(a?.side, b?.side, t), - shape: _lerpShapes(a?.shape, b?.shape, t), + shape: OutlinedBorder.lerp(a?.shape, b?.shape, t), labelStyle: TextStyle.lerp(a?.labelStyle, b?.labelStyle, t), secondaryLabelStyle: TextStyle.lerp(a?.secondaryLabelStyle, b?.secondaryLabelStyle, t), brightness: t < 0.5 ? a?.brightness ?? Brightness.light : b?.brightness ?? Brightness.light, @@ -566,27 +566,16 @@ class ChipThemeData with Diagnosticable { if (a == null && b == null) { return null; } - if (a is MaterialStateBorderSide) { - a = a.resolve({}); + if (a is WidgetStateBorderSide) { + a = a.resolve(const {}); } - if (b is MaterialStateBorderSide) { - b = b.resolve({}); + if (b is WidgetStateBorderSide) { + b = b.resolve(const {}); } - if (a == null) { - return BorderSide.lerp(BorderSide(width: 0, color: b!.color.withAlpha(0)), b, t); - } - if (b == null) { - return BorderSide.lerp(BorderSide(width: 0, color: a.color.withAlpha(0)), a, t); - } - return BorderSide.lerp(a, b, t); - } + a ??= BorderSide(width: 0, color: b!.color.withAlpha(0)); + b ??= BorderSide(width: 0, color: a.color.withAlpha(0)); - // TODO(perclasson): OutlinedBorder needs a lerp method - https://github.com/flutter/flutter/issues/60555. - static OutlinedBorder? _lerpShapes(OutlinedBorder? a, OutlinedBorder? b, double t) { - if (a == null && b == null) { - return null; - } - return ShapeBorder.lerp(a, b, t) as OutlinedBorder?; + return BorderSide.lerp(a, b, t); } @override diff --git a/packages/flutter/lib/src/material/search_bar_theme.dart b/packages/flutter/lib/src/material/search_bar_theme.dart index 8a99804bee029..1e8b71716ae29 100644 --- a/packages/flutter/lib/src/material/search_bar_theme.dart +++ b/packages/flutter/lib/src/material/search_bar_theme.dart @@ -155,7 +155,7 @@ class SearchBarThemeData with Diagnosticable { t, Color.lerp, ), - side: _lerpSides(a?.side, b?.side, t), + side: MaterialStateBorderSide.lerp(a?.side, b?.side, t), shape: MaterialStateProperty.lerp( a?.shape, b?.shape, @@ -304,18 +304,6 @@ class SearchBarThemeData with Diagnosticable { ), ); } - - // Special case because BorderSide.lerp() doesn't support null arguments - static MaterialStateProperty? _lerpSides( - MaterialStateProperty? a, - MaterialStateProperty? b, - double t, - ) { - if (identical(a, b)) { - return a; - } - return MaterialStateBorderSide.lerp(a, b, t); - } } /// Applies a search bar theme to descendant [SearchBar] widgets. diff --git a/packages/flutter/lib/src/material/search_view_theme.dart b/packages/flutter/lib/src/material/search_view_theme.dart index ee17846f08087..980c4a02c9bc3 100644 --- a/packages/flutter/lib/src/material/search_view_theme.dart +++ b/packages/flutter/lib/src/material/search_view_theme.dart @@ -225,12 +225,18 @@ class SearchViewThemeData with Diagnosticable { // Special case because BorderSide.lerp() doesn't support null arguments static BorderSide? _lerpSides(BorderSide? a, BorderSide? b, double t) { - if (a == null || b == null) { + if (a == null && b == null) { return null; } - if (identical(a, b)) { - return a; + if (a is WidgetStateBorderSide) { + a = a.resolve(const {}); } + if (b is WidgetStateBorderSide) { + b = b.resolve(const {}); + } + a ??= BorderSide(width: 0, color: b!.color.withAlpha(0)); + b ??= BorderSide(width: 0, color: a.color.withAlpha(0)); + return BorderSide.lerp(a, b, t); } } diff --git a/packages/flutter/lib/src/widgets/widget_state.dart b/packages/flutter/lib/src/widgets/widget_state.dart index a80e2330b5d85..b9f85d3b732f4 100644 --- a/packages/flutter/lib/src/widgets/widget_state.dart +++ b/packages/flutter/lib/src/widgets/widget_state.dart @@ -582,6 +582,9 @@ abstract class WidgetStateBorderSide extends BorderSide if (a == null && b == null) { return null; } + if (identical(a, b)) { + return a; + } return _LerpSides(a, b, t); } } diff --git a/packages/flutter/test/material/checkbox_theme_test.dart b/packages/flutter/test/material/checkbox_theme_test.dart index 685b44df6a35e..775faf3fc35e4 100644 --- a/packages/flutter/test/material/checkbox_theme_test.dart +++ b/packages/flutter/test/material/checkbox_theme_test.dart @@ -458,8 +458,44 @@ void main() { lerped.shape, const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), ); - // Returns null if either lerp value is null. - expect(lerped.side, null); + expect(lerped.side!.width, 2.0); + expect(lerped.side!.color, isSameColorAs(const Color(0x80000000))); + }); + + test('CheckboxThemeData lerp from null to populated parameters', () { + final CheckboxThemeData theme = CheckboxThemeData( + fillColor: MaterialStateProperty.all(const Color(0xfffffff0)), + checkColor: MaterialStateProperty.all(const Color(0xfffffff1)), + overlayColor: MaterialStateProperty.all(const Color(0xfffffff2)), + splashRadius: 4.0, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: const VisualDensity(vertical: 1.0, horizontal: 1.0), + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))), + side: const BorderSide(width: 4.0), + ); + final CheckboxThemeData lerped = CheckboxThemeData.lerp(null, theme, 0.25); + + expect( + lerped.fillColor!.resolve(const {}), + isSameColorAs(const Color(0x40fffff0)), + ); + expect( + lerped.checkColor!.resolve(const {}), + isSameColorAs(const Color(0x40fffff1)), + ); + expect( + lerped.overlayColor!.resolve(const {}), + isSameColorAs(const Color(0x40fffff2)), + ); + expect(lerped.splashRadius, 1); + expect(lerped.materialTapTargetSize, null); + expect(lerped.visualDensity, null); + expect( + lerped.shape, + const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(1.0))), + ); + expect(lerped.side!.width, 1.0); + expect(lerped.side!.color, isSameColorAs(const Color(0x40000000))); }); test('CheckboxThemeData lerp from populated parameters', () { diff --git a/packages/flutter/test/material/chip_theme_test.dart b/packages/flutter/test/material/chip_theme_test.dart index 6dafcefe774c3..aa5402007353f 100644 --- a/packages/flutter/test/material/chip_theme_test.dart +++ b/packages/flutter/test/material/chip_theme_test.dart @@ -738,7 +738,7 @@ void main() { expect(lerpBNull25.showCheckmark, equals(false)); expect(lerpBNull25.labelPadding, equals(const EdgeInsets.only(left: 6.0, right: 6.0))); expect(lerpBNull25.padding, equals(const EdgeInsets.all(3.0))); - expect(lerpBNull25.side!.color, isSameColorAs(Colors.black.withAlpha(0x3f))); + expect(lerpBNull25.side!.color, isSameColorAs(Colors.black.withAlpha(0xbf))); expect(lerpBNull25.shape, isA()); expect(lerpBNull25.labelStyle?.color, isSameColorAs(Colors.white.withAlpha(0xa7))); expect(lerpBNull25.secondaryLabelStyle?.color, isSameColorAs(Colors.black.withAlpha(0xa7))); @@ -760,7 +760,7 @@ void main() { expect(lerpBNull75.showCheckmark, equals(true)); expect(lerpBNull75.labelPadding, equals(const EdgeInsets.only(left: 2.0, right: 2.0))); expect(lerpBNull75.padding, equals(const EdgeInsets.all(1.0))); - expect(lerpBNull75.side!.color, isSameColorAs(Colors.black.withAlpha(0xbf))); + expect(lerpBNull75.side!.color, isSameColorAs(Colors.black.withAlpha(0x3f))); expect(lerpBNull75.shape, isA()); expect(lerpBNull75.labelStyle?.color, isSameColorAs(Colors.white.withAlpha(0x38))); expect(lerpBNull75.secondaryLabelStyle?.color, isSameColorAs(Colors.black.withAlpha(0x38))); From 61a4d24da5fe42b0961183f9dd0633f9561ad316 Mon Sep 17 00:00:00 2001 From: TheLastFlame <131446187+TheLastFlame@users.noreply.github.com> Date: Thu, 14 Aug 2025 05:07:29 +1000 Subject: [PATCH 020/720] Fix visual overlap of transparent routes barrier when using FadeForwardsPageTransitionsBuilder (#167032) Reopens https://github.com/flutter/flutter/pull/165726 with the removed test that has become irrelevant. Depends on: https://github.com/flutter/flutter/pull/167324 Actual master: https://github.com/user-attachments/assets/28619355-9c1e-4f06-9ede-38c4dddd13df After fix: https://github.com/user-attachments/assets/a5f2ecf2-5d8e-445a-b5a9-a7d6c0e3ec5d ## 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 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. --------- Co-authored-by: Justin McCandless Co-authored-by: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> --- .../src/material/page_transitions_theme.dart | 50 +++++++------ .../material/page_transitions_theme_test.dart | 75 +++++++++++++++++++ 2 files changed, 103 insertions(+), 22 deletions(-) diff --git a/packages/flutter/lib/src/material/page_transitions_theme.dart b/packages/flutter/lib/src/material/page_transitions_theme.dart index 13cd48b1b2827..c7051b1c09582 100644 --- a/packages/flutter/lib/src/material/page_transitions_theme.dart +++ b/packages/flutter/lib/src/material/page_transitions_theme.dart @@ -15,6 +15,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +import 'color_scheme.dart'; import 'colors.dart'; import 'theme.dart'; @@ -805,38 +806,43 @@ class FadeForwardsPageTransitionsBuilder extends PageTransitionsBuilder { Animation secondaryAnimation, Color? backgroundColor, Widget? child, - ) => DualTransitionBuilder( - animation: ReverseAnimation(secondaryAnimation), - forwardBuilder: (BuildContext context, Animation animation, Widget? child) { - return ColoredBox( - color: animation.isAnimating - ? backgroundColor ?? Theme.of(context).colorScheme.surface - : Colors.transparent, - child: FadeTransition( + ) { + final Widget builder = DualTransitionBuilder( + animation: ReverseAnimation(secondaryAnimation), + forwardBuilder: (BuildContext context, Animation animation, Widget? child) { + return FadeTransition( opacity: _fadeInTransition.animate(animation), child: SlideTransition( position: _secondaryForwardTranslationTween.animate(animation), child: child, ), - ), - ); - }, - reverseBuilder: (BuildContext context, Animation animation, Widget? child) { - return ColoredBox( - color: animation.isAnimating - ? backgroundColor ?? Theme.of(context).colorScheme.surface - : Colors.transparent, - child: FadeTransition( + ); + }, + reverseBuilder: (BuildContext context, Animation animation, Widget? child) { + return FadeTransition( opacity: _fadeOutTransition.animate(animation), child: SlideTransition( position: _secondaryBackwardTranslationTween.animate(animation), child: child, ), - ), - ); - }, - child: child, - ); + ); + }, + child: child, + ); + + final bool isOpaque = ModalRoute.opaqueOf(context) ?? true; + + if (!isOpaque) { + return builder; + } + + return ColoredBox( + color: secondaryAnimation.isAnimating + ? backgroundColor ?? ColorScheme.of(context).surface + : Colors.transparent, + child: builder, + ); + } @override Widget buildTransitions( diff --git a/packages/flutter/test/material/page_transitions_theme_test.dart b/packages/flutter/test/material/page_transitions_theme_test.dart index ca90afcef072a..bef2440dca09a 100644 --- a/packages/flutter/test/material/page_transitions_theme_test.dart +++ b/packages/flutter/test/material/page_transitions_theme_test.dart @@ -314,6 +314,81 @@ void main() { }, variant: TargetPlatformVariant.only(TargetPlatform.android), ); + + testWidgets( + 'FadeForwardsPageTransitionBuilder does not use ColoredBox for non-opaque routes', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: FadeForwardsPageTransitionsBuilder( + backgroundColor: Colors.lightGreen, + ), + }, + ), + ), + home: Builder( + builder: (BuildContext context) { + return Material( + child: TextButton( + onPressed: () { + Navigator.push( + context, + PageRouteBuilder( + opaque: false, + pageBuilder: (_, _, _) { + return Material( + child: TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const Text('page b')), + ); + }, + child: const Text('push b'), + ), + ); + }, + ), + ); + }, + child: const Text('push a'), + ), + ); + }, + ), + ), + ); + + await tester.tap(find.text('push a')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('push b')); + await tester.pump(const Duration(milliseconds: 400)); + + void findColoredBox() { + expect( + find.byWidgetPredicate((Widget w) => w is ColoredBox && w.color == Colors.lightGreen), + findsNothing, + ); + } + + // Check that ColoredBox is not used for non-opaque route. + findColoredBox(); + + await tester.pumpAndSettle(); + + Navigator.pop(tester.element(find.text('page b'))); + + await tester.pumpAndSettle(const Duration(milliseconds: 400)); + + // Check that ColoredBox is not used for non-opaque route + findColoredBox(); + }, + variant: TargetPlatformVariant.only(TargetPlatform.android), + ); }); testWidgets( From 4051fab3495839c28c74aa3aa698000c2eeccce2 Mon Sep 17 00:00:00 2001 From: masato Date: Thu, 14 Aug 2025 04:07:29 +0900 Subject: [PATCH 021/720] feat: Cupertino sheet implement upward stretch on full sheet (#168547) implimented(https://github.com/flutter/flutter/issues/161686) This PR implements a subtle stretch effect when the user drags the sheet upward. It achieves this by dynamically adjusting the top padding while the sheet is fully expanded. https://github.com/user-attachments/assets/ee98ab82-bc84-40b5-839f-82ae6de59e36 ## 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 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 [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: Tong Mu --- packages/flutter/lib/src/cupertino/sheet.dart | 205 +++++++++++++----- .../flutter/test/cupertino/sheet_test.dart | 32 +++ 2 files changed, 179 insertions(+), 58 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/sheet.dart b/packages/flutter/lib/src/cupertino/sheet.dart index 6d285b36116d2..474d4b3c6b418 100644 --- a/packages/flutter/lib/src/cupertino/sheet.dart +++ b/packages/flutter/lib/src/cupertino/sheet.dart @@ -29,6 +29,13 @@ const double _kRoundedDeviceCornersThreshold = 20.0; // iOS 18.0. const double _kTopGapRatio = 0.08; +// The minimum distance (i.e., maximum upward stretch) from the top of the sheet +// to the top of the screen, as a ratio of total screen height. This value represents +// how far the sheet can be temporarily pulled upward before snapping back. +// Determined through visual tuning to feel natural on +// running iOS 18.0 simulators. +const double _kStretchedTopGapRatio = 0.072; + // Tween for animating a Cupertino sheet onto the screen. // // Begins fully offscreen below the screen and ends onscreen with a small gap at @@ -353,7 +360,14 @@ class CupertinoSheetTransition extends StatefulWidget { State createState() => _CupertinoSheetTransitionState(); } -class _CupertinoSheetTransitionState extends State { +class _CupertinoSheetTransitionState extends State + with SingleTickerProviderStateMixin { + // Controls the top padding animation when the sheet is being slightly stretched upward. + late AnimationController _stretchDragController; + + // Animates the top padding of the sheet based on the _stretchDragController’s value. + late Animation _stretchDragAnimation; + // The offset animation when this page is being covered by another sheet. late Animation _secondaryPositionAnimation; @@ -369,6 +383,7 @@ class _CupertinoSheetTransitionState extends State { @override void initState() { super.initState(); + _setupAnimation(); } @@ -399,11 +414,19 @@ class _CupertinoSheetTransitionState extends State { reverseCurve: Curves.easeInToLinear, parent: widget.secondaryRouteAnimation, ); + _stretchDragController = AnimationController( + duration: const Duration(microseconds: 1), + vsync: this, + ); + _stretchDragAnimation = _stretchDragController.drive( + Tween(begin: _kTopGapRatio, end: _kStretchedTopGapRatio), + ); _secondaryPositionAnimation = _secondaryPositionCurve!.drive(_kMidUpTween); _secondaryScaleAnimation = _secondaryPositionCurve!.drive(_kScaleTween); } void _disposeCurve() { + _stretchDragController.dispose(); _primaryPositionCurve?.dispose(); _secondaryPositionCurve?.dispose(); _primaryPositionCurve = null; @@ -448,20 +471,49 @@ class _CupertinoSheetTransitionState extends State { @override Widget build(BuildContext context) { - return SizedBox.expand( - child: _coverSheetSecondaryTransition( - widget.secondaryRouteAnimation, - _coverSheetPrimaryTransition( - context, - widget.primaryRouteAnimation, - widget.linearTransition, - widget.child, + return _StretchDragControllerProvider( + controller: _stretchDragController, + child: SizedBox.expand( + child: AnimatedBuilder( + animation: _stretchDragAnimation, + builder: (BuildContext context, Widget? child) { + return Padding( + padding: EdgeInsets.only( + top: MediaQuery.heightOf(context) * _stretchDragAnimation.value, + ), + child: _coverSheetSecondaryTransition( + widget.secondaryRouteAnimation, + _coverSheetPrimaryTransition( + context, + widget.primaryRouteAnimation, + widget.linearTransition, + widget.child, + ), + ), + ); + }, ), ), ); } } +// Internally used to provide the controller for upward stretch animation. +class _StretchDragControllerProvider extends InheritedWidget { + const _StretchDragControllerProvider({required this.controller, required super.child}); + + final AnimationController controller; + + static _StretchDragControllerProvider? maybeOf(BuildContext context) { + return context.getInheritedWidgetOfExactType<_StretchDragControllerProvider>(); + } + + @override + bool updateShouldNotify(_StretchDragControllerProvider oldWidget) { + return false; + } +} + /// Route for displaying an iOS sheet styled page. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=5H-WvH5O29I} @@ -508,18 +560,14 @@ class CupertinoSheetRoute extends PageRoute with _CupertinoSheetRouteTrans @override Widget buildContent(BuildContext context) { - final double topPadding = MediaQuery.heightOf(context) * _kTopGapRatio; return MediaQuery.removePadding( context: context, removeTop: true, - child: Padding( - padding: EdgeInsets.only(top: topPadding), - child: ClipRSuperellipse( - borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - child: CupertinoUserInterfaceLevel( - data: CupertinoUserInterfaceLevelData.elevated, - child: _CupertinoSheetScope(child: builder(context)), - ), + child: ClipRSuperellipse( + borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), + child: CupertinoUserInterfaceLevel( + data: CupertinoUserInterfaceLevelData.elevated, + child: _CupertinoSheetScope(child: builder(context)), ), ), ); @@ -601,12 +649,12 @@ mixin _CupertinoSheetRouteTransitionMixin on PageRoute { return buildContent(context); } - static _CupertinoDownGestureController _startPopGesture(ModalRoute route) { - return _CupertinoDownGestureController( + static _CupertinoDragGestureController _startPopGesture(ModalRoute route) { + return _CupertinoDragGestureController( navigator: route.navigator!, getIsCurrent: () => route.isCurrent, getIsActive: () => route.isActive, - controller: route.controller!, // protected access + popDragController: route.controller!, // protected access ); } @@ -624,7 +672,7 @@ mixin _CupertinoSheetRouteTransitionMixin on PageRoute { primaryRouteAnimation: animation, secondaryRouteAnimation: secondaryAnimation, linearTransition: linearTransition, - child: _CupertinoDownGestureDetector( + child: _CupertinoDragGestureDetector( enabledCallback: () => enableDrag, onStartPopGesture: () => _startPopGesture(route), child: child, @@ -648,8 +696,8 @@ mixin _CupertinoSheetRouteTransitionMixin on PageRoute { } } -class _CupertinoDownGestureDetector extends StatefulWidget { - const _CupertinoDownGestureDetector({ +class _CupertinoDragGestureDetector extends StatefulWidget { + const _CupertinoDragGestureDetector({ super.key, required this.enabledCallback, required this.onStartPopGesture, @@ -660,20 +708,23 @@ class _CupertinoDownGestureDetector extends StatefulWidget { final ValueGetter enabledCallback; - final ValueGetter<_CupertinoDownGestureController> onStartPopGesture; + final ValueGetter<_CupertinoDragGestureController> onStartPopGesture; @override - _CupertinoDownGestureDetectorState createState() => _CupertinoDownGestureDetectorState(); + _CupertinoDragGestureDetectorState createState() => _CupertinoDragGestureDetectorState(); } -class _CupertinoDownGestureDetectorState extends State<_CupertinoDownGestureDetector> { - _CupertinoDownGestureController? _downGestureController; +class _CupertinoDragGestureDetectorState extends State<_CupertinoDragGestureDetector> { + _CupertinoDragGestureController? _dragGestureController; late VerticalDragGestureRecognizer _recognizer; + _StretchDragControllerProvider? _stretchDragController; @override void initState() { super.initState(); + assert(_stretchDragController == null); + _stretchDragController = _StretchDragControllerProvider.maybeOf(context); _recognizer = VerticalDragGestureRecognizer(debugOwner: this) ..onStart = _handleDragStart ..onUpdate = _handleDragUpdate @@ -681,17 +732,23 @@ class _CupertinoDownGestureDetectorState extends State<_CupertinoDownGestureD ..onCancel = _handleDragCancel; } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _stretchDragController = _StretchDragControllerProvider.maybeOf(context); + } + @override void dispose() { _recognizer.dispose(); // If this is disposed during a drag, call navigator.didStopUserGesture. - if (_downGestureController != null) { + if (_dragGestureController != null) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (_downGestureController?.navigator.mounted ?? false) { - _downGestureController?.navigator.didStopUserGesture(); + if (_dragGestureController?.navigator.mounted ?? false) { + _dragGestureController?.navigator.didStopUserGesture(); } - _downGestureController = null; + _dragGestureController = null; }); } super.dispose(); @@ -699,32 +756,43 @@ class _CupertinoDownGestureDetectorState extends State<_CupertinoDownGestureD void _handleDragStart(DragStartDetails details) { assert(mounted); - assert(_downGestureController == null); - _downGestureController = widget.onStartPopGesture(); + assert(_dragGestureController == null); + _dragGestureController = widget.onStartPopGesture(); } void _handleDragUpdate(DragUpdateDetails details) { assert(mounted); - assert(_downGestureController != null); - _downGestureController!.dragUpdate( - // Divide by size of the sheet. - details.primaryDelta! / (context.size!.height - (context.size!.height * _kTopGapRatio)), - ); + assert(_dragGestureController != null); + if (_stretchDragController == null) { + return; + } + _dragGestureController!.dragUpdate(details.primaryDelta!, _stretchDragController!.controller); } void _handleDragEnd(DragEndDetails details) { assert(mounted); - assert(_downGestureController != null); - _downGestureController!.dragEnd(details.velocity.pixelsPerSecond.dy / context.size!.height); - _downGestureController = null; + assert(_dragGestureController != null); + if (_stretchDragController == null) { + _dragGestureController = null; + return; + } + _dragGestureController!.dragEnd( + details.velocity.pixelsPerSecond.dy / context.size!.height, + _stretchDragController!.controller, + ); + _dragGestureController = null; } void _handleDragCancel() { assert(mounted); // This can be called even if start is not called, paired with the "down" event // that we don't consider here. - _downGestureController?.dragEnd(0.0); - _downGestureController = null; + if (_stretchDragController == null) { + _dragGestureController = null; + return; + } + _dragGestureController?.dragEnd(0.0, _stretchDragController!.controller); + _dragGestureController = null; } void _handlePointerDown(PointerDownEvent event) { @@ -743,31 +811,52 @@ class _CupertinoDownGestureDetectorState extends State<_CupertinoDownGestureD } } -class _CupertinoDownGestureController { +class _CupertinoDragGestureController { /// Creates a controller for an iOS-style back gesture. - _CupertinoDownGestureController({ + _CupertinoDragGestureController({ required this.navigator, - required this.controller, + required this.popDragController, required this.getIsActive, required this.getIsCurrent, }) { navigator.didStartUserGesture(); } - final AnimationController controller; + final AnimationController popDragController; final NavigatorState navigator; final ValueGetter getIsActive; final ValueGetter getIsCurrent; /// The drag gesture has changed by [delta]. The total range of the drag /// should be 0.0 to 1.0. - void dragUpdate(double delta) { - controller.value -= delta; + void dragUpdate(double delta, AnimationController upController) { + if (popDragController.value == 1.0 && delta < 0) { + // Divide by stretchable range (when dragging upward at max extent). + upController.value -= + delta / (navigator.context.size!.height * (_kTopGapRatio - _kStretchedTopGapRatio)); + } else { + // Divide by size of the sheet. + popDragController.value -= + delta / + (navigator.context.size!.height - (navigator.context.size!.height * _kTopGapRatio)); + } } /// The drag gesture has ended with a vertical motion of [velocity] as a /// fraction of screen height per second. - void dragEnd(double velocity) { + void dragEnd(double velocity, AnimationController upController) { + // If the sheet is in a stretched state (dragged upward beyond max size), + // reverse the stretch to return to the normal max height. + if (upController.value > 0) { + upController.animateBack( + 0.0, + duration: const Duration(milliseconds: 180), + curve: Curves.easeOut, + ); + navigator.didStopUserGesture(); + return; + } + // Fling in the appropriate direction. // // This curve has been determined through rigorously eyeballing native iOS @@ -793,11 +882,11 @@ class _CupertinoDownGestureController { // If the drag is dropped with low velocity, the sheet will pop if the // the drag goes a little past the halfway point on the screen. This is // eyeballed on a simulator running iOS 18.0. - animateForward = controller.value > 0.52; + animateForward = popDragController.value > 0.52; } if (animateForward) { - controller.animateTo( + popDragController.animateTo( 1.0, duration: _kDroppedSheetDragAnimationDuration, curve: animationCurve, @@ -809,8 +898,8 @@ class _CupertinoDownGestureController { rootNavigator.pop(); } - if (controller.isAnimating) { - controller.animateBack( + if (popDragController.isAnimating) { + popDragController.animateBack( 0.0, duration: _kDroppedSheetDragAnimationDuration, curve: animationCurve, @@ -818,17 +907,17 @@ class _CupertinoDownGestureController { } } - if (controller.isAnimating) { + if (popDragController.isAnimating) { // Keep the userGestureInProgress in true state so we don't change the // curve of the page transition mid-flight since CupertinoPageTransition // depends on userGestureInProgress. // late AnimationStatusListener animationStatusCallback; void animationStatusCallback(AnimationStatus status) { navigator.didStopUserGesture(); - controller.removeStatusListener(animationStatusCallback); + popDragController.removeStatusListener(animationStatusCallback); } - controller.addStatusListener(animationStatusCallback); + popDragController.addStatusListener(animationStatusCallback); } else { navigator.didStopUserGesture(); } diff --git a/packages/flutter/test/cupertino/sheet_test.dart b/packages/flutter/test/cupertino/sheet_test.dart index d1fc3cd68939d..081022503a90a 100644 --- a/packages/flutter/test/cupertino/sheet_test.dart +++ b/packages/flutter/test/cupertino/sheet_test.dart @@ -1301,6 +1301,38 @@ void main() { equals(tester.getBottomLeft(find.byType(SnackBar).first).dy), ); }); + + testWidgets('partial upward drag stretches and returns without popping', ( + WidgetTester tester, + ) async { + final GlobalKey homeKey = GlobalKey(); + final GlobalKey sheetKey = GlobalKey(); + + await tester.pumpWidget(dragGestureApp(homeKey, sheetKey)); + + await tester.tap(find.text('Push Page 2')); + await tester.pumpAndSettle(); + + expect(find.text('Page 2'), findsOneWidget); + + RenderBox box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; + final double initialPosition = box.localToGlobal(Offset.zero).dy; + + final TestGesture gesture = await tester.startGesture(const Offset(100, 400)); + await gesture.moveBy(const Offset(0, -100)); + await tester.pump(); + + box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; + final double stretchedPosition = box.localToGlobal(Offset.zero).dy; + expect(stretchedPosition, lessThan(initialPosition)); + + await gesture.up(); + await tester.pumpAndSettle(); + + box = tester.renderObject(find.byKey(sheetKey)) as RenderBox; + final double finalPosition = box.localToGlobal(Offset.zero).dy; + expect(finalPosition, initialPosition); + }); }); testWidgets('CupertinoSheet causes SystemUiOverlayStyle changes', (WidgetTester tester) async { From 9e99953a4e1e91047b4aaadb05738c0836291d66 Mon Sep 17 00:00:00 2001 From: Ahmed Mohamed Sameh Date: Wed, 13 Aug 2025 22:08:49 +0300 Subject: [PATCH 022/720] Make sure that a Chip doesn't crash in 0x0 environment (#173245) This is my attempt to handle https://github.com/flutter/flutter/issues/6537 for the Chip UI control. --- packages/flutter/test/material/chip_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index cd45348540827..9078b94a27f8c 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -6375,6 +6375,20 @@ void main() { }, ); + testWidgets('Chip renders at zero area', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Center( + child: SizedBox.shrink( + child: Scaffold(body: Chip(label: Text('X'))), + ), + ), + ), + ); + final Finder xText = find.text('X'); + expect(tester.getSize(xText).isEmpty, isTrue); + }); + testWidgets('RawChip renders at zero area', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( From f8e8a8dce95f735974b664a37c19ff20165770fe Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 13 Aug 2025 15:08:53 -0400 Subject: [PATCH 023/720] [ Widget Preview ] Add `--machine` mode (#173654) Currently only outputs a single event with information about where the widget preview environment is served from: `[{"event":"widget_preview.started","params":{"url":"http://localhost:61383"}}]` Fixes https://github.com/flutter/flutter/issues/173545 --- packages/flutter_tools/lib/executable.dart | 5 +- .../lib/src/commands/widget_preview.dart | 155 +++++++++++++++++- .../widget_preview_machine_test.dart | 123 ++++++++++++++ 3 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 packages/flutter_tools/test/integration.shard/widget_preview_machine_test.dart diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 76a4aaba2f2cc..8b4e107ee4f26 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -88,10 +88,7 @@ Future main(List args) async { final bool daemon = args.contains('daemon'); final bool runMachine = (args.contains('--machine') && args.contains('run')) || - (args.contains('--machine') && args.contains('attach')) || - // `flutter widget-preview start` starts an application that requires a logger - // to be setup for machine mode. - (args.contains('widget-preview') && args.contains('start')); + (args.contains('--machine') && args.contains('attach')); // Cache.flutterRoot must be set early because other features use it (e.g. // enginePath's initializer uses it). This can only work with the real diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart index 32854a03b03f9..ddf9e948234a0 100644 --- a/packages/flutter_tools/lib/src/commands/widget_preview.dart +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -16,9 +16,11 @@ import '../base/logger.dart'; import '../base/os.dart'; import '../base/platform.dart'; import '../base/process.dart'; +import '../base/terminal.dart'; import '../build_info.dart'; import '../bundle.dart' as bundle; import '../cache.dart'; +import '../convert.dart'; import '../device.dart'; import '../globals.dart' as globals; import '../isolated/resident_web_runner.dart'; @@ -116,7 +118,7 @@ abstract base class WidgetPreviewSubCommandBase extends FlutterCommand { final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with CreateBase { WidgetPreviewStartCommand({ this.verbose = false, - required this.logger, + required Logger logger, required this.fs, required this.projectFactory, required this.cache, @@ -126,11 +128,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C required this.processManager, required this.artifacts, @visibleForTesting WidgetPreviewDtdServices? dtdServicesOverride, - }) { + }) : logger = WidgetPreviewMachineAwareLogger(logger) { if (dtdServicesOverride != null) { _dtdService = dtdServicesOverride; } addPubOptions(); + addMachineOutputFlag(verboseHelp: verbose); argParser ..addFlag( kWebServer, @@ -189,7 +192,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C final FileSystem fs; @override - final Logger logger; + final WidgetPreviewMachineAwareLogger logger; @override final FlutterProjectFactory projectFactory; @@ -255,6 +258,9 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C ? fs.directory(customPreviewScaffoldOutput) : rootProject.widgetPreviewScaffold; + final bool machine = boolArg(FlutterGlobalOptions.kMachineFlag); + logger.machine = machine; + // Check to see if a preview scaffold has already been generated. If not, // generate one. final bool generateScaffoldProject = @@ -444,6 +450,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C ); unawaited(_widgetPreviewApp!.run(appStartedCompleter: appStarted)); await appStarted.future; + logger.sendEvent('started', {'url': flutterDevice.devFS!.baseUri.toString()}); } } on Exception catch (error) { throwToolExit(error.toString()); @@ -491,3 +498,145 @@ final class WidgetPreviewCleanCommand extends WidgetPreviewSubCommandBase { return FlutterCommandResult.success(); } } + +/// A custom logger for the widget-preview commands that disables non-event output to stdio when +/// machine mode is enabled. +final class WidgetPreviewMachineAwareLogger extends DelegatingLogger { + WidgetPreviewMachineAwareLogger(super.delegate); + + var machine = false; + + @override + void printError( + String message, { + StackTrace? stackTrace, + bool? emphasis, + TerminalColor? color, + int? indent, + int? hangingIndent, + bool? wrap, + }) { + if (machine) { + return; + } + super.printError( + message, + stackTrace: stackTrace, + emphasis: emphasis, + color: color, + indent: indent, + hangingIndent: hangingIndent, + wrap: wrap, + ); + } + + @override + void printWarning( + String message, { + bool? emphasis, + TerminalColor? color, + int? indent, + int? hangingIndent, + bool? wrap, + bool fatal = true, + }) { + if (machine) { + return; + } + super.printWarning( + message, + emphasis: emphasis, + color: color, + indent: indent, + hangingIndent: hangingIndent, + wrap: wrap, + fatal: fatal, + ); + } + + @override + void printStatus( + String message, { + bool? emphasis, + TerminalColor? color, + bool? newline, + int? indent, + int? hangingIndent, + bool? wrap, + }) { + if (machine) { + return; + } + super.printStatus( + message, + emphasis: emphasis, + color: color, + newline: newline, + indent: indent, + hangingIndent: hangingIndent, + wrap: wrap, + ); + } + + @override + void printBox(String message, {String? title}) { + if (machine) { + return; + } + super.printBox(message, title: title); + } + + @override + void printTrace(String message) { + if (machine) { + return; + } + super.printTrace(message); + } + + @override + void sendEvent(String name, [Map? args]) { + if (!machine) { + return; + } + super.printStatus( + json.encode([ + {'event': 'widget_preview.$name', 'params': ?args}, + ]), + ); + } + + @override + Status startProgress( + String message, { + String? progressId, + int progressIndicatorPadding = kDefaultStatusPadding, + }) { + if (machine) { + return SilentStatus(stopwatch: Stopwatch()); + } + return super.startProgress( + message, + progressId: progressId, + progressIndicatorPadding: progressIndicatorPadding, + ); + } + + @override + Status startSpinner({ + VoidCallback? onFinish, + Duration? timeout, + SlowWarningCallback? slowWarningCallback, + TerminalColor? warningColor, + }) { + if (machine) { + return SilentStatus(stopwatch: Stopwatch()); + } + return super.startSpinner( + onFinish: onFinish, + timeout: timeout, + slowWarningCallback: slowWarningCallback, + warningColor: warningColor, + ); + } +} diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_machine_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_machine_test.dart new file mode 100644 index 0000000000000..744981c63655a --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/widget_preview_machine_test.dart @@ -0,0 +1,123 @@ +// 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 'package:file/file.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/commands/widget_preview.dart'; +import 'package:flutter_tools/src/widget_preview/dtd_services.dart'; +import 'package:http/http.dart'; +import 'package:process/process.dart'; + +import '../src/common.dart'; +import 'test_data/basic_project.dart'; +import 'test_utils.dart'; + +typedef ExpectedEvent = ({String event, FutureOr Function(Map)? validator}); + +final launchEvents = [ + ( + event: 'widget_preview.started', + validator: (Map params) async { + if (params case {'uri': final String uri}) { + try { + final Response response = await get(Uri.parse(uri)); + expect(response.statusCode, HttpStatus.ok, reason: 'Failed to retrieve widget previewer'); + } catch (e) { + fail('Failed to access widget previewer: $e'); + } + } + }, + ), +]; + +void main() { + late Directory tempDir; + Process? process; + DtdLauncher? dtdLauncher; + final project = BasicProject(); + const ProcessManager processManager = LocalProcessManager(); + + setUp(() async { + tempDir = createResolvedTempDirectorySync('widget_preview_test.'); + await project.setUpIn(tempDir); + }); + + tearDown(() async { + process?.kill(); + process = null; + await dtdLauncher?.dispose(); + dtdLauncher = null; + tryToDelete(tempDir); + }); + + Future runWidgetPreviewMachineMode({ + required List expectedEvents, + bool useWebServer = false, + }) async { + expect(expectedEvents, isNotEmpty); + process = await processManager.start([ + flutterBin, + 'widget-preview', + 'start', + '--machine', + '--${WidgetPreviewStartCommand.kHeadless}', + if (useWebServer) '--${WidgetPreviewStartCommand.kWebServer}', + ], workingDirectory: tempDir.path); + + final completer = Completer(); + var nextExpectationIndex = 0; + process!.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(( + String message, + ) async { + printOnFailure('STDOUT: $message'); + if (completer.isCompleted) { + return; + } + try { + final Object? event = json.decode(message); + if (event case [final Map eventObject]) { + final ExpectedEvent expectation = expectedEvents[nextExpectationIndex]; + if (expectation.event == eventObject['event']) { + await expectation.validator?.call(eventObject); + ++nextExpectationIndex; + } + } + if (nextExpectationIndex == expectedEvents.length) { + completer.complete(); + } + } on FormatException { + // Do nothing. + } + }); + + process!.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) { + printOnFailure('STDERR: $msg'); + }); + + unawaited( + process!.exitCode.then((int exitCode) { + if (completer.isCompleted) { + return; + } + completer.completeError( + TestFailure('The widget previewer exited unexpectedly (exit code: $exitCode)'), + ); + }), + ); + await completer.future; + } + + group('flutter widget-preview start --machine', () { + testWithoutContext('launches in browser', () async { + await runWidgetPreviewMachineMode(expectedEvents: launchEvents); + }); + + testWithoutContext('launches web server', () async { + await runWidgetPreviewMachineMode(expectedEvents: launchEvents, useWebServer: true); + }); + }); +} From 90a892293c166a96acca5743876a96fbc244be8c Mon Sep 17 00:00:00 2001 From: Albin PK <56157868+albinpk@users.noreply.github.com> Date: Thu, 14 Aug 2025 00:42:53 +0530 Subject: [PATCH 024/720] fix: selected date decorator renders outside PageView in `DatePickerDialog` dialog (#171718) Wraps `PageView` with a transparent `Material` to ensure the selected date decorator is painted in the correct context, avoiding visual glitches during transitions. Fixes #171717 ## 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 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. --------- Co-authored-by: Tong Mu Co-authored-by: Huy --- .../src/material/calendar_date_picker.dart | 20 ++++++++++------ .../material/calendar_date_picker_test.dart | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/flutter/lib/src/material/calendar_date_picker.dart b/packages/flutter/lib/src/material/calendar_date_picker.dart index d9d877225a567..137f140890254 100644 --- a/packages/flutter/lib/src/material/calendar_date_picker.dart +++ b/packages/flutter/lib/src/material/calendar_date_picker.dart @@ -21,6 +21,7 @@ import 'icon_button.dart'; import 'icons.dart'; import 'ink_decoration.dart'; import 'ink_well.dart'; +import 'material.dart'; import 'material_localizations.dart'; import 'material_state.dart'; import 'theme.dart'; @@ -888,13 +889,18 @@ class _MonthPickerState extends State<_MonthPicker> { child: _FocusedDate( calendarDelegate: widget.calendarDelegate, date: _dayGridFocus.hasFocus ? _focusedDay : null, - child: PageView.builder( - key: _pageViewKey, - controller: _pageController, - itemBuilder: _buildItems, - itemCount: - widget.calendarDelegate.monthDelta(widget.firstDate, widget.lastDate) + 1, - onPageChanged: _handleMonthPageChanged, + // Wrap the PageView with `Material`, so when its child paints on materials + // the content won't go out of boundary during page transition. + child: Material( + type: MaterialType.transparency, + child: PageView.builder( + key: _pageViewKey, + controller: _pageController, + itemBuilder: _buildItems, + itemCount: + widget.calendarDelegate.monthDelta(widget.firstDate, widget.lastDate) + 1, + onPageChanged: _handleMonthPageChanged, + ), ), ), ), diff --git a/packages/flutter/test/material/calendar_date_picker_test.dart b/packages/flutter/test/material/calendar_date_picker_test.dart index abf2c3334e4db..84f4761da3aeb 100644 --- a/packages/flutter/test/material/calendar_date_picker_test.dart +++ b/packages/flutter/test/material/calendar_date_picker_test.dart @@ -1435,6 +1435,29 @@ void main() { ), ); }); + + testWidgets('Ink feature paints on inner Material', (WidgetTester tester) async { + await tester.pumpWidget( + calendarDatePicker( + firstDate: DateTime(2025, DateTime.june), + initialDate: DateTime(2025, DateTime.july, 20), + lastDate: DateTime(2025, DateTime.august, 31), + ), + ); + + // Material outside the PageView. + final MaterialInkController outerMaterial = Material.of( + tester.element(find.byType(FocusableActionDetector)), + ); + // Material directly wrapping the PageView. + final MaterialInkController innerMaterial = Material.of( + tester.element(find.byType(PageView)), + ); + + // Only the inner Material should have ink features. + expect((outerMaterial as dynamic).debugInkFeatures, isNull); + expect((innerMaterial as dynamic).debugInkFeatures, hasLength(31)); + }); }); group('YearPicker', () { From db1ede369f4057ee240b16f377b988d9158d1b6f Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 13 Aug 2025 15:31:35 -0400 Subject: [PATCH 025/720] Roll Packages from 08a9b2cc46dd to 6cb9113d5fa4 (1 revision) (#173726) https://github.com/flutter/packages/compare/08a9b2cc46dd...6cb9113d5fa4 2025-08-13 koji.wakamiya@gmail.com [go_router_builder] Migrate to Element2 API and update dependencies (flutter/packages#9649) 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 748e1dad0a451..5e06b9179cb6e 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -08a9b2cc46ddb8777a719f745c132b216b2b6121 +6cb9113d5fa49a35774e4582898fae3ea4aec958 From 446adb1acc5f07d065954bc6028a8dd2390cd68f Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Wed, 13 Aug 2025 13:19:16 -0700 Subject: [PATCH 026/720] Do not include `:unittests` unless `enable_unittests` (#173729) Closes https://github.com/flutter/flutter/issues/173728. This will need to be cherrypicked into both 3.35 and 3.36. --- engine/src/flutter/BUILD.gn | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/src/flutter/BUILD.gn b/engine/src/flutter/BUILD.gn index 1258d615fd6a4..f5ace7c353167 100644 --- a/engine/src/flutter/BUILD.gn +++ b/engine/src/flutter/BUILD.gn @@ -78,10 +78,12 @@ group("flutter") { if (!is_qnx) { public_deps = [ - ":unittests", "//flutter/shell/platform/embedder:flutter_engine", "//flutter/sky", ] + if (enable_unittests) { + public_deps += [ ":unittests" ] + } } # Ensure the example for a sample embedder compiles. From d9daafded597c7d1d8790ba2013a30d195cf3881 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 13 Aug 2025 16:37:43 -0400 Subject: [PATCH 027/720] Roll Skia from f7fdda3cd0e6 to 525e2bf80559 (7 revisions) (#173727) https://skia.googlesource.com/skia.git/+log/f7fdda3cd0e6..525e2bf80559 2025-08-13 nicolettep@google.com [graphite] Add SPIR-V validation to SPIR-V transformer 2025-08-13 kjlubick@google.com Update fuzzer documentation to include new bucket 2025-08-13 robertphillips@google.com Rehabilitate some Mac15 jobs 2025-08-13 mike@reedtribe.org use pathbuilder for dash culling 2025-08-13 kjlubick@google.com Add notes about oss-fuzz and status 2025-08-13 kjlubick@google.com Reland "Fix bazel release build to not compile debug code" 2025-08-13 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 1ecf22990490 to c3c9f7778507 (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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 b8db7732e4676..77d8607a51497 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': 'f7fdda3cd0e60ae063fdc33a33175e57a87bf23c', + 'skia_revision': '525e2bf80559a8babfc020ba61050523fe943812', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 1c0ee96c8fd724caebb8ce7418ff465b9a5ab34b Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Wed, 13 Aug 2025 17:03:14 -0400 Subject: [PATCH 028/720] [ Tool ] Fix missing import for widget_preview.dart (#173731) The import was removed in a PR that landed after all presubmits passed. --- packages/flutter_tools/lib/src/commands/widget_preview.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart index ddf9e948234a0..b0ac42df70fad 100644 --- a/packages/flutter_tools/lib/src/commands/widget_preview.dart +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -27,6 +27,7 @@ import '../isolated/resident_web_runner.dart'; import '../project.dart'; import '../resident_runner.dart'; import '../runner/flutter_command.dart'; +import '../runner/flutter_command_runner.dart'; import '../web/web_device.dart'; import '../widget_preview/analytics.dart'; import '../widget_preview/dependency_graph.dart'; From 72e1bf1a15b59bf046795a62c20d13ffd080ffed Mon Sep 17 00:00:00 2001 From: Ahmed Mohamed Sameh Date: Thu, 14 Aug 2025 02:53:03 +0300 Subject: [PATCH 029/720] Make sure that a ChoiceChip doesn't crash in 0x0 environment (#173322) This is my attempt to handle https://github.com/flutter/flutter/issues/6537 for the ChoiceChip UI control. --- .../flutter/test/material/choice_chip_test.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/flutter/test/material/choice_chip_test.dart b/packages/flutter/test/material/choice_chip_test.dart index 85b0dbd630027..052acddbad4e4 100644 --- a/packages/flutter/test/material/choice_chip_test.dart +++ b/packages/flutter/test/material/choice_chip_test.dart @@ -848,4 +848,18 @@ void main() { SystemMouseCursors.forbidden, ); }); + + testWidgets('ChoiceChip renders at zero area', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Center( + child: SizedBox.shrink( + child: Scaffold(body: ChoiceChip(label: Text('X'), selected: true)), + ), + ), + ), + ); + final Finder xText = find.text('X'); + expect(tester.getSize(xText).isEmpty, isTrue); + }); } From f83d8cfd3a93aec687a162dda69505ff54479342 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Wed, 13 Aug 2025 19:53:05 -0400 Subject: [PATCH 030/720] [web] Popping a nameless route should preserve the correct route name (#173652) Fixes https://github.com/flutter/flutter/issues/173356 --- .../lib/src/engine/navigation/history.dart | 41 ++++++++++--------- .../web_ui/lib/src/engine/test_embedding.dart | 4 +- .../lib/web_ui/test/engine/history_test.dart | 37 +++++++++++++++-- 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/history.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/history.dart index 99d44c8a2fb42..b5c8f35331285 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/history.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/navigation/history.dart @@ -6,7 +6,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 '../dom.dart'; import '../platform_dispatcher.dart'; import '../services/message_codec.dart'; import '../services/message_codecs.dart'; @@ -257,20 +256,30 @@ class SingleEntryBrowserHistory extends BrowserHistory { _setupStrategy(strategy); - final String path = currentPath; - if (!_isFlutterEntry(domWindow.history.state)) { + _currentRouteName = currentPath; + if (!_isFlutterEntry(currentState)) { // An entry may not have come from Flutter, for example, when the user // refreshes the page. They land directly on the "flutter" entry, so // there's no need to set up the "origin" and "flutter" entries, we can // safely assume they are already set up. _setupOriginEntry(strategy); - _setupFlutterEntry(strategy, path: path); + _setupFlutterEntry(strategy); } } @override final ui_web.UrlStrategy? urlStrategy; + /// The route name of the current page. + /// + /// This is updated whenever the framework calls `setRouteName`. This is then + /// used when the user hits the back button to pop a nameless route, to restore + /// the route name from before the nameless route was pushed. + /// + /// This is also used to track the user-provided url when they change it + /// directly in the address bar. + String _currentRouteName = '/'; + static const MethodCall _popRouteMethodCall = MethodCall('popRoute'); static const String _kFlutterTag = 'flutter'; static const String _kOriginTag = 'origin'; @@ -303,11 +312,11 @@ class SingleEntryBrowserHistory extends BrowserHistory { @override void setRouteName(String? routeName, {Object? state, bool replace = false}) { if (urlStrategy != null) { - _setupFlutterEntry(urlStrategy!, replace: true, path: routeName); + _currentRouteName = routeName ?? currentPath; + _setupFlutterEntry(urlStrategy!, replace: true); } } - String? _userProvidedRouteName; @override void onPopState(Object? state) { if (_isOriginEntry(state)) { @@ -323,17 +332,13 @@ class SingleEntryBrowserHistory extends BrowserHistory { // We get into this scenario when the user changes the url manually. It // causes a new entry to be pushed on top of our "flutter" one. When this // happens it first goes to the "else" section below where we capture the - // path into `_userProvidedRouteName` then trigger a history back which + // path into `_currentRouteName` then trigger a history back which // brings us here. - assert(_userProvidedRouteName != null); - - final String newRouteName = _userProvidedRouteName!; - _userProvidedRouteName = null; // Send a 'pushRoute' platform message so the app handles it accordingly. EnginePlatformDispatcher.instance.invokeOnPlatformMessage( 'flutter/navigation', - const JSONMethodCodec().encodeMethodCall(MethodCall('pushRoute', newRouteName)), + const JSONMethodCodec().encodeMethodCall(MethodCall('pushRoute', _currentRouteName)), (_) {}, ); } else { @@ -342,7 +347,7 @@ class SingleEntryBrowserHistory extends BrowserHistory { // example. // 1. We first capture the user's desired path. - _userProvidedRouteName = currentPath; + _currentRouteName = currentPath; // 2. Then we remove the new entry. // This will take us back to our "flutter" entry and it causes a new @@ -360,13 +365,9 @@ class SingleEntryBrowserHistory extends BrowserHistory { /// This method is used manipulate the Flutter Entry which is always the /// active entry while the Flutter app is running. - void _setupFlutterEntry(ui_web.UrlStrategy strategy, {bool replace = false, String? path}) { - path ??= currentPath; - if (replace) { - strategy.replaceState(_flutterState, 'flutter', path); - } else { - strategy.pushState(_flutterState, 'flutter', path); - } + void _setupFlutterEntry(ui_web.UrlStrategy strategy, {bool replace = false}) { + final updateState = replace ? strategy.replaceState : strategy.pushState; + updateState(_flutterState, 'flutter', _currentRouteName); } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/test_embedding.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/test_embedding.dart index b33cc96cac3bd..cc71134d7207d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/test_embedding.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/test_embedding.dart @@ -15,7 +15,7 @@ const bool _debugLogHistoryActions = false; class TestHistoryEntry { const TestHistoryEntry(this.state, this.title, this.url); - final dynamic state; + final Object? state; final String? title; final String url; @@ -45,7 +45,7 @@ class TestUrlStrategy implements ui_web.UrlStrategy { String getPath() => currentEntry.url; @override - dynamic getState() => currentEntry.state; + Object? getState() => currentEntry.state; int _currentEntryIndex; int get currentEntryIndex => _currentEntryIndex; diff --git a/engine/src/flutter/lib/web_ui/test/engine/history_test.dart b/engine/src/flutter/lib/web_ui/test/engine/history_test.dart index ba412939542e5..40af749fecce8 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/history_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/history_test.dart @@ -203,6 +203,8 @@ void testMain() { expect(spy.messages[0].channel, 'flutter/navigation'); expect(spy.messages[0].methodName, 'popRoute'); expect(spy.messages[0].methodArguments, isNull); + // The framework responds by updating to the most current route name. + await routeUpdated('/home'); // We still have 2 entries. expect(strategy.history, hasLength(2)); expect(strategy.currentEntryIndex, 1); @@ -293,7 +295,7 @@ void testMain() { expect(spy.messages[0].methodName, 'pushRoute'); expect(spy.messages[0].methodArguments, '/page3'); spy.messages.clear(); - // 2. The framework sends a `routePushed` platform message. + // 2. The framework sends a `routeUpdated` platform message. await routeUpdated('/page3'); // 3. The history state should reflect that /page3 is currently active. expect(strategy.history, hasLength(3)); @@ -309,9 +311,9 @@ void testMain() { expect(spy.messages[0].methodName, 'popRoute'); expect(spy.messages[0].methodArguments, isNull); spy.messages.clear(); - // 2. The framework sends a `routePopped` platform message. + // 2. The framework sends a `routeUpdated` platform message. await routeUpdated('/home'); - // 3. The history state should reflect that /page1 is currently active. + // 3. The history state should reflect that /home is currently active. expect(strategy.history, hasLength(2)); expect(strategy.currentEntryIndex, 1); expect(strategy.currentEntry.state, flutterState); @@ -341,6 +343,35 @@ void testMain() { expect(strategy.currentEntry.state, flutterState); expect(strategy.currentEntry.url, '/home'); }); + + test('popping a nameless route does not change url', () async { + final TestUrlStrategy strategy = TestUrlStrategy.fromEntry( + const TestHistoryEntry(null, null, '/home'), + ); + await implicitView.debugInitializeHistory(strategy, useSingle: true); + + // Go to a named route. + await routeUpdated('/named-route'); + expect(strategy.currentEntry.url, '/named-route'); + + // Now, push a nameless route. The url shouldn't change. + // In a real app, this would be `Navigator.push(context, ...)`; + // Here, we simulate it by NOT calling `routeUpdated`. + + // Click back to pop the nameless route. + await strategy.go(-1); + + // A `popRoute` message should have been sent to the framework. + expect(spy.messages, hasLength(1)); + expect(spy.messages[0].channel, 'flutter/navigation'); + expect(spy.messages[0].methodName, 'popRoute'); + + // Because the popped route was nameless, the framework doesn't send any updated route + // information. + + // The url from before the nameless route should've been preserved. + expect(strategy.currentEntry.url, '/named-route'); + }); }); group('$MultiEntriesBrowserHistory', () { From bfee6183ca26fdb542480319fefb6ed65c88dc43 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 13 Aug 2025 19:58:04 -0400 Subject: [PATCH 031/720] Roll Skia from 525e2bf80559 to 5852eddfd404 (2 revisions) (#173740) https://skia.googlesource.com/skia.git/+log/525e2bf80559..5852eddfd404 2025-08-13 bungeman@google.com Fix Android NDK FontMgr alias scanning 2025-08-13 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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 77d8607a51497..7829c9f4ab708 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '525e2bf80559a8babfc020ba61050523fe943812', + 'skia_revision': '5852eddfd404123022571f6fd3e7b9ec491316ec', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From e1eb1a67dd9c543ff4dd015ba64395543f74bb76 Mon Sep 17 00:00:00 2001 From: Azat Chorekliyev Date: Thu, 14 Aug 2025 03:10:11 +0300 Subject: [PATCH 032/720] Allow empty initial time when using text input mode in showTimePicker dialog (#172847) Added ability to allow empty initial time when using text input mode in showTimePicker dialog https://github.com/flutter/flutter/issues/169131 - [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 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: Tong Mu --- .../flutter/lib/src/material/time_picker.dart | 41 ++++++++- .../test/material/time_picker_test.dart | 88 +++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/time_picker.dart b/packages/flutter/lib/src/material/time_picker.dart index 2ff88aece6144..a0c3096c05322 100644 --- a/packages/flutter/lib/src/material/time_picker.dart +++ b/packages/flutter/lib/src/material/time_picker.dart @@ -1636,6 +1636,7 @@ class _TimePickerInput extends StatefulWidget { required this.helpText, required this.autofocusHour, required this.autofocusMinute, + required this.emptyInitialTime, this.restorationId, }); @@ -1666,6 +1667,13 @@ class _TimePickerInput extends StatefulWidget { /// from the surrounding [RestorationScope] using the provided restoration ID. final String? restorationId; + /// If true and [TimePickerEntryMode.input] is used, hour and minute fields + /// start empty instead of using [initialSelectedTime]. + /// + /// Useful when users prefer manual input without clearing pre-filled values. + /// Ignored in dial mode. + final bool emptyInitialTime; + @override _TimePickerInputState createState() => _TimePickerInputState(); } @@ -1851,6 +1859,7 @@ class _TimePickerInputState extends State<_TimePickerInput> with RestorationMixi onSavedSubmitted: _handleHourSavedSubmitted, onChanged: _handleHourChanged, hourLabelText: widget.hourLabelText, + emptyInitialTime: widget.emptyInitialTime, ), ), if (!hourHasError.value && !minuteHasError.value) @@ -1885,6 +1894,7 @@ class _TimePickerInputState extends State<_TimePickerInput> with RestorationMixi validator: _validateMinute, onSavedSubmitted: _handleMinuteSavedSubmitted, minuteLabelText: widget.minuteLabelText, + emptyInitialTime: widget.emptyInitialTime, ), ), if (!hourHasError.value && !minuteHasError.value) @@ -1935,6 +1945,7 @@ class _HourTextField extends StatelessWidget { required this.onSavedSubmitted, required this.onChanged, required this.hourLabelText, + required this.emptyInitialTime, this.restorationId, }); @@ -1947,6 +1958,7 @@ class _HourTextField extends StatelessWidget { final ValueChanged onChanged; final String? hourLabelText; final String? restorationId; + final bool emptyInitialTime; @override Widget build(BuildContext context) { @@ -1960,6 +1972,7 @@ class _HourTextField extends StatelessWidget { semanticHintText: hourLabelText ?? MaterialLocalizations.of(context).timePickerHourLabel, validator: validator, onSavedSubmitted: onSavedSubmitted, + emptyInitialTime: emptyInitialTime, onChanged: onChanged, ); } @@ -1974,6 +1987,7 @@ class _MinuteTextField extends StatelessWidget { required this.validator, required this.onSavedSubmitted, required this.minuteLabelText, + required this.emptyInitialTime, this.restorationId, }); @@ -1985,6 +1999,7 @@ class _MinuteTextField extends StatelessWidget { final ValueChanged onSavedSubmitted; final String? minuteLabelText; final String? restorationId; + final bool emptyInitialTime; @override Widget build(BuildContext context) { @@ -1997,6 +2012,7 @@ class _MinuteTextField extends StatelessWidget { style: style, semanticHintText: minuteLabelText ?? MaterialLocalizations.of(context).timePickerMinuteLabel, validator: validator, + emptyInitialTime: emptyInitialTime, onSavedSubmitted: onSavedSubmitted, ); } @@ -2013,6 +2029,7 @@ class _HourMinuteTextField extends StatefulWidget { required this.validator, required this.onSavedSubmitted, this.restorationId, + required this.emptyInitialTime, this.onChanged, }); @@ -2026,6 +2043,7 @@ class _HourMinuteTextField extends StatefulWidget { final ValueChanged onSavedSubmitted; final ValueChanged? onChanged; final String? restorationId; + final bool emptyInitialTime; @override _HourMinuteTextFieldState createState() => _HourMinuteTextFieldState(); @@ -2058,9 +2076,12 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> with Restora super.didChangeDependencies(); // Only set the text value if it has not been populated with a localized // version yet. + // If emptyInitialTime is true, set it to an empty string to indicate no + // initial time. if (!controllerHasBeenSet.value) { controllerHasBeenSet.value = true; - controller.value.value = TextEditingValue(text: _formattedValue); + final String initialTextValue = widget.emptyInitialTime ? '' : _formattedValue; + controller.value.value = TextEditingValue(text: initialTextValue); } } @@ -2113,7 +2134,8 @@ class _HourMinuteTextFieldState extends State<_HourMinuteTextField> with Restora ).applyDefaults(inputDecorationTheme); // Remove the hint text when focused because the centered cursor // appears odd above the hint text. - final String? hintText = focusNode.hasFocus ? null : _formattedValue; + // Clear the hint text when emptyInitialTime is true. + final String? hintText = focusNode.hasFocus || widget.emptyInitialTime ? null : _formattedValue; // Because the fill color is specified in both the inputDecorationTheme and // the TimePickerTheme, if there's one in the user's input decoration theme, @@ -2213,6 +2235,7 @@ class TimePickerDialog extends StatefulWidget { this.onEntryModeChanged, this.switchToInputEntryModeIcon, this.switchToTimerEntryModeIcon, + this.emptyInitialInput = false, }); /// The time initially selected when the dialog is shown. @@ -2279,6 +2302,12 @@ class TimePickerDialog extends StatefulWidget { /// {@macro flutter.material.time_picker.switchToTimerEntryModeIcon} final Icon? switchToTimerEntryModeIcon; + /// If true and entry mode is [TimePickerEntryMode.input], the hour and minute + /// fields will be empty on start instead of pre-filled with [initialTime]. + /// + /// Has no effect in dial mode. + final bool emptyInitialInput; + @override State createState() => _TimePickerDialogState(); } @@ -2617,6 +2646,7 @@ class _TimePickerDialogState extends State with RestorationMix onEntryModeChanged: _handleEntryModeChanged, switchToInputEntryModeIcon: widget.switchToInputEntryModeIcon, switchToTimerEntryModeIcon: widget.switchToTimerEntryModeIcon, + emptyInitialInput: widget.emptyInitialInput, ), ); if (_entryMode.value != TimePickerEntryMode.input && @@ -2661,6 +2691,7 @@ class _TimePicker extends StatefulWidget { this.onEntryModeChanged, this.switchToInputEntryModeIcon, this.switchToTimerEntryModeIcon, + required this.emptyInitialInput, }); /// Optionally provide your own text for the help text at the top of the @@ -2735,6 +2766,9 @@ class _TimePicker extends StatefulWidget { /// {@macro flutter.material.time_picker.switchToTimerEntryModeIcon} final Icon? switchToTimerEntryModeIcon; + /// If true, input fields start empty in input mode. + final bool emptyInitialInput; + @override State<_TimePicker> createState() => _TimePickerState(); } @@ -2996,6 +3030,7 @@ class _TimePickerState extends State<_TimePicker> with RestorationMixin { autofocusHour: _autofocusHour.value, autofocusMinute: _autofocusMinute.value, restorationId: 'time_picker_input', + emptyInitialTime: widget.emptyInitialInput, ), ], ); @@ -3148,6 +3183,7 @@ Future showTimePicker({ Orientation? orientation, Icon? switchToInputEntryModeIcon, Icon? switchToTimerEntryModeIcon, + bool emptyInitialInput = false, }) async { assert(debugCheckHasMaterialLocalizations(context)); @@ -3164,6 +3200,7 @@ Future showTimePicker({ onEntryModeChanged: onEntryModeChanged, switchToInputEntryModeIcon: switchToInputEntryModeIcon, switchToTimerEntryModeIcon: switchToTimerEntryModeIcon, + emptyInitialInput: emptyInitialInput, ); return showDialog( context: context, diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart index 54c7afda964a7..ae2cbb5189341 100644 --- a/packages/flutter/test/material/time_picker_test.dart +++ b/packages/flutter/test/material/time_picker_test.dart @@ -2273,6 +2273,89 @@ void main() { expect(result, equals(const TimeOfDay(hour: 6, minute: 45))); }); }); + + group('Time picker - emptyInitialInput (${materialType.name})', () { + testWidgets('Fields are empty and show correct hints when emptyInitialInput is true', ( + WidgetTester tester, + ) async { + await startPicker( + tester, + (_) {}, + entryMode: TimePickerEntryMode.input, + materialType: materialType, + emptyInitialInput: true, + ); + await tester.pump(); + + final List textFields = tester + .widgetList(find.byType(TextField)) + .toList(); + + expect(textFields[0].controller?.text, isEmpty); // hour + expect(textFields[1].controller?.text, isEmpty); // minute + expect(textFields[0].decoration?.hintText, isNull); + expect(textFields[1].decoration?.hintText, isNull); + await finishPicker(tester); + }); + + testWidgets('User sets hour/minute after initially empty fields', ( + WidgetTester tester, + ) async { + late TimeOfDay result; + await startPicker( + tester, + (TimeOfDay? time) { + result = time!; + }, + entryMode: TimePickerEntryMode.input, + materialType: materialType, + emptyInitialInput: true, + ); + + final List textFields = tester + .widgetList(find.byType(TextField)) + .toList(); + + expect(textFields[0].controller?.text, isEmpty); // hour + expect(textFields[1].controller?.text, isEmpty); // minute + expect(textFields[0].decoration?.hintText, isNull); + expect(textFields[1].decoration?.hintText, isNull); + + await tester.enterText(find.byType(TextField).first, '11'); + await tester.enterText(find.byType(TextField).last, '30'); + await finishPicker(tester); + + expect(result, equals(const TimeOfDay(hour: 11, minute: 30))); + }); + + testWidgets('User overrides default values when emptyInitialInput is false', ( + WidgetTester tester, + ) async { + late TimeOfDay result; + await startPicker( + tester, + (TimeOfDay? time) { + result = time!; + }, + entryMode: TimePickerEntryMode.input, + materialType: materialType, + ); + + final List textFields = tester + .widgetList(find.byType(TextField)) + .toList(); + + expect(textFields[0].controller?.text, '7'); // hour + expect(textFields[1].controller?.text, '00'); // minute + + await tester.enterText(find.byType(TextField).first, '8'); + await tester.enterText(find.byType(TextField).last, '15'); + await tester.pump(); + await finishPicker(tester); + + expect(result, equals(const TimeOfDay(hour: 8, minute: 15))); + }); + }); } testWidgets('Material3 - Time selector separator default text style', ( @@ -2569,6 +2652,7 @@ class _TimePickerLauncher extends StatefulWidget { this.restorationId, this.cancelText, this.confirmText, + required this.emptyInitialInput, }); final ValueChanged onChanged; @@ -2576,6 +2660,7 @@ class _TimePickerLauncher extends StatefulWidget { final String? restorationId; final String? cancelText; final String? confirmText; + final bool emptyInitialInput; @override _TimePickerLauncherState createState() => _TimePickerLauncherState(); @@ -2653,6 +2738,7 @@ class _TimePickerLauncherState extends State<_TimePickerLauncher> with Restorati context: context, initialTime: const TimeOfDay(hour: 7, minute: 0), initialEntryMode: widget.entryMode, + emptyInitialInput: widget.emptyInitialInput, ), ); } else { @@ -2682,6 +2768,7 @@ Future startPicker( MaterialType? materialType, String? cancelText, String? confirmText, + bool emptyInitialInput = false, }) async { await tester.pumpWidget( MaterialApp( @@ -2694,6 +2781,7 @@ Future startPicker( restorationId: restorationId, cancelText: cancelText, confirmText: confirmText, + emptyInitialInput: emptyInitialInput, ), ), ); From d9c31fbb1da6c1e3d4050a8e0fbb037c96af67a3 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 13 Aug 2025 23:54:31 -0400 Subject: [PATCH 033/720] Roll Skia from 5852eddfd404 to b3e86773dae1 (1 revision) (#173750) https://skia.googlesource.com/skia.git/+log/5852eddfd404..b3e86773dae1 2025-08-13 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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 7829c9f4ab708..29e7ca0020dd1 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '5852eddfd404123022571f6fd3e7b9ec491316ec', + 'skia_revision': 'b3e86773dae140037b98fea678a0b9606e3828f4', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 9b219ec4459e66852b32c3191f2fdbfb0341bdb4 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 14 Aug 2025 02:35:10 -0400 Subject: [PATCH 034/720] Roll Dart SDK from 73153bdc1459 to 9b4691f35139 (3 revisions) (#173755) https://dart.googlesource.com/sdk.git/+log/73153bdc1459..9b4691f35139 2025-08-14 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-96.0.dev 2025-08-14 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-95.0.dev 2025-08-13 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-94.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 chinmaygarde@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 29e7ca0020dd1..b3f318e0f2cb9 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '73153bdc1459d34da816c13ece0c6812b97caf92', + 'dart_revision': '9b4691f351395372a8964674559d0235616ca38e', # 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 c932faf754723287ebf8fc64bbb72a85046f6afb Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 14 Aug 2025 05:43:29 -0400 Subject: [PATCH 035/720] Roll Skia from b3e86773dae1 to dca5f05fee87 (4 revisions) (#173763) https://skia.googlesource.com/skia.git/+log/b3e86773dae1..dca5f05fee87 2025-08-14 skia-autoroll@skia-public.iam.gserviceaccount.com Roll ANGLE from cfeea900811c to 899f3505748e (2 revisions) 2025-08-14 skia-autoroll@skia-public.iam.gserviceaccount.com Roll SwiftShader from 98d09f8e88db to fed7f25ca957 (1 revision) 2025-08-14 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Dawn from 180eb3f989b3 to e07d4f333e72 (20 revisions) 2025-08-14 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from c3c9f7778507 to 1d9ad72b24bd (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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 b3f318e0f2cb9..5ced9d394f802 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': 'b3e86773dae140037b98fea678a0b9606e3828f4', + 'skia_revision': 'dca5f05fee87961bd96a83ead6549838b725660d', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 389172f211222b8c24d0f61b051c0212c7026221 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 14 Aug 2025 10:38:52 -0400 Subject: [PATCH 036/720] Removing all of the tests and updating the API to no longer rely on the ffi package --- .../lib/src/widgets/_window_win32.dart | 370 ++++++++--- packages/flutter/pubspec.yaml | 3 +- .../test/widgets/window_win32_test.dart | 626 ------------------ 3 files changed, 275 insertions(+), 724 deletions(-) delete mode 100644 packages/flutter/test/widgets/window_win32_test.dart diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index 89c059280571f..bbed872f60106 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -14,10 +14,9 @@ // // See: https://github.com/flutter/flutter/issues/30701. -import 'dart:ffi' hide Size; +import 'dart:ffi' as ffi; import 'dart:io'; import 'dart:ui' show Display, FlutterView; -import 'package:ffi/ffi.dart' as ffi; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -28,7 +27,7 @@ import 'binding.dart'; /// /// {@macro flutter.widgets.windowing.experimental} @internal -typedef HWND = Pointer; +typedef HWND = ffi.Pointer; const int _WM_SIZE = 0x0005; const int _WM_ACTIVATE = 0x0006; @@ -94,17 +93,19 @@ class WindowingOwnerWin32 extends WindowingOwner { @internal WindowingOwnerWin32() : win32PlatformInterface = _NativeWin32PlatformInterface(), - platformDispatcher = PlatformDispatcher.instance { + platformDispatcher = PlatformDispatcher.instance, + allocator = _CallocAllocator._() { if (!Platform.isWindows) { throw UnsupportedError('Only available on the Win32 platform'); } - final Pointer request = ffi.calloc() - ..ref.onMessage = NativeCallable)>.isolateLocal( - _onMessage, - ).nativeFunction; + final ffi.Pointer request = allocator() + ..ref.onMessage = + ffi.NativeCallable)>.isolateLocal( + _onMessage, + ).nativeFunction; win32PlatformInterface.initialize(platformDispatcher.engineId!, request); - ffi.calloc.free(request); + allocator.free(request); } /// Creates a new [WindowingOwnerWin32] instance for testing purposes. @@ -121,13 +122,15 @@ class WindowingOwnerWin32 extends WindowingOwner { WindowingOwnerWin32.test({ required this.win32PlatformInterface, required this.platformDispatcher, + required this.allocator, }) { - final Pointer request = ffi.calloc() - ..ref.onMessage = NativeCallable)>.isolateLocal( - _onMessage, - ).nativeFunction; + final ffi.Pointer request = allocator() + ..ref.onMessage = + ffi.NativeCallable)>.isolateLocal( + _onMessage, + ).nativeFunction; win32PlatformInterface.initialize(platformDispatcher.engineId!, request); - ffi.calloc.free(request); + allocator.free(request); } final List _messageHandlers = []; @@ -146,6 +149,14 @@ class WindowingOwnerWin32 extends WindowingOwner { @internal final PlatformDispatcher platformDispatcher; + /// The [Allocator] used for allocating native memory in this owner. + /// + /// This can be overridden via the [WindowingOwnerWin32.test] constructor. + /// + /// {@macro flutter.widgets.windowing.experimental} + @internal + final ffi.Allocator allocator; + @internal @override RegularWindowController createRegularWindowController({ @@ -205,7 +216,7 @@ class WindowingOwnerWin32 extends WindowingOwner { _messageHandlers.remove(handler); } - void _onMessage(Pointer message) { + void _onMessage(ffi.Pointer message) { final List handlers = List.from(_messageHandlers); final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( (FlutterView view) => view.viewId == message.ref.viewId, @@ -263,15 +274,15 @@ class RegularWindowControllerWin32 extends RegularWindowController _delegate = delegate, super.empty() { owner.addMessageHandler(this); - final Pointer request = ffi.calloc() + final ffi.Pointer request = owner.allocator() ..ref.preferredSize.from(preferredSize) ..ref.preferredConstraints.from(preferredConstraints) - ..ref.title = (title ?? 'Regular window').toNativeUtf16(); + ..ref.title = (title ?? 'Regular window').toNativeUtf16(allocator: _owner.allocator); final int viewId = _owner.win32PlatformInterface.createWindow( _owner.platformDispatcher.engineId!, request, ); - ffi.calloc.free(request); + owner.allocator.free(request); final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( (FlutterView view) => view.viewId == viewId, ); @@ -302,13 +313,13 @@ class RegularWindowControllerWin32 extends RegularWindowController return ''; } - final Pointer data = ffi.calloc(length + 1); + final ffi.Pointer data = _owner.allocator(length + 1); try { - final Pointer buffer = data.cast(); + final ffi.Pointer<_Utf16> buffer = data.cast<_Utf16>(); _owner.win32PlatformInterface.getWindowText(getWindowHandle(), buffer, length + 1); return buffer.toDartString(); } finally { - ffi.calloc.free(data); + _owner.allocator.free(data); } } @@ -344,22 +355,23 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal void setSize(Size? size) { _ensureNotDestroyed(); - final Pointer request = ffi.calloc(); + final ffi.Pointer request = _owner.allocator(); request.ref.hasSize = size != null; request.ref.width = size?.width ?? 0; request.ref.height = size?.height ?? 0; _owner.win32PlatformInterface.setWindowContentSize(getWindowHandle(), request); - ffi.calloc.free(request); + _owner.allocator.free(request); } @override @internal void setConstraints(BoxConstraints constraints) { _ensureNotDestroyed(); - final Pointer request = ffi.calloc(); + final ffi.Pointer request = _owner + .allocator(); request.ref.from(constraints); _owner.win32PlatformInterface.setWindowConstraints(getWindowHandle(), request); - ffi.calloc.free(request); + _owner.allocator.free(request); notifyListeners(); } @@ -368,9 +380,9 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal void setTitle(String title) { _ensureNotDestroyed(); - final Pointer titlePointer = title.toNativeUtf16(); + final ffi.Pointer<_Utf16> titlePointer = title.toNativeUtf16(allocator: _owner.allocator); _owner.win32PlatformInterface.setWindowTitle(getWindowHandle(), titlePointer); - ffi.calloc.free(titlePointer); + _owner.allocator.free(titlePointer); notifyListeners(); } @@ -407,12 +419,13 @@ class RegularWindowControllerWin32 extends RegularWindowController @override @internal void setFullscreen(bool fullscreen, {Display? display}) { - final Pointer request = ffi.calloc(); + final ffi.Pointer request = _owner + .allocator(); request.ref.hasDisplayId = false; request.ref.displayId = display?.id ?? 0; request.ref.fullscreen = fullscreen; _owner.win32PlatformInterface.setFullscreen(getWindowHandle(), request); - ffi.calloc.free(request); + _owner.allocator.free(request); } /// Returns HWND pointer to the top level window. @@ -465,7 +478,7 @@ class RegularWindowControllerWin32 extends RegularWindowController } } -/// Abstract class that wraps native access to the win32 API via FFI. +/// Abstract class that wraps native access to the win32 API. /// /// Used by [WindowingOwnerWin32]. /// @@ -492,14 +505,14 @@ abstract class Win32PlatformInterface { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal - void initialize(int engineId, Pointer request); + void initialize(int engineId, ffi.Pointer request); /// Create a regular window on the provided engine. /// /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal - int createWindow(int engineId, Pointer request); + int createWindow(int engineId, ffi.Pointer request); /// Retrieve the window handle associated with the provided engine and view ids. /// @@ -527,21 +540,21 @@ abstract class Win32PlatformInterface { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal - void setWindowTitle(HWND windowHandle, Pointer title); + void setWindowTitle(HWND windowHandle, ffi.Pointer<_Utf16> title); /// Set the content size of the window. /// /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal - void setWindowContentSize(HWND windowHandle, Pointer size); + void setWindowContentSize(HWND windowHandle, ffi.Pointer size); /// Set the constraints of the window. /// /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal - void setWindowConstraints(HWND windowHandle, Pointer constraints); + void setWindowConstraints(HWND windowHandle, ffi.Pointer constraints); /// Show the window. /// @@ -569,7 +582,7 @@ abstract class Win32PlatformInterface { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal - void setFullscreen(HWND windowHandle, Pointer request); + void setFullscreen(HWND windowHandle, ffi.Pointer request); /// Retrieve the fullscreen status of the window. /// @@ -590,7 +603,7 @@ abstract class Win32PlatformInterface { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal - int getWindowText(HWND windowHandle, Pointer lpString, int maxLength); + int getWindowText(HWND windowHandle, ffi.Pointer<_Utf16> lpString, int maxLength); /// Retrieve the currently focused window handle. /// @@ -607,12 +620,12 @@ class _NativeWin32PlatformInterface extends Win32PlatformInterface { } @override - void initialize(int engineId, Pointer request) { + void initialize(int engineId, ffi.Pointer request) { _initializeWindowing(engineId, request); } @override - int createWindow(int engineId, Pointer request) { + int createWindow(int engineId, ffi.Pointer request) { return _createWindow(engineId, request); } @@ -632,17 +645,17 @@ class _NativeWin32PlatformInterface extends Win32PlatformInterface { } @override - void setWindowTitle(HWND windowHandle, Pointer title) { + void setWindowTitle(HWND windowHandle, ffi.Pointer<_Utf16> title) { _setWindowTitle(windowHandle, title); } @override - void setWindowContentSize(HWND windowHandle, Pointer size) { + void setWindowContentSize(HWND windowHandle, ffi.Pointer size) { _setWindowContentSize(windowHandle, size); } @override - void setWindowConstraints(HWND windowHandle, Pointer constraints) { + void setWindowConstraints(HWND windowHandle, ffi.Pointer constraints) { _setWindowConstraints(windowHandle, constraints); } @@ -662,7 +675,7 @@ class _NativeWin32PlatformInterface extends Win32PlatformInterface { } @override - void setFullscreen(HWND windowHandle, Pointer request) { + void setFullscreen(HWND windowHandle, ffi.Pointer request) { _setFullscreen(windowHandle, request); } @@ -677,7 +690,7 @@ class _NativeWin32PlatformInterface extends Win32PlatformInterface { } @override - int getWindowText(HWND windowHandle, Pointer lpString, int maxLength) { + int getWindowText(HWND windowHandle, ffi.Pointer<_Utf16> lpString, int maxLength) { return _getWindowText(windowHandle, lpString, maxLength); } @@ -686,72 +699,87 @@ class _NativeWin32PlatformInterface extends Win32PlatformInterface { return _getForegroundWindow(); } - @Native(symbol: 'InternalFlutterWindows_WindowManager_HasTopLevelWindows') + @ffi.Native( + symbol: 'InternalFlutterWindows_WindowManager_HasTopLevelWindows', + ) external static bool _hasTopLevelWindows(int engineId); - @Native)>( + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_Initialize', ) - external static void _initializeWindowing(int engineId, Pointer request); + external static void _initializeWindowing( + int engineId, + ffi.Pointer request, + ); - @Native)>( + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_CreateRegularWindow', ) - external static int _createWindow(int engineId, Pointer request); + external static int _createWindow(int engineId, ffi.Pointer request); - @Native( + @ffi.Native( symbol: 'InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle', ) external static HWND _getWindowHandle(int engineId, int viewId); - @Native(symbol: 'DestroyWindow') + @ffi.Native(symbol: 'DestroyWindow') external static void _destroyWindow(HWND windowHandle); - @Native( + @ffi.Native( symbol: 'InternalFlutterWindows_WindowManager_GetWindowContentSize', ) external static ActualContentSize _getWindowContentSize(HWND windowHandle); - @Native)>(symbol: 'SetWindowTextW') - external static void _setWindowTitle(HWND windowHandle, Pointer title); + @ffi.Native)>(symbol: 'SetWindowTextW') + external static void _setWindowTitle(HWND windowHandle, ffi.Pointer<_Utf16> title); - @Native)>( + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetWindowSize', ) - external static void _setWindowContentSize(HWND windowHandle, Pointer size); + external static void _setWindowContentSize( + HWND windowHandle, + ffi.Pointer size, + ); - @Native)>( + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetWindowConstraints', ) external static void _setWindowConstraints( HWND windowHandle, - Pointer constraints, + ffi.Pointer constraints, ); - @Native(symbol: 'ShowWindow') + @ffi.Native(symbol: 'ShowWindow') external static void _showWindow(HWND windowHandle, int command); - @Native(symbol: 'IsIconic') + @ffi.Native(symbol: 'IsIconic') external static int _isIconic(HWND windowHandle); - @Native(symbol: 'IsZoomed') + @ffi.Native(symbol: 'IsZoomed') external static int _isZoomed(HWND windowHandle); - @Native)>( + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetFullscreen', ) - external static void _setFullscreen(HWND windowHandle, Pointer request); + external static void _setFullscreen( + HWND windowHandle, + ffi.Pointer request, + ); - @Native(symbol: 'InternalFlutterWindows_WindowManager_GetFullscreen') + @ffi.Native(symbol: 'InternalFlutterWindows_WindowManager_GetFullscreen') external static bool _getFullscreen(HWND windowHandle); - @Native(symbol: 'GetWindowTextLengthW') + @ffi.Native(symbol: 'GetWindowTextLengthW') external static int _getWindowTextLength(HWND windowHandle); - @Native, Int32)>(symbol: 'GetWindowTextW') - external static int _getWindowText(HWND windowHandle, Pointer lpString, int maxLength); + @ffi.Native, ffi.Int32)>(symbol: 'GetWindowTextW') + external static int _getWindowText( + HWND windowHandle, + ffi.Pointer<_Utf16> lpString, + int maxLength, + ); - @Native(symbol: 'GetForegroundWindow') + @ffi.Native(symbol: 'GetForegroundWindow') external static HWND _getForegroundWindow(); } @@ -760,10 +788,10 @@ class _NativeWin32PlatformInterface extends Win32PlatformInterface { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal -final class WindowCreationRequest extends Struct { +final class WindowCreationRequest extends ffi.Struct { external WindowSizeRequest preferredSize; external WindowConstraintsRequest preferredConstraints; - external Pointer title; + external ffi.Pointer<_Utf16> title; } /// Payload for the initialization request for the windowing subsystem used @@ -772,8 +800,9 @@ final class WindowCreationRequest extends Struct { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal -final class WindowingInitRequest extends Struct { - external Pointer)>> onMessage; +final class WindowingInitRequest extends ffi.Struct { + external ffi.Pointer)>> + onMessage; } /// Payload for the size of a window used by [WindowCreationRequest] and @@ -782,14 +811,14 @@ final class WindowingInitRequest extends Struct { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal -final class WindowSizeRequest extends Struct { - @Bool() +final class WindowSizeRequest extends ffi.Struct { + @ffi.Bool() external bool hasSize; - @Double() + @ffi.Double() external double width; - @Double() + @ffi.Double() external double height; void from(Size? size) { @@ -805,20 +834,20 @@ final class WindowSizeRequest extends Struct { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal -final class WindowConstraintsRequest extends Struct { - @Bool() +final class WindowConstraintsRequest extends ffi.Struct { + @ffi.Bool() external bool hasConstraints; - @Double() + @ffi.Double() external double minWidth; - @Double() + @ffi.Double() external double minHeight; - @Double() + @ffi.Double() external double maxWidth; - @Double() + @ffi.Double() external double maxHeight; void from(BoxConstraints? constraints) { @@ -835,25 +864,25 @@ final class WindowConstraintsRequest extends Struct { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal -final class WindowsMessage extends Struct { - @Int64() +final class WindowsMessage extends ffi.Struct { + @ffi.Int64() external int viewId; external HWND windowHandle; - @Int32() + @ffi.Int32() external int message; - @Int64() + @ffi.Int64() external int wParam; - @Int64() + @ffi.Int64() external int lParam; - @Int64() + @ffi.Int64() external int lResult; - @Bool() + @ffi.Bool() external bool handled; } @@ -863,11 +892,11 @@ final class WindowsMessage extends Struct { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal -final class ActualContentSize extends Struct { - @Double() +final class ActualContentSize extends ffi.Struct { + @ffi.Double() external double width; - @Double() + @ffi.Double() external double height; } @@ -876,13 +905,162 @@ final class ActualContentSize extends Struct { /// {@macro flutter.widgets.windowing.experimental} @visibleForTesting @internal -final class WindowFullscreenRequest extends Struct { - @Bool() +final class WindowFullscreenRequest extends ffi.Struct { + @ffi.Bool() external bool fullscreen; - @Bool() + @ffi.Bool() external bool hasDisplayId; - @Uint64() + @ffi.Uint64() external int displayId; } + +/// The contents of a native zero-terminated array of UTF-16 code units. +/// +/// The Utf16 type itself has no functionality, it's only intended to be used +/// through a `Pointer` representing the entire array. This pointer is +/// the equivalent of a char pointer (`const wchar_t*`) in C code. The +/// individual UTF-16 code units are stored in native byte order. +final class _Utf16 extends ffi.Opaque {} + +/// Extension method for converting a`Pointer` to a [String]. +extension _Utf16Pointer on ffi.Pointer<_Utf16> { + /// The number of UTF-16 code units in this zero-terminated UTF-16 string. + /// + /// The UTF-16 code units of the strings are the non-zero code units up to + /// the first zero code unit. + int get length { + _ensureNotNullptr('length'); + final ffi.Pointer codeUnits = cast(); + return _length(codeUnits); + } + + /// Converts this UTF-16 encoded string to a Dart string. + /// + /// Decodes the UTF-16 code units of this zero-terminated code unit array as + /// Unicode code points and creates a Dart string containing those code + /// points. + /// + /// If [length] is provided, zero-termination is ignored and the result can + /// contain NUL characters. + /// + /// If [length] is not provided, the returned string is the string up til + /// but not including the first NUL character. + String toDartString({int? length}) { + _ensureNotNullptr('toDartString'); + final ffi.Pointer codeUnits = cast(); + if (length == null) { + return _toUnknownLengthString(codeUnits); + } else { + RangeError.checkNotNegative(length, 'length'); + return _toKnownLengthString(codeUnits, length); + } + } + + static String _toKnownLengthString(ffi.Pointer codeUnits, int length) => + String.fromCharCodes(codeUnits.asTypedList(length)); + + static String _toUnknownLengthString(ffi.Pointer codeUnits) { + final StringBuffer buffer = StringBuffer(); + int i = 0; + while (true) { + final int char = (codeUnits + i).value; + if (char == 0) { + return buffer.toString(); + } + buffer.writeCharCode(char); + i++; + } + } + + static int _length(ffi.Pointer codeUnits) { + int length = 0; + while (codeUnits[length] != 0) { + length++; + } + return length; + } + + void _ensureNotNullptr(String operation) { + if (this == ffi.nullptr) { + throw UnsupportedError("Operation '$operation' not allowed on a 'nullptr'."); + } + } +} + +/// Extension method for converting a [String] to a `Pointer`. +extension _StringUtf16Pointer on String { + /// Creates a zero-terminated [Utf16] code-unit array from this String. + /// + /// If this [String] contains NUL characters, converting it back to a string + /// using [Utf16Pointer.toDartString] will truncate the result if a length is + /// not passed. + /// + /// Returns an [allocator]-allocated pointer to the result. + ffi.Pointer<_Utf16> toNativeUtf16({required ffi.Allocator allocator}) { + final units = codeUnits; + final result = allocator(units.length + 1); + final nativeString = result.asTypedList(units.length + 1); + nativeString.setRange(0, units.length, units); + nativeString[units.length] = 0; + return result.cast(); + } +} + +typedef _WinCoTaskMemAllocNative = ffi.Pointer Function(ffi.Size); +typedef _WinCoTaskMemAlloc = ffi.Pointer Function(int); +typedef _WinCoTaskMemFreeNative = ffi.Void Function(ffi.Pointer); +typedef _WinCoTaskMemFree = void Function(ffi.Pointer); + +final class _CallocAllocator implements ffi.Allocator { + _CallocAllocator._() { + _ole32lib = ffi.DynamicLibrary.open('ole32.dll'); + _winCoTaskMemAlloc = _ole32lib.lookupFunction<_WinCoTaskMemAllocNative, _WinCoTaskMemAlloc>( + 'CoTaskMemAlloc', + ); + _winCoTaskMemFreePointer = _ole32lib.lookup('CoTaskMemFree'); + _winCoTaskMemFree = _winCoTaskMemFreePointer.asFunction(); + } + + late final ffi.DynamicLibrary _ole32lib; + late final _WinCoTaskMemAlloc _winCoTaskMemAlloc; + late final ffi.Pointer> _winCoTaskMemFreePointer; + late final _WinCoTaskMemFree _winCoTaskMemFree; + + /// Fills a block of memory with a specified value. + void _fillMemory(ffi.Pointer destination, int length, int fill) { + final ptr = destination.cast(); + for (int i = 0; i < length; i++) { + ptr[i] = fill; + } + } + + /// Fills a block of memory with zeros. + /// + void _zeroMemory(ffi.Pointer destination, int length) => _fillMemory(destination, length, 0); + + /// Allocates [byteCount] bytes of zero-initialized of memory on the native + /// heap. + @override + ffi.Pointer allocate(int byteCount, {int? alignment}) { + ffi.Pointer result; + result = _winCoTaskMemAlloc(byteCount).cast(); + if (result.address == 0) { + throw ArgumentError('Could not allocate $byteCount bytes.'); + } + if (Platform.isWindows) { + _zeroMemory(result, byteCount); + } + return result; + } + + /// Releases memory allocated on the native heap. + @override + void free(ffi.Pointer pointer) { + _winCoTaskMemFree(pointer); + } + + /// Returns a pointer to a native free function. + ffi.Pointer get nativeFree => _winCoTaskMemFreePointer; +} diff --git a/packages/flutter/pubspec.yaml b/packages/flutter/pubspec.yaml index 78e514622505e..c8fdce53648b3 100644 --- a/packages/flutter/pubspec.yaml +++ b/packages/flutter/pubspec.yaml @@ -19,7 +19,6 @@ dependencies: vector_math: 2.2.0 sky_engine: sdk: flutter - ffi: 2.1.4 dev_dependencies: flutter_driver: @@ -41,4 +40,4 @@ dev_dependencies: path: any platform: any -# PUBSPEC CHECKSUM: sdkqgj +# PUBSPEC CHECKSUM: spkhmh diff --git a/packages/flutter/test/widgets/window_win32_test.dart b/packages/flutter/test/widgets/window_win32_test.dart deleted file mode 100644 index ef7d0c7fc7efd..0000000000000 --- a/packages/flutter/test/widgets/window_win32_test.dart +++ /dev/null @@ -1,626 +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 'dart:ffi' hide Size; -import 'dart:io'; -import 'package:ffi/ffi.dart' as ffi; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/src/foundation/_features.dart'; -import 'package:flutter/src/widgets/_window.dart'; - -import 'package:flutter/src/widgets/_window_win32.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('Win32 window test', () { - setUp(() { - isWindowingEnabled = true; - }); - - group('Platform.isWindows is false', () { - setUp(() { - Platform.isWindows = false; - }); - - test('WindowingOwnerWin32 constructor throws when not on windows', () { - expect(() => WindowingOwnerWin32(), throwsUnsupportedError); - }); - }); - - testWidgets('WindowingOwner32 constructor initializes', (WidgetTester tester) async { - bool isInitialized = false; - WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onInitialize: (WindowingInitRequest request) => isInitialized = true, - ), - platformDispatcher: tester.platformDispatcher, - ); - - expect(isInitialized, true); - }); - - testWidgets('WindowingOwner32 can create a regular window', (WidgetTester tester) async { - bool hasCreated = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onCreateWindow: (int engineId, WindowCreationRequest request) { - expect(engineId, tester.platformDispatcher.engineId); - expect(request.preferredSize.hasSize, true); - expect(request.preferredSize.width, 400); - expect(request.preferredSize.height, 300); - - expect(request.preferredConstraints.hasConstraints, true); - expect(request.preferredConstraints.minWidth, 100); - expect(request.preferredConstraints.minHeight, 101); - expect(request.preferredConstraints.maxWidth, 500); - expect(request.preferredConstraints.maxHeight, 501); - hasCreated = true; - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - owner.createRegularWindowController( - preferredSize: const Size(400, 300), - preferredConstraints: const BoxConstraints( - minWidth: 100, - minHeight: 101, - maxWidth: 500, - maxHeight: 501, - ), - delegate: RegularWindowControllerDelegate(), - ); - - expect(hasCreated, true); - }); - - testWidgets('Sending WM_SIZE to WindowingOwner32 notifies listeners', ( - WidgetTester tester, - ) async { - const int WM_SIZE = 0x0005; - late void Function(Pointer) messageFunc; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onInitialize: (WindowingInitRequest request) { - messageFunc = request.onMessage.asFunction(); - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - bool listenerTriggered = false; - controller.addListener(() => listenerTriggered = true); - final Pointer message = ffi.calloc(); - message.ref.viewId = 0; - message.ref.message = WM_SIZE; - messageFunc(message); - - expect(listenerTriggered, true); - }); - - testWidgets('Sending WM_ACTIVATE to WindowingOwner32 notifies listeners', ( - WidgetTester tester, - ) async { - const int WM_ACTIVATE = 0x0006; - late void Function(Pointer) messageFunc; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onInitialize: (WindowingInitRequest request) { - messageFunc = request.onMessage.asFunction(); - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - bool listenerTriggered = false; - controller.addListener(() => listenerTriggered = true); - final Pointer message = ffi.calloc(); - message.ref.viewId = 0; - message.ref.message = WM_ACTIVATE; - messageFunc(message); - - expect(listenerTriggered, true); - }); - - testWidgets('Sending WM_CLOSE message to WindowingOwner32 results in window being destroyed', ( - WidgetTester tester, - ) async { - const int WM_CLOSE = 0x0010; - bool hasDestroyed = false; - late void Function(Pointer) messageFunc; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onInitialize: (WindowingInitRequest request) { - messageFunc = request.onMessage.asFunction(); - }, - onDestroyWindow: (HWND hwnd) { - hasDestroyed = true; - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - owner.createRegularWindowController(delegate: RegularWindowControllerDelegate()); - - final Pointer message = ffi.calloc(); - message.ref.viewId = 0; - message.ref.message = WM_CLOSE; - messageFunc(message); - - expect(hasDestroyed, true); - }); - - testWidgets('WindowingOwner32 can destroy a regular window', (WidgetTester tester) async { - bool hasDestroyed = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onDestroyWindow: (HWND hwnd) { - hasDestroyed = true; - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.destroy(); - expect(hasDestroyed, true); - }); - - testWidgets('WindowingOwner32 can get content size', (WidgetTester tester) async { - bool hasGottenContentSize = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onGetContentSize: (HWND hwnd) { - hasGottenContentSize = true; - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.contentSize; - expect(hasGottenContentSize, true); - }); - - testWidgets('WindowingOwner32 can set title', (WidgetTester tester) async { - bool hasSetTitle = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onSetTitle: (HWND hwnd, Pointer title) { - hasSetTitle = true; - expect(title.toDartString(), 'Hello world'); - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.setTitle('Hello world'); - expect(hasSetTitle, true); - }); - - testWidgets('WindowingOwner32 can set content size', (WidgetTester tester) async { - bool hasSetSize = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onSetContentSize: (HWND hwnd, WindowSizeRequest request) { - hasSetSize = true; - expect(request.hasSize, true); - expect(request.width, 800); - expect(request.height, 600); - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.setSize(const Size(800, 600)); - expect(hasSetSize, true); - }); - - testWidgets('WindowingOwner32 can set constraints', (WidgetTester tester) async { - bool hasSetConstraints = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onSetConstraints: (HWND hwnd, WindowConstraintsRequest request) { - hasSetConstraints = true; - expect(request.hasConstraints, true); - expect(request.minWidth, 100); - expect(request.minHeight, 101); - expect(request.maxWidth, 500); - expect(request.maxHeight, 501); - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.setConstraints( - const BoxConstraints(minWidth: 100, minHeight: 101, maxWidth: 500, maxHeight: 501), - ); - expect(hasSetConstraints, true); - }); - - testWidgets('WindowingOwner32 can activate', (WidgetTester tester) async { - bool hasShown = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onShowWindow: (HWND hwnd, int sw) { - hasShown = true; - expect(sw, 9); // SW_RESTORE - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.activate(); - expect(hasShown, true); - }); - - testWidgets('WindowingOwner32 can maximize', (WidgetTester tester) async { - bool hasMaximized = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onShowWindow: (HWND hwnd, int sw) { - hasMaximized = true; - expect(sw, 3); // SW_MAXIMIZE - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.setMaximized(true); - expect(hasMaximized, true); - }); - - testWidgets('WindowingOwner32 can unmaximize', (WidgetTester tester) async { - bool hasUnmaximized = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onShowWindow: (HWND hwnd, int sw) { - hasUnmaximized = true; - expect(sw, 9); // SW_RESTORE - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.setMaximized(false); - expect(hasUnmaximized, true); - }); - - testWidgets('WindowingOwner32 can minimize', (WidgetTester tester) async { - bool hasMinimized = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onShowWindow: (HWND hwnd, int sw) { - hasMinimized = true; - expect(sw, 6); // SW_MINIMIZE - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.setMinimized(true); - expect(hasMinimized, true); - }); - - testWidgets('WindowingOwner32 can unmaximize', (WidgetTester tester) async { - bool hasUnminimized = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onShowWindow: (HWND hwnd, int sw) { - hasUnminimized = true; - expect(sw, 9); // SW_RESTORE - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.setMinimized(false); - expect(hasUnminimized, true); - }); - - testWidgets('WindowingOwner32 can set fullscreen', (WidgetTester tester) async { - bool hasFullscreen = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onSetFullscreen: (HWND hwnd, WindowFullscreenRequest request) { - hasFullscreen = true; - expect(request.fullscreen, true); - expect(request.hasDisplayId, false); - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.setFullscreen(true); - expect(hasFullscreen, true); - }); - - testWidgets('WindowingOwner32 can get isMinimized', (WidgetTester tester) async { - bool hasCalledIsMinimized = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onIsIconic: (HWND hwnd) { - hasCalledIsMinimized = true; - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.isMinimized; - expect(hasCalledIsMinimized, true); - }); - - testWidgets('WindowingOwner32 can get isMaximized', (WidgetTester tester) async { - bool hasCalledIsMaximized = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onIsZoomed: (HWND hwnd) { - hasCalledIsMaximized = true; - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.isMaximized; - expect(hasCalledIsMaximized, true); - }); - - testWidgets('WindowingOwner32 can get isFullscreen', (WidgetTester tester) async { - bool hasCalledIsFullscreen = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onGetFullscreen: (HWND hwnd) { - hasCalledIsFullscreen = true; - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.isFullscreen; - expect(hasCalledIsFullscreen, true); - }); - - testWidgets('WindowingOwner32 can get title', (WidgetTester tester) async { - bool hasCalledTextLengthGetter = false; - bool hasCalledTextGetter = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onGetWindowTextLength: (HWND hwnd) { - hasCalledTextLengthGetter = true; - }, - onGetWindowText: (HWND hwnd, Pointer title, int length) { - hasCalledTextGetter = true; - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.title; - expect(hasCalledTextLengthGetter, true); - expect(hasCalledTextGetter, true); - }); - - testWidgets('WindowingOwner32 can get isActivated', (WidgetTester tester) async { - bool hasCalledIsActivated = false; - final WindowingOwnerWin32 owner = WindowingOwnerWin32.test( - win32PlatformInterface: _MockWin32PlatformInterface( - onGetForegroundWindow: () { - hasCalledIsActivated = true; - }, - ), - platformDispatcher: tester.platformDispatcher, - ); - - final RegularWindowController controller = owner.createRegularWindowController( - delegate: RegularWindowControllerDelegate(), - ); - - controller.isActivated; - expect(hasCalledIsActivated, true); - }); - }); -} - -class _MockWin32PlatformInterface extends Win32PlatformInterface { - _MockWin32PlatformInterface({ - this.onInitialize, - this.onCreateWindow, - this.onDestroyWindow, - this.onGetContentSize, - this.onSetTitle, - this.onSetContentSize, - this.onSetConstraints, - this.onShowWindow, - this.onIsIconic, - this.onIsZoomed, - this.onSetFullscreen, - this.onGetFullscreen, - this.onGetWindowTextLength, - this.onGetWindowText, - this.onGetForegroundWindow, - }); - - final int viewId = 0; - final HWND hwnd = Pointer.fromAddress(0x8000); - final bool _hasToplevelWindows = true; - Pointer? size; - - final void Function(WindowingInitRequest)? onInitialize; - final void Function(int, WindowCreationRequest)? onCreateWindow; - final void Function(HWND)? onDestroyWindow; - final void Function(HWND)? onGetContentSize; - final void Function(HWND, Pointer)? onSetTitle; - final void Function(HWND, WindowSizeRequest)? onSetContentSize; - final void Function(HWND, WindowConstraintsRequest)? onSetConstraints; - final void Function(HWND, int)? onShowWindow; - final void Function(HWND)? onIsIconic; - final void Function(HWND)? onIsZoomed; - final void Function(HWND, WindowFullscreenRequest)? onSetFullscreen; - final void Function(HWND)? onGetFullscreen; - final void Function(HWND)? onGetWindowTextLength; - final void Function(HWND, Pointer, int)? onGetWindowText; - final VoidCallback? onGetForegroundWindow; - - @override - bool hasTopLevelWindows(int engineId) { - return _hasToplevelWindows; - } - - @override - void initialize(int engineId, Pointer request) { - onInitialize?.call(request.ref); - } - - @override - int createWindow(int engineId, Pointer request) { - onCreateWindow?.call(engineId, request.ref); - return viewId; - } - - @override - HWND getWindowHandle(int engineId, int viewId) { - return hwnd; - } - - @override - void destroyWindow(HWND windowHandle) { - onDestroyWindow?.call(windowHandle); - } - - @override - ActualContentSize getWindowContentSize(HWND windowHandle) { - onGetContentSize?.call(windowHandle); - size = ffi.calloc(); - size!.ref.width = 800; - size!.ref.height = 600; - return size!.ref; - } - - @override - void setWindowTitle(HWND windowHandle, Pointer title) { - onSetTitle?.call(windowHandle, title); - } - - @override - void setWindowContentSize(HWND windowHandle, Pointer size) { - onSetContentSize?.call(windowHandle, size.ref); - } - - @override - void setWindowConstraints(HWND windowHandle, Pointer constraints) { - onSetConstraints?.call(windowHandle, constraints.ref); - } - - @override - void showWindow(HWND windowHandle, int command) { - onShowWindow?.call(windowHandle, command); - } - - @override - int isIconic(HWND windowHandle) { - onIsIconic?.call(windowHandle); - return 0; - } - - @override - int isZoomed(HWND windowHandle) { - onIsZoomed?.call(windowHandle); - return 0; - } - - @override - void setFullscreen(HWND windowHandle, Pointer request) { - onSetFullscreen?.call(windowHandle, request.ref); - } - - @override - bool getFullscreen(HWND windowHandle) { - onGetFullscreen?.call(windowHandle); - return false; - } - - @override - int getWindowTextLength(HWND windowHandle) { - onGetWindowTextLength?.call(windowHandle); - return 10; - } - - @override - int getWindowText(HWND windowHandle, Pointer lpString, int maxLength) { - onGetWindowText?.call(windowHandle, lpString, maxLength); - return 10; - } - - @override - HWND getForegroundWindow() { - onGetForegroundWindow?.call(); - return hwnd; - } -} From e27f0a6943e07b6d3420fb185432705b87101fa8 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 14 Aug 2025 10:53:31 -0400 Subject: [PATCH 037/720] Removing unnecessary interfaces when we're no longer doing a bunch of mock testing --- .../lib/src/widgets/_window_win32.dart | 459 ++++-------------- 1 file changed, 81 insertions(+), 378 deletions(-) diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index bbed872f60106..9c0e81cd55616 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -21,7 +21,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import '_window.dart'; -import 'binding.dart'; /// A Win32 window handle. /// @@ -91,64 +90,22 @@ class WindowingOwnerWin32 extends WindowingOwner { /// /// * [WindowingOwner], the abstract class that manages native windows. @internal - WindowingOwnerWin32() - : win32PlatformInterface = _NativeWin32PlatformInterface(), - platformDispatcher = PlatformDispatcher.instance, - allocator = _CallocAllocator._() { + WindowingOwnerWin32() : allocator = _CallocAllocator._() { if (!Platform.isWindows) { throw UnsupportedError('Only available on the Win32 platform'); } - final ffi.Pointer request = allocator() + final ffi.Pointer<_WindowingInitRequest> request = allocator<_WindowingInitRequest>() ..ref.onMessage = - ffi.NativeCallable)>.isolateLocal( + ffi.NativeCallable)>.isolateLocal( _onMessage, ).nativeFunction; - win32PlatformInterface.initialize(platformDispatcher.engineId!, request); - allocator.free(request); - } - - /// Creates a new [WindowingOwnerWin32] instance for testing purposes. - /// - /// This constructor will not throw when we are not on the win32 platform. - /// - /// This constructor takes a [win32PlatformInterface], which is most likely - /// a mock interface in addition to a custom [platformDispatcher] so that - /// [PlatformDispatcher.engineId] can successfully be mocked. - /// - /// {@macro flutter.widgets.windowing.experimental} - @internal - @visibleForTesting - WindowingOwnerWin32.test({ - required this.win32PlatformInterface, - required this.platformDispatcher, - required this.allocator, - }) { - final ffi.Pointer request = allocator() - ..ref.onMessage = - ffi.NativeCallable)>.isolateLocal( - _onMessage, - ).nativeFunction; - win32PlatformInterface.initialize(platformDispatcher.engineId!, request); + _Win32PlatformInterface.initializeWindowing(PlatformDispatcher.instance.engineId!, request); allocator.free(request); } final List _messageHandlers = []; - /// Provides access to the native win32 backend. - /// - /// {@macro flutter.widgets.windowing.experimental} - @internal - final Win32PlatformInterface win32PlatformInterface; - - /// The [PlatformDispatcher]. - /// - /// This will differ from [PlatformDispatcher.instance] during testing. - /// - /// {@macro flutter.widgets.windowing.experimental} - @internal - final PlatformDispatcher platformDispatcher; - /// The [Allocator] used for allocating native memory in this owner. /// /// This can be overridden via the [WindowingOwnerWin32.test] constructor. @@ -216,9 +173,9 @@ class WindowingOwnerWin32 extends WindowingOwner { _messageHandlers.remove(handler); } - void _onMessage(ffi.Pointer message) { + void _onMessage(ffi.Pointer<_WindowsMessage> message) { final List handlers = List.from(_messageHandlers); - final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( + final FlutterView flutterView = PlatformDispatcher.instance.views.firstWhere( (FlutterView view) => view.viewId == message.ref.viewId, ); for (final WindowsMessageHandler handler in handlers) { @@ -240,7 +197,7 @@ class WindowingOwnerWin32 extends WindowingOwner { @internal @override bool hasTopLevelWindows() { - return win32PlatformInterface.hasTopLevelWindows(platformDispatcher.engineId!); + return _Win32PlatformInterface.hasTopLevelWindows(PlatformDispatcher.instance.engineId!); } } @@ -274,16 +231,16 @@ class RegularWindowControllerWin32 extends RegularWindowController _delegate = delegate, super.empty() { owner.addMessageHandler(this); - final ffi.Pointer request = owner.allocator() + final ffi.Pointer<_WindowCreationRequest> request = owner.allocator<_WindowCreationRequest>() ..ref.preferredSize.from(preferredSize) ..ref.preferredConstraints.from(preferredConstraints) ..ref.title = (title ?? 'Regular window').toNativeUtf16(allocator: _owner.allocator); - final int viewId = _owner.win32PlatformInterface.createWindow( - _owner.platformDispatcher.engineId!, + final int viewId = _Win32PlatformInterface.createWindow( + PlatformDispatcher.instance.engineId!, request, ); owner.allocator.free(request); - final FlutterView flutterView = WidgetsBinding.instance.platformDispatcher.views.firstWhere( + final FlutterView flutterView = PlatformDispatcher.instance.views.firstWhere( (FlutterView view) => view.viewId == viewId, ); rootView = flutterView; @@ -297,9 +254,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal Size get contentSize { _ensureNotDestroyed(); - final ActualContentSize size = _owner.win32PlatformInterface.getWindowContentSize( - getWindowHandle(), - ); + final _ActualContentSize size = _Win32PlatformInterface.getWindowContentSize(getWindowHandle()); final Size result = Size(size.width, size.height); return result; } @@ -308,7 +263,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal String get title { _ensureNotDestroyed(); - final int length = _owner.win32PlatformInterface.getWindowTextLength(getWindowHandle()); + final int length = _Win32PlatformInterface.getWindowTextLength(getWindowHandle()); if (length == 0) { return ''; } @@ -316,7 +271,7 @@ class RegularWindowControllerWin32 extends RegularWindowController final ffi.Pointer data = _owner.allocator(length + 1); try { final ffi.Pointer<_Utf16> buffer = data.cast<_Utf16>(); - _owner.win32PlatformInterface.getWindowText(getWindowHandle(), buffer, length + 1); + _Win32PlatformInterface.getWindowText(getWindowHandle(), buffer, length + 1); return buffer.toDartString(); } finally { _owner.allocator.free(data); @@ -327,39 +282,39 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal bool get isActivated { _ensureNotDestroyed(); - return _owner.win32PlatformInterface.getForegroundWindow() == getWindowHandle(); + return _Win32PlatformInterface.getForegroundWindow() == getWindowHandle(); } @override @internal bool get isMaximized { _ensureNotDestroyed(); - return _owner.win32PlatformInterface.isZoomed(getWindowHandle()) != 0; + return _Win32PlatformInterface.isZoomed(getWindowHandle()) != 0; } @override @internal bool get isMinimized { _ensureNotDestroyed(); - return _owner.win32PlatformInterface.isIconic(getWindowHandle()) != 0; + return _Win32PlatformInterface.isIconic(getWindowHandle()) != 0; } @override @internal bool get isFullscreen { _ensureNotDestroyed(); - return _owner.win32PlatformInterface.getFullscreen(getWindowHandle()); + return _Win32PlatformInterface.getFullscreen(getWindowHandle()); } @override @internal void setSize(Size? size) { _ensureNotDestroyed(); - final ffi.Pointer request = _owner.allocator(); + final ffi.Pointer<_WindowSizeRequest> request = _owner.allocator<_WindowSizeRequest>(); request.ref.hasSize = size != null; request.ref.width = size?.width ?? 0; request.ref.height = size?.height ?? 0; - _owner.win32PlatformInterface.setWindowContentSize(getWindowHandle(), request); + _Win32PlatformInterface.setWindowContentSize(getWindowHandle(), request); _owner.allocator.free(request); } @@ -367,10 +322,10 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal void setConstraints(BoxConstraints constraints) { _ensureNotDestroyed(); - final ffi.Pointer request = _owner - .allocator(); + final ffi.Pointer<_WindowConstraintsRequest> request = _owner + .allocator<_WindowConstraintsRequest>(); request.ref.from(constraints); - _owner.win32PlatformInterface.setWindowConstraints(getWindowHandle(), request); + _Win32PlatformInterface.setWindowConstraints(getWindowHandle(), request); _owner.allocator.free(request); notifyListeners(); @@ -381,7 +336,7 @@ class RegularWindowControllerWin32 extends RegularWindowController void setTitle(String title) { _ensureNotDestroyed(); final ffi.Pointer<_Utf16> titlePointer = title.toNativeUtf16(allocator: _owner.allocator); - _owner.win32PlatformInterface.setWindowTitle(getWindowHandle(), titlePointer); + _Win32PlatformInterface.setWindowTitle(getWindowHandle(), titlePointer); _owner.allocator.free(titlePointer); notifyListeners(); @@ -391,7 +346,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal void activate() { _ensureNotDestroyed(); - _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE); + _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE); } @override @@ -399,9 +354,9 @@ class RegularWindowControllerWin32 extends RegularWindowController void setMaximized(bool maximized) { _ensureNotDestroyed(); if (maximized) { - _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_MAXIMIZE); + _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_MAXIMIZE); } else { - _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE); + _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE); } } @@ -410,21 +365,21 @@ class RegularWindowControllerWin32 extends RegularWindowController void setMinimized(bool minimized) { _ensureNotDestroyed(); if (minimized) { - _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_MINIMIZE); + _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_MINIMIZE); } else { - _owner.win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE); + _Win32PlatformInterface.showWindow(getWindowHandle(), _SW_RESTORE); } } @override @internal void setFullscreen(bool fullscreen, {Display? display}) { - final ffi.Pointer request = _owner - .allocator(); + final ffi.Pointer<_WindowFullscreenRequest> request = _owner + .allocator<_WindowFullscreenRequest>(); request.ref.hasDisplayId = false; request.ref.displayId = display?.id ?? 0; request.ref.fullscreen = fullscreen; - _owner.win32PlatformInterface.setFullscreen(getWindowHandle(), request); + _Win32PlatformInterface.setFullscreen(getWindowHandle(), request); _owner.allocator.free(request); } @@ -432,8 +387,8 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal HWND getWindowHandle() { _ensureNotDestroyed(); - return _owner.win32PlatformInterface.getWindowHandle( - _owner.platformDispatcher.engineId!, + return _Win32PlatformInterface.getWindowHandle( + PlatformDispatcher.instance.engineId!, rootView.viewId, ); } @@ -449,7 +404,7 @@ class RegularWindowControllerWin32 extends RegularWindowController if (_destroyed) { return; } - _owner.win32PlatformInterface.destroyWindow(getWindowHandle()); + _Win32PlatformInterface.destroyWindow(getWindowHandle()); _destroyed = true; _delegate.onWindowDestroyed(); _owner.removeMessageHandler(this); @@ -478,340 +433,104 @@ class RegularWindowControllerWin32 extends RegularWindowController } } -/// Abstract class that wraps native access to the win32 API. -/// -/// Used by [WindowingOwnerWin32]. -/// -/// Overriding this is only useful for testing purposes. -/// -/// {@macro flutter.widgets.windowing.experimental} -/// -/// See also: -/// -/// * [WindowingOwnerWin32], the user of this interface. -@visibleForTesting -@internal -abstract class Win32PlatformInterface { - /// Checks if the engine specified by [engineId] has any top level - /// windows created on it. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - bool hasTopLevelWindows(int engineId); - - /// Initialize the window subsystem for the provided engine. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - void initialize(int engineId, ffi.Pointer request); - - /// Create a regular window on the provided engine. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - int createWindow(int engineId, ffi.Pointer request); - - /// Retrieve the window handle associated with the provided engine and view ids. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - HWND getWindowHandle(int engineId, int viewId); - - /// Destroy a window given its window handle. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - void destroyWindow(HWND windowHandle); - - /// Retrieve the current content size of a window given its handle. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - ActualContentSize getWindowContentSize(HWND windowHandle); - - /// Set the title of a window. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - void setWindowTitle(HWND windowHandle, ffi.Pointer<_Utf16> title); - - /// Set the content size of the window. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - void setWindowContentSize(HWND windowHandle, ffi.Pointer size); - - /// Set the constraints of the window. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - void setWindowConstraints(HWND windowHandle, ffi.Pointer constraints); - - /// Show the window. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - void showWindow(HWND windowHandle, int command); - - /// Check if the window is minimized. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - int isIconic(HWND windowHandle); - - /// Check if the window is maximized. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - int isZoomed(HWND windowHandle); - - /// Request that the window change its fullscreen status. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - void setFullscreen(HWND windowHandle, ffi.Pointer request); - - /// Retrieve the fullscreen status of the window. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - bool getFullscreen(HWND windowHandle); - - /// Retrieve the text length of the title of the window. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - int getWindowTextLength(HWND windowHandle); - - /// Retrieve the title of the window. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - int getWindowText(HWND windowHandle, ffi.Pointer<_Utf16> lpString, int maxLength); - - /// Retrieve the currently focused window handle. - /// - /// {@macro flutter.widgets.windowing.experimental} - @visibleForTesting - @internal - HWND getForegroundWindow(); -} - -class _NativeWin32PlatformInterface extends Win32PlatformInterface { - @override - bool hasTopLevelWindows(int engineId) { - return _hasTopLevelWindows(engineId); - } - - @override - void initialize(int engineId, ffi.Pointer request) { - _initializeWindowing(engineId, request); - } - - @override - int createWindow(int engineId, ffi.Pointer request) { - return _createWindow(engineId, request); - } - - @override - HWND getWindowHandle(int engineId, int viewId) { - return _getWindowHandle(engineId, viewId); - } - - @override - void destroyWindow(HWND windowHandle) { - _destroyWindow(windowHandle); - } - - @override - ActualContentSize getWindowContentSize(HWND windowHandle) { - return _getWindowContentSize(windowHandle); - } - - @override - void setWindowTitle(HWND windowHandle, ffi.Pointer<_Utf16> title) { - _setWindowTitle(windowHandle, title); - } - - @override - void setWindowContentSize(HWND windowHandle, ffi.Pointer size) { - _setWindowContentSize(windowHandle, size); - } - - @override - void setWindowConstraints(HWND windowHandle, ffi.Pointer constraints) { - _setWindowConstraints(windowHandle, constraints); - } - - @override - void showWindow(HWND windowHandle, int command) { - _showWindow(windowHandle, command); - } - - @override - int isIconic(HWND windowHandle) { - return _isIconic(windowHandle); - } - - @override - int isZoomed(HWND windowHandle) { - return _isZoomed(windowHandle); - } - - @override - void setFullscreen(HWND windowHandle, ffi.Pointer request) { - _setFullscreen(windowHandle, request); - } - - @override - bool getFullscreen(HWND windowHandle) { - return _getFullscreen(windowHandle); - } - - @override - int getWindowTextLength(HWND windowHandle) { - return _getWindowTextLength(windowHandle); - } - - @override - int getWindowText(HWND windowHandle, ffi.Pointer<_Utf16> lpString, int maxLength) { - return _getWindowText(windowHandle, lpString, maxLength); - } - - @override - HWND getForegroundWindow() { - return _getForegroundWindow(); - } - +class _Win32PlatformInterface { @ffi.Native( symbol: 'InternalFlutterWindows_WindowManager_HasTopLevelWindows', ) - external static bool _hasTopLevelWindows(int engineId); + external static bool hasTopLevelWindows(int engineId); - @ffi.Native)>( + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_Initialize', ) - external static void _initializeWindowing( + external static void initializeWindowing( int engineId, - ffi.Pointer request, + ffi.Pointer<_WindowingInitRequest> request, ); - @ffi.Native)>( + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_CreateRegularWindow', ) - external static int _createWindow(int engineId, ffi.Pointer request); + external static int createWindow(int engineId, ffi.Pointer<_WindowCreationRequest> request); @ffi.Native( symbol: 'InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle', ) - external static HWND _getWindowHandle(int engineId, int viewId); + external static HWND getWindowHandle(int engineId, int viewId); @ffi.Native(symbol: 'DestroyWindow') - external static void _destroyWindow(HWND windowHandle); + external static void destroyWindow(HWND windowHandle); - @ffi.Native( + @ffi.Native<_ActualContentSize Function(HWND)>( symbol: 'InternalFlutterWindows_WindowManager_GetWindowContentSize', ) - external static ActualContentSize _getWindowContentSize(HWND windowHandle); + external static _ActualContentSize getWindowContentSize(HWND windowHandle); @ffi.Native)>(symbol: 'SetWindowTextW') - external static void _setWindowTitle(HWND windowHandle, ffi.Pointer<_Utf16> title); + external static void setWindowTitle(HWND windowHandle, ffi.Pointer<_Utf16> title); - @ffi.Native)>( + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetWindowSize', ) - external static void _setWindowContentSize( + external static void setWindowContentSize( HWND windowHandle, - ffi.Pointer size, + ffi.Pointer<_WindowSizeRequest> size, ); - @ffi.Native)>( + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetWindowConstraints', ) - external static void _setWindowConstraints( + external static void setWindowConstraints( HWND windowHandle, - ffi.Pointer constraints, + ffi.Pointer<_WindowConstraintsRequest> constraints, ); @ffi.Native(symbol: 'ShowWindow') - external static void _showWindow(HWND windowHandle, int command); + external static void showWindow(HWND windowHandle, int command); @ffi.Native(symbol: 'IsIconic') - external static int _isIconic(HWND windowHandle); + external static int isIconic(HWND windowHandle); @ffi.Native(symbol: 'IsZoomed') - external static int _isZoomed(HWND windowHandle); + external static int isZoomed(HWND windowHandle); - @ffi.Native)>( + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetFullscreen', ) - external static void _setFullscreen( + external static void setFullscreen( HWND windowHandle, - ffi.Pointer request, + ffi.Pointer<_WindowFullscreenRequest> request, ); @ffi.Native(symbol: 'InternalFlutterWindows_WindowManager_GetFullscreen') - external static bool _getFullscreen(HWND windowHandle); + external static bool getFullscreen(HWND windowHandle); @ffi.Native(symbol: 'GetWindowTextLengthW') - external static int _getWindowTextLength(HWND windowHandle); + external static int getWindowTextLength(HWND windowHandle); @ffi.Native, ffi.Int32)>(symbol: 'GetWindowTextW') - external static int _getWindowText( - HWND windowHandle, - ffi.Pointer<_Utf16> lpString, - int maxLength, - ); + external static int getWindowText(HWND windowHandle, ffi.Pointer<_Utf16> lpString, int maxLength); @ffi.Native(symbol: 'GetForegroundWindow') - external static HWND _getForegroundWindow(); + external static HWND getForegroundWindow(); } -/// Payload for the creation method used by [Win32PlatformInterface.createWindow]. -/// -/// {@macro flutter.widgets.windowing.experimental} -@visibleForTesting -@internal -final class WindowCreationRequest extends ffi.Struct { - external WindowSizeRequest preferredSize; - external WindowConstraintsRequest preferredConstraints; +/// Payload for the creation method used by [_Win32PlatformInterface.createWindow]. +final class _WindowCreationRequest extends ffi.Struct { + external _WindowSizeRequest preferredSize; + external _WindowConstraintsRequest preferredConstraints; external ffi.Pointer<_Utf16> title; } /// Payload for the initialization request for the windowing subsystem used /// by the constructor for [WindowingOwnerWin32]. -/// -/// {@macro flutter.widgets.windowing.experimental} -@visibleForTesting -@internal -final class WindowingInitRequest extends ffi.Struct { - external ffi.Pointer)>> +final class _WindowingInitRequest extends ffi.Struct { + external ffi.Pointer)>> onMessage; } -/// Payload for the size of a window used by [WindowCreationRequest] and -/// [Win32PlatformInterface.setWindowContentSize]. -/// -/// {@macro flutter.widgets.windowing.experimental} -@visibleForTesting -@internal -final class WindowSizeRequest extends ffi.Struct { +/// Payload for the size of a window used by [_WindowCreationRequest] and +/// [_Win32PlatformInterface.setWindowContentSize]. +final class _WindowSizeRequest extends ffi.Struct { @ffi.Bool() external bool hasSize; @@ -828,13 +547,9 @@ final class WindowSizeRequest extends ffi.Struct { } } -/// Payload for the constraints of a window used by [WindowCreationRequest] and -/// [Win32PlatformInterface.setWindowConstraints]. -/// -/// {@macro flutter.widgets.windowing.experimental} -@visibleForTesting -@internal -final class WindowConstraintsRequest extends ffi.Struct { +/// Payload for the constraints of a window used by [_WindowCreationRequest] and +/// [_Win32PlatformInterface.setWindowConstraints]. +final class _WindowConstraintsRequest extends ffi.Struct { @ffi.Bool() external bool hasConstraints; @@ -859,12 +574,8 @@ final class WindowConstraintsRequest extends ffi.Struct { } } -/// A message received for all toplevel windows, used by [WindowingInitRequest]. -/// -/// {@macro flutter.widgets.windowing.experimental} -@visibleForTesting -@internal -final class WindowsMessage extends ffi.Struct { +/// A message received for all toplevel windows, used by [_WindowingInitRequest]. +final class _WindowsMessage extends ffi.Struct { @ffi.Int64() external int viewId; @@ -887,12 +598,8 @@ final class WindowsMessage extends ffi.Struct { } /// Holds the real size of a window as retrieved from -/// [Win32PlatformInterface.getWindowContentSize]. -/// -/// {@macro flutter.widgets.windowing.experimental} -@visibleForTesting -@internal -final class ActualContentSize extends ffi.Struct { +/// [_Win32PlatformInterface.getWindowContentSize]. +final class _ActualContentSize extends ffi.Struct { @ffi.Double() external double width; @@ -900,12 +607,8 @@ final class ActualContentSize extends ffi.Struct { external double height; } -/// Payload for the [Win32PlatformInterface.setFullscreen] request. -/// -/// {@macro flutter.widgets.windowing.experimental} -@visibleForTesting -@internal -final class WindowFullscreenRequest extends ffi.Struct { +/// Payload for the [_Win32PlatformInterface.setFullscreen] request. +final class _WindowFullscreenRequest extends ffi.Struct { @ffi.Bool() external bool fullscreen; From f4334d27934b2ec5ddbbcdb16c15ba4acd5f6c49 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 14 Aug 2025 10:54:39 -0400 Subject: [PATCH 038/720] Roll Dart SDK from 9b4691f35139 to 214a7f829913 (2 revisions) (#173769) https://dart.googlesource.com/sdk.git/+log/9b4691f35139..214a7f829913 2025-08-14 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-98.0.dev 2025-08-14 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-97.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 chinmaygarde@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 5ced9d394f802..a88564a19bbd9 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '9b4691f351395372a8964674559d0235616ca38e', + 'dart_revision': '214a7f8299135e56d01f3fd32596636f1fb6ba94', # 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 d0851959fead17f898eb29026430f234e79f4b90 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 14 Aug 2025 09:05:26 -0700 Subject: [PATCH 039/720] Thread sub-builders for every engine-uploading builder (#173742) Closes https://github.com/flutter/flutter/issues/173655. We should consider making these flags either the default (opt-out to `false`) or evergreen (no opt-out). --- engine/src/flutter/ci/builders/linux_arm_host_engine.json | 2 ++ engine/src/flutter/ci/builders/linux_host_desktop_engine.json | 2 ++ engine/src/flutter/ci/builders/linux_web_engine_build.json | 2 ++ engine/src/flutter/ci/builders/mac_android_aot_engine.json | 2 ++ .../src/flutter/ci/builders/windows_android_aot_engine.json | 2 ++ engine/src/flutter/ci/builders/windows_arm_host_engine.json | 4 +++- engine/src/flutter/ci/builders/windows_host_engine.json | 2 ++ 7 files changed, 15 insertions(+), 1 deletion(-) 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 60008fc008da7..59f96d696d3c1 100644 --- a/engine/src/flutter/ci/builders/linux_arm_host_engine.json +++ b/engine/src/flutter/ci/builders/linux_arm_host_engine.json @@ -8,6 +8,8 @@ "definition files." ], "luci_flags": { + "delay_collect_builds": true, + "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/linux_host_desktop_engine.json b/engine/src/flutter/ci/builders/linux_host_desktop_engine.json index fd54b210c9e96..a7a32f07fafbd 100644 --- a/engine/src/flutter/ci/builders/linux_host_desktop_engine.json +++ b/engine/src/flutter/ci/builders/linux_host_desktop_engine.json @@ -8,6 +8,8 @@ "definition files." ], "luci_flags": { + "delay_collect_builds": true, + "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/linux_web_engine_build.json b/engine/src/flutter/ci/builders/linux_web_engine_build.json index c02a5aa8b2273..749458994fe2a 100644 --- a/engine/src/flutter/ci/builders/linux_web_engine_build.json +++ b/engine/src/flutter/ci/builders/linux_web_engine_build.json @@ -8,6 +8,8 @@ "definition files." ], "luci_flags": { + "delay_collect_builds": true, + "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/mac_android_aot_engine.json b/engine/src/flutter/ci/builders/mac_android_aot_engine.json index 57f61d4c0b156..9de57f7c3e2a8 100644 --- a/engine/src/flutter/ci/builders/mac_android_aot_engine.json +++ b/engine/src/flutter/ci/builders/mac_android_aot_engine.json @@ -8,6 +8,8 @@ "definition files." ], "luci_flags": { + "delay_collect_builds": true, + "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/windows_android_aot_engine.json b/engine/src/flutter/ci/builders/windows_android_aot_engine.json index f51cc809a8d8e..e660d7b60ab95 100644 --- a/engine/src/flutter/ci/builders/windows_android_aot_engine.json +++ b/engine/src/flutter/ci/builders/windows_android_aot_engine.json @@ -1,5 +1,7 @@ { "luci_flags": { + "delay_collect_builds": true, + "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/windows_arm_host_engine.json b/engine/src/flutter/ci/builders/windows_arm_host_engine.json index cfbbd524e3f9b..ac771ae8cf61b 100644 --- a/engine/src/flutter/ci/builders/windows_arm_host_engine.json +++ b/engine/src/flutter/ci/builders/windows_arm_host_engine.json @@ -1,6 +1,8 @@ { "luci_flags": { - "upload_content_hash": true + "delay_collect_builds": true, + "parallel_download_builds": true, + "upload_content_hash": true }, "builds": [ { diff --git a/engine/src/flutter/ci/builders/windows_host_engine.json b/engine/src/flutter/ci/builders/windows_host_engine.json index ae9d426228d76..d864eb6e8b05a 100644 --- a/engine/src/flutter/ci/builders/windows_host_engine.json +++ b/engine/src/flutter/ci/builders/windows_host_engine.json @@ -8,6 +8,8 @@ "definition files." ], "luci_flags": { + "delay_collect_builds": true, + "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ From e2c7311956fa08d84ba29fc431cbd6ae896d86cc Mon Sep 17 00:00:00 2001 From: EdwynZN Date: Thu, 14 Aug 2025 11:36:09 -0600 Subject: [PATCH 040/720] Fix default minimumSize in dropdownMenu when maximumSize is null (#169438) Previous change in `DropDownMenu` https://github.com/flutter/flutter/pull/162380 forced the menu width to be constrained between the width or anchorWidth and the maximumSize width menuStyle, but giving a minimum of zero when maximumSize was not enforced or null. This prevents width or anchorWidth to be used at all as the minimum will be zero always if maximumSize is null Now this sets the default value back to width or anchorWidth if maximumSize is null, keeping consistency of the previous configuration while using the fix of maximumSize when provided. Fixes https://github.com/flutter/flutter/issues/170970 ## 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 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 [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: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> --- .../lib/src/material/dropdown_menu.dart | 4 +- .../test/material/dropdown_menu_test.dart | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index 9d3504bbe5e4b..8b72310abf438 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -1044,7 +1044,7 @@ class _DropdownMenuState extends State> { final double? effectiveMaximumWidth = effectiveMenuStyle!.maximumSize ?.resolve(states) ?.width; - return Size(math.min(widget.width!, effectiveMaximumWidth ?? 0.0), 0.0); + return Size(math.min(widget.width!, effectiveMaximumWidth ?? widget.width!), 0.0); }), ); } else if (anchorWidth != null) { @@ -1053,7 +1053,7 @@ class _DropdownMenuState extends State> { final double? effectiveMaximumWidth = effectiveMenuStyle!.maximumSize ?.resolve(states) ?.width; - return Size(math.min(anchorWidth, effectiveMaximumWidth ?? 0.0), 0.0); + return Size(math.min(anchorWidth, effectiveMaximumWidth ?? anchorWidth), 0.0); }), ); } diff --git a/packages/flutter/test/material/dropdown_menu_test.dart b/packages/flutter/test/material/dropdown_menu_test.dart index 5c7f73b7ba0a5..6dc39c72d2fc1 100644 --- a/packages/flutter/test/material/dropdown_menu_test.dart +++ b/packages/flutter/test/material/dropdown_menu_test.dart @@ -4301,8 +4301,77 @@ void main() { expect(tester.takeException(), isNull); expect(tester.getSize(findMenuItemButton(menuChildren.first.label)).width, 150.0); + + // The overwrite of menuStyle is different when a width is provided but maximumSize is not, + // So it needs to be tested separately. + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: DropdownMenu( + width: 200.0, + dropdownMenuEntries: menuChildren, + menuStyle: const MenuStyle(), + ), + ), + ), + ); + + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + + expect(tester.getSize(findMenuItemButton(menuChildren.first.label)).width, 200.0); }); + testWidgets( + 'ensure items are constrained to intrinsic size of DropdownMenu (width or anchor) when no maximumSize', + (WidgetTester tester) async { + const String shortLabel = 'Male'; + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: DropdownMenu( + width: 200, + dropdownMenuEntries: >[ + DropdownMenuEntry(value: 0, label: shortLabel), + ], + menuStyle: MenuStyle(), + ), + ), + ), + ); + + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + + expect(tester.getSize(findMenuItemButton(shortLabel)).width, 200); + + // Use expandedInsets to anchor the TextField to the same size as the parent. + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: SizedBox( + width: double.infinity, + child: DropdownMenu( + expandedInsets: EdgeInsets.symmetric(horizontal: 20), + dropdownMenuEntries: >[ + DropdownMenuEntry(value: 0, label: shortLabel), + ], + menuStyle: MenuStyle(), + ), + ), + ), + ), + ); + + await tester.tap(find.byType(TextField)); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isNull); + // Default width is 800, so the expected width is 800 - padding (20 + 20). + expect(tester.getSize(findMenuItemButton(shortLabel)).width, 760.0); + }, + ); + // Regression test for https://github.com/flutter/flutter/issues/164905. testWidgets('ensure exclude semantics for trailing button', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); From ee3551fbe473b0f175a360cfd5d6f14313ba79bc Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Thu, 14 Aug 2025 19:38:02 +0200 Subject: [PATCH 041/720] Fix InputDecorator label padding (#173344) ## Description This PR fixes the label padding for an InputDecorator with prefixIcon and/or suffixIcon. # Before The label was shorter than the available space because `InputDecoration.contentPadding.horizontal` was applied even when `prefixIcon` and `suffixIcon` were defined. The icons width replaces the corresponding `contentPadding`, so both should not be used at the same time to compute the available space. image # After The label takes all the available space. image ## Related Issue Fixes [Label padding is wrong for InputDecorator with prefixIcon and/or suffixIcon ](https://github.com/flutter/flutter/issues/173341) ## Tests Adds 1 test. --- .../lib/src/material/dropdown_menu.dart | 8 +- .../lib/src/material/input_decorator.dart | 7 +- .../test/material/input_decorator_test.dart | 87 +++++++++++++++++-- 3 files changed, 85 insertions(+), 17 deletions(-) diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart index 8b72310abf438..122a796d44c6e 100644 --- a/packages/flutter/lib/src/material/dropdown_menu.dart +++ b/packages/flutter/lib/src/material/dropdown_menu.dart @@ -1404,16 +1404,16 @@ class _RenderDropdownMenuBody extends RenderBox child = childParentData.nextSibling; continue; } - final double maxIntrinsicWidth = child.getMinIntrinsicWidth(height); + final double minIntrinsicWidth = child.getMinIntrinsicWidth(height); // Add the width of leading icon. if (child == lastChild) { - width += maxIntrinsicWidth; + width += minIntrinsicWidth; } // Add the width of trailing icon. if (child == childBefore(lastChild!)) { - width += maxIntrinsicWidth; + width += minIntrinsicWidth; } - width = math.max(width, maxIntrinsicWidth); + width = math.max(width, minIntrinsicWidth); final _DropdownMenuBodyParentData childParentData = child.parentData! as _DropdownMenuBodyParentData; child = childParentData.nextSibling; diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index 85016c2e03df4..04ccff4fc8183 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -1012,16 +1012,15 @@ class _RenderDecoration extends RenderBox final double topHeight; if (label != null) { final double suffixIconSpace = decoration.border.isOutline - ? lerpDouble(suffixIconSize.width, 0.0, decoration.floatingLabelProgress)! + ? lerpDouble(suffixIconSize.width, contentPadding.end, decoration.floatingLabelProgress)! : suffixIconSize.width; final double labelWidth = math.max( 0.0, constraints.maxWidth - (decoration.inputGap * 2 + iconWidth + - contentPadding.horizontal + - prefixIconSize.width + - suffixIconSpace), + (prefixIcon == null ? contentPadding.start : prefixIconSize.width) + + (suffixIcon == null ? contentPadding.end : suffixIconSpace)), ); // Increase the available width for the label when it is scaled down. diff --git a/packages/flutter/test/material/input_decorator_test.dart b/packages/flutter/test/material/input_decorator_test.dart index 1271fb9dddf65..11f0be146d9d3 100644 --- a/packages/flutter/test/material/input_decorator_test.dart +++ b/packages/flutter/test/material/input_decorator_test.dart @@ -2951,6 +2951,71 @@ void main() { // TODO(bleroux): fix input decorator to not rely on forcing the label text line height to 1.0. }); + testWidgets('When the label appears within the input its padding is correct', ( + WidgetTester tester, + ) async { + // Define a label larger than the available decorator, the label will fill + // all the available space (decorator width minus padding and affixes). + const Widget largeLabel = SizedBox(key: customLabelKey, width: 1000, height: 16); + await tester.pumpWidget( + buildInputDecorator( + isEmpty: true, + decoration: const InputDecoration(filled: true, label: largeLabel), + ), + ); + + // For filled and/or outlined decoration, the horizontal padding is 16. + const double horizontalPadding = 16.0; + expect(getCustomLabelRect(tester).left, horizontalPadding); + expect(getCustomLabelRect(tester).right, 800 - horizontalPadding); + + // Outlined decorator. + await tester.pumpWidget( + buildInputDecorator( + isEmpty: true, + decoration: const InputDecoration(border: OutlineInputBorder(), label: largeLabel), + ), + ); + + expect(getCustomLabelRect(tester).left, horizontalPadding); + expect(getCustomLabelRect(tester).right, 800 - horizontalPadding); + + // Rebuild with affixes. + await tester.pumpWidget( + buildInputDecorator( + isEmpty: true, + decoration: const InputDecoration( + filled: true, + label: largeLabel, + suffixIcon: Icon(Icons.align_horizontal_left_sharp), + prefixIcon: Icon(Icons.align_horizontal_right_sharp), + ), + ), + ); + + // When suffixIcon and/or prefixIcon are set, the corresponding horizontal + // padding is 52 (48 for the icon + 4 input gap based on M3 spec). + const double affixesHorizontalPadding = 52.0; + expect(getCustomLabelRect(tester).left, affixesHorizontalPadding); + expect(getCustomLabelRect(tester).right, 800 - affixesHorizontalPadding); + + // Outlined decorator. + await tester.pumpWidget( + buildInputDecorator( + isEmpty: true, + decoration: const InputDecoration( + border: OutlineInputBorder(), + label: largeLabel, + suffixIcon: Icon(Icons.align_horizontal_left_sharp), + prefixIcon: Icon(Icons.align_horizontal_right_sharp), + ), + ), + ); + + expect(getCustomLabelRect(tester).left, affixesHorizontalPadding); + expect(getCustomLabelRect(tester).right, 800 - affixesHorizontalPadding); + }); + testWidgets( 'The label appears above the input when there is no content and floatingLabelBehavior is always', (WidgetTester tester) async { @@ -8770,15 +8835,19 @@ void main() { 'Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.'; Widget getLabeledInputDecorator(FloatingLabelBehavior floatingLabelBehavior) => MaterialApp( - home: Material( - child: SizedBox( - width: 300, - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(borderSide: BorderSide(color: Colors.greenAccent)), - suffixIcon: const Icon(Icons.arrow_drop_down), - floatingLabelBehavior: floatingLabelBehavior, - labelText: labelText, + home: MaterialApp( + home: Scaffold( + body: SizedBox( + width: 300, + child: TextField( + decoration: InputDecoration( + border: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.greenAccent), + ), + suffixIcon: const Icon(Icons.arrow_drop_down), + floatingLabelBehavior: floatingLabelBehavior, + labelText: labelText, + ), ), ), ), From e68da97bd5b1ce73f3e897ac5ca66cc907bfb8fc Mon Sep 17 00:00:00 2001 From: Victor Sanni Date: Thu, 14 Aug 2025 11:06:57 -0700 Subject: [PATCH 042/720] [VPAT][A11y] Announce Autocomplete search results status (#173480) Status announcements 'Search results found' and 'no results found' were observed on a search view on the iOS sheets app. Fixes [[VPAT][A11y] autocomplete must announce status when search result is available](https://github.com/flutter/flutter/issues/173064) Fixes b/429094918 --- .../flutter/lib/src/widgets/autocomplete.dart | 16 + .../lib/src/widgets/localizations.dart | 14 + .../test/widgets/autocomplete_test.dart | 72 +++ .../test/widgets/localizations_test.dart | 2 + .../l10n/generated_widgets_localizations.dart | 474 ++++++++++++++++++ .../lib/src/l10n/widgets_af.arb | 4 +- .../lib/src/l10n/widgets_am.arb | 4 +- .../lib/src/l10n/widgets_ar.arb | 4 +- .../lib/src/l10n/widgets_as.arb | 4 +- .../lib/src/l10n/widgets_az.arb | 4 +- .../lib/src/l10n/widgets_be.arb | 4 +- .../lib/src/l10n/widgets_bg.arb | 4 +- .../lib/src/l10n/widgets_bn.arb | 4 +- .../lib/src/l10n/widgets_bs.arb | 4 +- .../lib/src/l10n/widgets_ca.arb | 4 +- .../lib/src/l10n/widgets_cs.arb | 4 +- .../lib/src/l10n/widgets_cy.arb | 4 +- .../lib/src/l10n/widgets_da.arb | 4 +- .../lib/src/l10n/widgets_de.arb | 4 +- .../lib/src/l10n/widgets_el.arb | 4 +- .../lib/src/l10n/widgets_en.arb | 4 + .../lib/src/l10n/widgets_es.arb | 4 +- .../lib/src/l10n/widgets_et.arb | 4 +- .../lib/src/l10n/widgets_eu.arb | 4 +- .../lib/src/l10n/widgets_fa.arb | 4 +- .../lib/src/l10n/widgets_fi.arb | 4 +- .../lib/src/l10n/widgets_fil.arb | 4 +- .../lib/src/l10n/widgets_fr.arb | 4 +- .../lib/src/l10n/widgets_gl.arb | 4 +- .../lib/src/l10n/widgets_gsw.arb | 4 +- .../lib/src/l10n/widgets_gu.arb | 4 +- .../lib/src/l10n/widgets_he.arb | 4 +- .../lib/src/l10n/widgets_hi.arb | 4 +- .../lib/src/l10n/widgets_hr.arb | 4 +- .../lib/src/l10n/widgets_hu.arb | 4 +- .../lib/src/l10n/widgets_hy.arb | 4 +- .../lib/src/l10n/widgets_id.arb | 4 +- .../lib/src/l10n/widgets_is.arb | 4 +- .../lib/src/l10n/widgets_it.arb | 4 +- .../lib/src/l10n/widgets_ja.arb | 4 +- .../lib/src/l10n/widgets_ka.arb | 4 +- .../lib/src/l10n/widgets_kk.arb | 4 +- .../lib/src/l10n/widgets_km.arb | 4 +- .../lib/src/l10n/widgets_kn.arb | 6 +- .../lib/src/l10n/widgets_ko.arb | 4 +- .../lib/src/l10n/widgets_ky.arb | 4 +- .../lib/src/l10n/widgets_lo.arb | 4 +- .../lib/src/l10n/widgets_lt.arb | 4 +- .../lib/src/l10n/widgets_lv.arb | 4 +- .../lib/src/l10n/widgets_mk.arb | 4 +- .../lib/src/l10n/widgets_ml.arb | 4 +- .../lib/src/l10n/widgets_mn.arb | 4 +- .../lib/src/l10n/widgets_mr.arb | 4 +- .../lib/src/l10n/widgets_ms.arb | 4 +- .../lib/src/l10n/widgets_my.arb | 4 +- .../lib/src/l10n/widgets_nb.arb | 4 +- .../lib/src/l10n/widgets_ne.arb | 4 +- .../lib/src/l10n/widgets_nl.arb | 4 +- .../lib/src/l10n/widgets_no.arb | 4 +- .../lib/src/l10n/widgets_or.arb | 4 +- .../lib/src/l10n/widgets_pa.arb | 4 +- .../lib/src/l10n/widgets_pl.arb | 4 +- .../lib/src/l10n/widgets_ps.arb | 4 +- .../lib/src/l10n/widgets_pt.arb | 4 +- .../lib/src/l10n/widgets_ro.arb | 4 +- .../lib/src/l10n/widgets_ru.arb | 4 +- .../lib/src/l10n/widgets_si.arb | 4 +- .../lib/src/l10n/widgets_sk.arb | 4 +- .../lib/src/l10n/widgets_sl.arb | 4 +- .../lib/src/l10n/widgets_sq.arb | 4 +- .../lib/src/l10n/widgets_sr.arb | 4 +- .../lib/src/l10n/widgets_sv.arb | 4 +- .../lib/src/l10n/widgets_sw.arb | 4 +- .../lib/src/l10n/widgets_ta.arb | 4 +- .../lib/src/l10n/widgets_te.arb | 4 +- .../lib/src/l10n/widgets_th.arb | 4 +- .../lib/src/l10n/widgets_tl.arb | 4 +- .../lib/src/l10n/widgets_tr.arb | 4 +- .../lib/src/l10n/widgets_uk.arb | 4 +- .../lib/src/l10n/widgets_ur.arb | 4 +- .../lib/src/l10n/widgets_uz.arb | 4 +- .../lib/src/l10n/widgets_vi.arb | 4 +- .../lib/src/l10n/widgets_zh.arb | 4 +- .../lib/src/l10n/widgets_zu.arb | 4 +- 84 files changed, 818 insertions(+), 78 deletions(-) diff --git a/packages/flutter/lib/src/widgets/autocomplete.dart b/packages/flutter/lib/src/widgets/autocomplete.dart index 97931a93d5d97..8a007dcb5109c 100644 --- a/packages/flutter/lib/src/widgets/autocomplete.dart +++ b/packages/flutter/lib/src/widgets/autocomplete.dart @@ -19,6 +19,8 @@ import 'editable_text.dart'; import 'focus_manager.dart'; import 'framework.dart'; import 'inherited_notifier.dart'; +import 'localizations.dart'; +import 'media_query.dart'; import 'overlay.dart'; import 'shortcuts.dart'; import 'tap_region.dart'; @@ -404,6 +406,17 @@ class _RawAutocompleteState extends State> } } + void _announceSemantics(bool resultsAvailable) { + if (!MediaQuery.supportsAnnounceOf(context)) { + return; + } + final WidgetsLocalizations localizations = WidgetsLocalizations.of(context); + final String optionsHint = resultsAvailable + ? localizations.searchResultsFound + : localizations.noResultsFound; + SemanticsService.announce(optionsHint, localizations.textDirection); + } + // Assigning an ID to every call of _onChangedField is necessary to avoid a // situation where _options is updated by an older call when multiple // _onChangedField calls are running simultaneously. @@ -426,6 +439,9 @@ class _RawAutocompleteState extends State> if (callId != _onChangedCallId || !shouldUpdateOptions) { return; } + if (_options.isEmpty != options.isEmpty) { + _announceSemantics(options.isNotEmpty); + } _options = options; _updateHighlight(_highlightedOptionIndex.value); final T? selection = _selection; diff --git a/packages/flutter/lib/src/widgets/localizations.dart b/packages/flutter/lib/src/widgets/localizations.dart index a9487d3ca3d4b..633b3ff223b85 100644 --- a/packages/flutter/lib/src/widgets/localizations.dart +++ b/packages/flutter/lib/src/widgets/localizations.dart @@ -194,6 +194,14 @@ abstract class WidgetsLocalizations { /// list one space right in the list. String get reorderItemRight; + /// The semantics label used for [RawAutocomplete] when the options list goes + /// from empty to non-empty. + String get searchResultsFound => 'Search results found'; + + /// The semantics label used for [RawAutocomplete] when the options list goes + /// from non-empty to empty. + String get noResultsFound => 'No results found'; + /// Label for "copy" edit buttons and menu items. String get copyButtonLabel; @@ -284,6 +292,12 @@ class DefaultWidgetsLocalizations implements WidgetsLocalizations { @override String get reorderItemToStart => 'Move to the start'; + @override + String get searchResultsFound => 'Search results found'; + + @override + String get noResultsFound => 'No results found'; + @override String get copyButtonLabel => 'Copy'; diff --git a/packages/flutter/test/widgets/autocomplete_test.dart b/packages/flutter/test/widgets/autocomplete_test.dart index 8c002b2447876..a328dc849f582 100644 --- a/packages/flutter/test/widgets/autocomplete_test.dart +++ b/packages/flutter/test/widgets/autocomplete_test.dart @@ -3271,6 +3271,78 @@ void main() { expect(find.byKey(optionsKey), findsOneWidget); }); + testWidgets('Autocomplete Semantics announcement', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + final GlobalKey fieldKey = GlobalKey(); + final GlobalKey optionsKey = GlobalKey(); + late Iterable lastOptions; + late FocusNode focusNode; + late TextEditingController textEditingController; + const DefaultWidgetsLocalizations localizations = DefaultWidgetsLocalizations(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: RawAutocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + return kOptions.where((String option) { + return option.contains(textEditingValue.text.toLowerCase()); + }); + }, + fieldViewBuilder: + ( + BuildContext context, + TextEditingController fieldTextEditingController, + FocusNode fieldFocusNode, + VoidCallback onFieldSubmitted, + ) { + focusNode = fieldFocusNode; + textEditingController = fieldTextEditingController; + return TextField( + key: fieldKey, + focusNode: focusNode, + controller: textEditingController, + ); + }, + optionsViewBuilder: + ( + BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options, + ) { + lastOptions = options; + return Container(key: optionsKey); + }, + ), + ), + ), + ); + + expect(find.byKey(fieldKey), findsOneWidget); + expect(find.byKey(optionsKey), findsNothing); + + expect(tester.takeAnnouncements(), isEmpty); + + focusNode.requestFocus(); + await tester.pump(); + expect(find.byKey(optionsKey), findsOneWidget); + expect(lastOptions.length, kOptions.length); + expect(tester.takeAnnouncements().first.message, localizations.searchResultsFound); + + await tester.enterText(find.byKey(fieldKey), 'a'); + await tester.pump(); + expect(find.byKey(optionsKey), findsOneWidget); + expect(lastOptions.length, greaterThan(0)); + expect(tester.takeAnnouncements(), isEmpty); + + await tester.enterText(find.byKey(fieldKey), 'zzzz'); + await tester.pump(); + expect(find.byKey(optionsKey), findsNothing); + expect(tester.takeAnnouncements().first.message, localizations.noResultsFound); + + handle.dispose(); + }); + testWidgets('RawAutocomplete renders at zero area', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/widgets/localizations_test.dart b/packages/flutter/test/widgets/localizations_test.dart index a622ba6c0343c..cdc639c9b5dd3 100644 --- a/packages/flutter/test/widgets/localizations_test.dart +++ b/packages/flutter/test/widgets/localizations_test.dart @@ -22,6 +22,8 @@ void main() { expect(localizations.reorderItemRight, isNotNull); expect(localizations.reorderItemToEnd, isNotNull); expect(localizations.reorderItemToStart, isNotNull); + expect(localizations.searchResultsFound, isNotNull); + expect(localizations.noResultsFound, isNotNull); expect(localizations.copyButtonLabel, isNotNull); expect(localizations.cutButtonLabel, isNotNull); expect(localizations.pasteButtonLabel, isNotNull); 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 8a6ac52ba3fd0..dfc6f78aea681 100644 --- a/packages/flutter_localizations/lib/src/l10n/generated_widgets_localizations.dart +++ b/packages/flutter_localizations/lib/src/l10n/generated_widgets_localizations.dart @@ -37,6 +37,9 @@ class WidgetsLocalizationAf extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Kyk op'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Plak'; @@ -58,6 +61,9 @@ class WidgetsLocalizationAf extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Skuif op'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Deursoek web'; @@ -84,6 +90,9 @@ class WidgetsLocalizationAm extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'ይመልከቱ'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'ለጥፍ'; @@ -105,6 +114,9 @@ class WidgetsLocalizationAm extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ወደ ላይ ውሰድ'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ድርን ፈልግ'; @@ -131,6 +143,9 @@ class WidgetsLocalizationAr extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'بحث عام'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'لصق'; @@ -152,6 +167,9 @@ class WidgetsLocalizationAr extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'نقل لأعلى'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'البحث على الويب'; @@ -178,6 +196,9 @@ class WidgetsLocalizationAs extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'ওপৰলৈ চাওক'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => "পে'ষ্ট কৰক"; @@ -199,6 +220,9 @@ class WidgetsLocalizationAs extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ওপৰলৈ নিয়ক'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ৱেবত সন্ধান কৰক'; @@ -225,6 +249,9 @@ class WidgetsLocalizationAz extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Axtarın'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Yerləşdirin'; @@ -246,6 +273,9 @@ class WidgetsLocalizationAz extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Yuxarı köçürün'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Vebdə axtarın'; @@ -272,6 +302,9 @@ class WidgetsLocalizationBe extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Знайсці'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Уставіць'; @@ -293,6 +326,9 @@ class WidgetsLocalizationBe extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Перамясціць уверх'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Пошук у сетцы'; @@ -319,6 +355,9 @@ class WidgetsLocalizationBg extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Look Up'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Поставяне'; @@ -340,6 +379,9 @@ class WidgetsLocalizationBg extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Преместване нагоре'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Търсене в мрежата'; @@ -366,6 +408,9 @@ class WidgetsLocalizationBn extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'লুক-আপ'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'পেস্ট করুন'; @@ -387,6 +432,9 @@ class WidgetsLocalizationBn extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'উপরের দিকে সরান'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ওয়েবে সার্চ করুন'; @@ -413,6 +461,9 @@ class WidgetsLocalizationBs extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Pogled nagore'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Zalijepi'; @@ -434,6 +485,9 @@ class WidgetsLocalizationBs extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Pomjeri nagore'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Pretraži Web'; @@ -460,6 +514,9 @@ class WidgetsLocalizationCa extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Mira amunt'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Enganxa'; @@ -481,6 +538,9 @@ class WidgetsLocalizationCa extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Mou amunt'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Cerca al web'; @@ -507,6 +567,9 @@ class WidgetsLocalizationCs extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Vyhledat'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Vložit'; @@ -528,6 +591,9 @@ class WidgetsLocalizationCs extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Přesunout nahoru'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Vyhledávat na webu'; @@ -554,6 +620,9 @@ class WidgetsLocalizationCy extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Chwilio'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Gludo'; @@ -575,6 +644,9 @@ class WidgetsLocalizationCy extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Symud i fyny'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => "Chwilio'r We"; @@ -601,6 +673,9 @@ class WidgetsLocalizationDa extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Slå op'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Indsæt'; @@ -622,6 +697,9 @@ class WidgetsLocalizationDa extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Flyt op'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Søg på nettet'; @@ -648,6 +726,9 @@ class WidgetsLocalizationDe extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Nachschlagen'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Einsetzen'; @@ -669,6 +750,9 @@ class WidgetsLocalizationDe extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Nach oben verschieben'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Im Web suchen'; @@ -703,6 +787,9 @@ class WidgetsLocalizationEl extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Look Up'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Επικόλληση'; @@ -724,6 +811,9 @@ class WidgetsLocalizationEl extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Μετακίνηση προς τα πάνω'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Αναζήτηση στον ιστό'; @@ -750,6 +840,9 @@ class WidgetsLocalizationEn extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Look Up'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Paste'; @@ -771,6 +864,9 @@ class WidgetsLocalizationEn extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Move up'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Search Web'; @@ -930,6 +1026,9 @@ class WidgetsLocalizationEs extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Buscador visual'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Pegar'; @@ -951,6 +1050,9 @@ class WidgetsLocalizationEs extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Mover hacia arriba'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Buscar en la Web'; @@ -1257,6 +1359,9 @@ class WidgetsLocalizationEt extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Look Up'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Kleebi'; @@ -1278,6 +1383,9 @@ class WidgetsLocalizationEt extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Teisalda üles'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Otsi veebist'; @@ -1304,6 +1412,9 @@ class WidgetsLocalizationEu extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Bilatu'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Itsatsi'; @@ -1325,6 +1436,9 @@ class WidgetsLocalizationEu extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Eraman gora'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Bilatu sarean'; @@ -1351,6 +1465,9 @@ class WidgetsLocalizationFa extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'جستجو'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'جای‌گذاری'; @@ -1372,6 +1489,9 @@ class WidgetsLocalizationFa extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'انتقال به بالا'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'جستجو در وب'; @@ -1398,6 +1518,9 @@ class WidgetsLocalizationFi extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Hae'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Liitä'; @@ -1419,6 +1542,9 @@ class WidgetsLocalizationFi extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Siirrä ylös'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Hae verkosta'; @@ -1445,6 +1571,9 @@ class WidgetsLocalizationFil extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Tumingin sa Itaas'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'I-paste'; @@ -1466,6 +1595,9 @@ class WidgetsLocalizationFil extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Ilipat pataas'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Maghanap sa Web'; @@ -1492,6 +1624,9 @@ class WidgetsLocalizationFr extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Recherche visuelle'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Coller'; @@ -1513,6 +1648,9 @@ class WidgetsLocalizationFr extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Déplacer vers le haut'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Rechercher sur le Web'; @@ -1556,6 +1694,9 @@ class WidgetsLocalizationGl extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Mirar cara arriba'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Pegar'; @@ -1577,6 +1718,9 @@ class WidgetsLocalizationGl extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Mover cara arriba'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Buscar na Web'; @@ -1603,6 +1747,9 @@ class WidgetsLocalizationGsw extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Nachschlagen'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Einsetzen'; @@ -1624,6 +1771,9 @@ class WidgetsLocalizationGsw extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Nach oben verschieben'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Im Web suchen'; @@ -1650,6 +1800,9 @@ class WidgetsLocalizationGu extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'શોધો'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'પેસ્ટ કરો'; @@ -1671,6 +1824,9 @@ class WidgetsLocalizationGu extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ઉપર ખસેડો'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'વેબ પર શોધો'; @@ -1697,6 +1853,9 @@ class WidgetsLocalizationHe extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'חיפוש'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'הדבקה'; @@ -1718,6 +1877,9 @@ class WidgetsLocalizationHe extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'העברה למעלה'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'חיפוש באינטרנט'; @@ -1744,6 +1906,9 @@ class WidgetsLocalizationHi extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'लुक अप बटन'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'चिपकाएं'; @@ -1765,6 +1930,9 @@ class WidgetsLocalizationHi extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ऊपर ले जाएं'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'वेब पर खोजें'; @@ -1791,6 +1959,9 @@ class WidgetsLocalizationHr extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Pogled prema gore'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Zalijepi'; @@ -1812,6 +1983,9 @@ class WidgetsLocalizationHr extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Pomakni prema gore'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Pretraži web'; @@ -1838,6 +2012,9 @@ class WidgetsLocalizationHu extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Felfelé nézés'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Beillesztés'; @@ -1859,6 +2036,9 @@ class WidgetsLocalizationHu extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Áthelyezés felfelé'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Keresés az interneten'; @@ -1885,6 +2065,9 @@ class WidgetsLocalizationHy extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Փնտրել'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Տեղադրել'; @@ -1906,6 +2089,9 @@ class WidgetsLocalizationHy extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Տեղափոխել վերև'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Որոնել համացանցում'; @@ -1932,6 +2118,9 @@ class WidgetsLocalizationId extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Cari'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Tempel'; @@ -1953,6 +2142,9 @@ class WidgetsLocalizationId extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Naikkan'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Telusuri di Web'; @@ -1979,6 +2171,9 @@ class WidgetsLocalizationIs extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Look Up'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Líma'; @@ -2000,6 +2195,9 @@ class WidgetsLocalizationIs extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Færa upp'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Leita á vefnum'; @@ -2026,6 +2224,9 @@ class WidgetsLocalizationIt extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Cerca'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Incolla'; @@ -2047,6 +2248,9 @@ class WidgetsLocalizationIt extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Sposta su'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Cerca sul web'; @@ -2073,6 +2277,9 @@ class WidgetsLocalizationJa extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => '調べる'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => '貼り付け'; @@ -2094,6 +2301,9 @@ class WidgetsLocalizationJa extends GlobalWidgetsLocalizations { @override String get reorderItemUp => '上に移動'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ウェブを検索'; @@ -2120,6 +2330,9 @@ class WidgetsLocalizationKa extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'აიხედეთ ზემოთ'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'ჩასმა'; @@ -2141,6 +2354,9 @@ class WidgetsLocalizationKa extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ზემოთ გადატანა'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ვებში ძიება'; @@ -2167,6 +2383,9 @@ class WidgetsLocalizationKk extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Іздеу'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Қою'; @@ -2188,6 +2407,9 @@ class WidgetsLocalizationKk extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Жоғарыға жылжыту'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Интернеттен іздеу'; @@ -2214,6 +2436,9 @@ class WidgetsLocalizationKm extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'រកមើល'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'ដាក់​ចូល'; @@ -2235,6 +2460,9 @@ class WidgetsLocalizationKm extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ផ្លាស់ទី​ឡើង​លើ'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ស្វែងរក​លើបណ្ដាញ'; @@ -2261,6 +2489,9 @@ class WidgetsLocalizationKn extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => '\u{cae}\u{cc7}\u{cb2}\u{cc6}\u{20}\u{ca8}\u{ccb}\u{ca1}\u{cbf}'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => '\u{c85}\u{c82}\u{c9f}\u{cbf}\u{cb8}\u{cbf}'; @@ -2282,6 +2513,9 @@ class WidgetsLocalizationKn extends GlobalWidgetsLocalizations { @override String get reorderItemUp => '\u{cae}\u{cc7}\u{cb2}\u{cc6}\u{20}\u{cb8}\u{cb0}\u{cbf}\u{cb8}\u{cbf}'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => '\u{cb5}\u{cc6}\u{cac}\u{ccd}\u{200c}\u{ca8}\u{cb2}\u{ccd}\u{cb2}\u{cbf}\u{20}\u{cb9}\u{cc1}\u{ca1}\u{cc1}\u{c95}\u{cbf}'; @@ -2308,6 +2542,9 @@ class WidgetsLocalizationKo extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => '찾기'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => '붙여넣기'; @@ -2329,6 +2566,9 @@ class WidgetsLocalizationKo extends GlobalWidgetsLocalizations { @override String get reorderItemUp => '위로 이동'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => '웹 검색'; @@ -2355,6 +2595,9 @@ class WidgetsLocalizationKy extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Издөө'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Чаптоо'; @@ -2376,6 +2619,9 @@ class WidgetsLocalizationKy extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Жогору жылдыруу'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Интернеттен издөө'; @@ -2402,6 +2648,9 @@ class WidgetsLocalizationLo extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'ຊອກຫາຂໍ້ມູນ'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'ວາງ'; @@ -2423,6 +2672,9 @@ class WidgetsLocalizationLo extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ຍ້າຍຂຶ້ນ'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ຊອກຫາຢູ່ອິນເຕີເນັດ'; @@ -2449,6 +2701,9 @@ class WidgetsLocalizationLt extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Ieškoti'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Įklijuoti'; @@ -2470,6 +2725,9 @@ class WidgetsLocalizationLt extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Perkelti aukštyn'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Ieškoti žiniatinklyje'; @@ -2496,6 +2754,9 @@ class WidgetsLocalizationLv extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Meklēt'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Ielīmēt'; @@ -2517,6 +2778,9 @@ class WidgetsLocalizationLv extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Pārvietot uz augšu'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Meklēt tīmeklī'; @@ -2543,6 +2807,9 @@ class WidgetsLocalizationMk extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Погледнете нагоре'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Залепи'; @@ -2564,6 +2831,9 @@ class WidgetsLocalizationMk extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Преместете нагоре'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Пребарајте на интернет'; @@ -2590,6 +2860,9 @@ class WidgetsLocalizationMl extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'മുകളിലേക്ക് നോക്കുക'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'ഒട്ടിക്കുക'; @@ -2611,6 +2884,9 @@ class WidgetsLocalizationMl extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'മുകളിലോട്ട് നീക്കുക'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'വെബിൽ തിരയുക'; @@ -2637,6 +2913,9 @@ class WidgetsLocalizationMn extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Дээшээ харах'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Буулгах'; @@ -2658,6 +2937,9 @@ class WidgetsLocalizationMn extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Дээш зөөх'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Вебээс хайх'; @@ -2684,6 +2966,9 @@ class WidgetsLocalizationMr extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'शोध घ्या'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'पेस्ट करा'; @@ -2705,6 +2990,9 @@ class WidgetsLocalizationMr extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'वर हलवा'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'वेबवर शोधा'; @@ -2731,6 +3019,9 @@ class WidgetsLocalizationMs extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Lihat ke Atas'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Tampal'; @@ -2752,6 +3043,9 @@ class WidgetsLocalizationMs extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Alih ke atas'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Buat carian pada Web'; @@ -2778,6 +3072,9 @@ class WidgetsLocalizationMy extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'အပေါ်ကြည့်ရန်'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'ကူးထည့်ရန်'; @@ -2799,6 +3096,9 @@ class WidgetsLocalizationMy extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'အပေါ်သို့ ရွှေ့ရန်'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ဝဘ်တွင်ရှာရန်'; @@ -2825,6 +3125,9 @@ class WidgetsLocalizationNb extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Slå opp'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Lim inn'; @@ -2846,6 +3149,9 @@ class WidgetsLocalizationNb extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Flytt opp'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Søk på nettet'; @@ -2872,6 +3178,9 @@ class WidgetsLocalizationNe extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'माथितिर हेर्नुहोस्'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'टाँस्नुहोस्'; @@ -2893,6 +3202,9 @@ class WidgetsLocalizationNe extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'माथि सार्नुहोस्'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'वेबमा खोज्नुहोस्'; @@ -2919,6 +3231,9 @@ class WidgetsLocalizationNl extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Opzoeken'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Plakken'; @@ -2940,6 +3255,9 @@ class WidgetsLocalizationNl extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Omhoog verplaatsen'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Op internet zoeken'; @@ -2966,6 +3284,9 @@ class WidgetsLocalizationNo extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Slå opp'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Lim inn'; @@ -2987,6 +3308,9 @@ class WidgetsLocalizationNo extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Flytt opp'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Søk på nettet'; @@ -3013,6 +3337,9 @@ class WidgetsLocalizationOr extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'ଉପରକୁ ଦେଖନ୍ତୁ'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'ପେଷ୍ଟ କରନ୍ତୁ'; @@ -3034,6 +3361,9 @@ class WidgetsLocalizationOr extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ଉପରକୁ ନିଅନ୍ତୁ'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ୱେବ ସର୍ଚ୍ଚ କରନ୍ତୁ'; @@ -3060,6 +3390,9 @@ class WidgetsLocalizationPa extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'ਖੋਜੋ'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'ਪੇਸਟ ਕਰੋ'; @@ -3081,6 +3414,9 @@ class WidgetsLocalizationPa extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ਉੱਪਰ ਲਿਜਾਓ'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => "ਵੈੱਬ 'ਤੇ ਖੋਜੋ"; @@ -3107,6 +3443,9 @@ class WidgetsLocalizationPl extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Sprawdź'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Wklej'; @@ -3128,6 +3467,9 @@ class WidgetsLocalizationPl extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Przenieś w górę'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Szukaj w internecie'; @@ -3154,6 +3496,9 @@ class WidgetsLocalizationPs extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Look Up'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'پیټ کړئ'; @@ -3175,6 +3520,9 @@ class WidgetsLocalizationPs extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Move up'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Search Web'; @@ -3201,6 +3549,9 @@ class WidgetsLocalizationPt extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Pesquisar'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Colar'; @@ -3222,6 +3573,9 @@ class WidgetsLocalizationPt extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Mover para cima'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Pesquisar na Web'; @@ -3265,6 +3619,9 @@ class WidgetsLocalizationRo extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Privire în sus'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Inserați'; @@ -3286,6 +3643,9 @@ class WidgetsLocalizationRo extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Mutați în sus'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Căutați pe web'; @@ -3312,6 +3672,9 @@ class WidgetsLocalizationRu extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Найти'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Вставить'; @@ -3333,6 +3696,9 @@ class WidgetsLocalizationRu extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Переместить вверх'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Искать в интернете'; @@ -3359,6 +3725,9 @@ class WidgetsLocalizationSi extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'උඩ බලන්න'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'අලවන්න'; @@ -3380,6 +3749,9 @@ class WidgetsLocalizationSi extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ඉහළට ගෙන යන්න'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'වෙබය සොයන්න'; @@ -3406,6 +3778,9 @@ class WidgetsLocalizationSk extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Pohľad nahor'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Prilepiť'; @@ -3427,6 +3802,9 @@ class WidgetsLocalizationSk extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Presunúť nahor'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Hľadať na webe'; @@ -3453,6 +3831,9 @@ class WidgetsLocalizationSl extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Pogled gor'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Prilepi'; @@ -3474,6 +3855,9 @@ class WidgetsLocalizationSl extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Premakni navzgor'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Iskanje v spletu'; @@ -3500,6 +3884,9 @@ class WidgetsLocalizationSq extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Kërko'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Ngjit'; @@ -3521,6 +3908,9 @@ class WidgetsLocalizationSq extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Lëvize lart'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Kërko në ueb'; @@ -3547,6 +3937,9 @@ class WidgetsLocalizationSr extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Поглед нагоре'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Налепи'; @@ -3568,6 +3961,9 @@ class WidgetsLocalizationSr extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Померите нагоре'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Претражи веб'; @@ -3649,6 +4045,9 @@ class WidgetsLocalizationSv extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Titta upp'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Klistra in'; @@ -3670,6 +4069,9 @@ class WidgetsLocalizationSv extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Flytta uppåt'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Sök på webben'; @@ -3696,6 +4098,9 @@ class WidgetsLocalizationSw extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Tafuta'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Bandika'; @@ -3717,6 +4122,9 @@ class WidgetsLocalizationSw extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Sogeza juu'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Tafuta kwenye Wavuti'; @@ -3743,6 +4151,9 @@ class WidgetsLocalizationTa extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'தேடு'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'ஒட்டு'; @@ -3764,6 +4175,9 @@ class WidgetsLocalizationTa extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'மேலே நகர்த்தவும்'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'இணையத்தில் தேடு'; @@ -3790,6 +4204,9 @@ class WidgetsLocalizationTe extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'వెతకండి'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'పేస్ట్ చేయండి'; @@ -3811,6 +4228,9 @@ class WidgetsLocalizationTe extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'పైకి జరపండి'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'వెబ్‌లో సెర్చ్ చేయండి'; @@ -3837,6 +4257,9 @@ class WidgetsLocalizationTh extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'ค้นหา'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'วาง'; @@ -3858,6 +4281,9 @@ class WidgetsLocalizationTh extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'ย้ายขึ้น'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ค้นหาบนอินเทอร์เน็ต'; @@ -3884,6 +4310,9 @@ class WidgetsLocalizationTl extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Tumingin sa Itaas'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'I-paste'; @@ -3905,6 +4334,9 @@ class WidgetsLocalizationTl extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Ilipat pataas'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Maghanap sa Web'; @@ -3931,6 +4363,9 @@ class WidgetsLocalizationTr extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Ara'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Yapıştır'; @@ -3952,6 +4387,9 @@ class WidgetsLocalizationTr extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Yukarı taşı'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => "Web'de Ara"; @@ -3978,6 +4416,9 @@ class WidgetsLocalizationUk extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Шукати'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Вставити'; @@ -3999,6 +4440,9 @@ class WidgetsLocalizationUk extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Перемістити вгору'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Пошук в Інтернеті'; @@ -4025,6 +4469,9 @@ class WidgetsLocalizationUr extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'تفصیل دیکھیں'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'پیسٹ کریں'; @@ -4046,6 +4493,9 @@ class WidgetsLocalizationUr extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'اوپر منتقل کریں'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'ویب تلاش کریں'; @@ -4072,6 +4522,9 @@ class WidgetsLocalizationUz extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Tepaga qarang'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Joylash'; @@ -4093,6 +4546,9 @@ class WidgetsLocalizationUz extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Tepaga siljitish'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Internetdan qidirish'; @@ -4119,6 +4575,9 @@ class WidgetsLocalizationVi extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Tra cứu'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Dán'; @@ -4140,6 +4599,9 @@ class WidgetsLocalizationVi extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Di chuyển lên'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Tìm kiếm trên web'; @@ -4166,6 +4628,9 @@ class WidgetsLocalizationZh extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => '查询'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => '粘贴'; @@ -4187,6 +4652,9 @@ class WidgetsLocalizationZh extends GlobalWidgetsLocalizations { @override String get reorderItemUp => '上移'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => '搜索'; @@ -4290,6 +4758,9 @@ class WidgetsLocalizationZu extends GlobalWidgetsLocalizations { @override String get lookUpButtonLabel => 'Bheka Phezulu'; + @override + String get noResultsFound => 'No results found'; + @override String get pasteButtonLabel => 'Namathisela'; @@ -4311,6 +4782,9 @@ class WidgetsLocalizationZu extends GlobalWidgetsLocalizations { @override String get reorderItemUp => 'Iya phezulu'; + @override + String get searchResultsFound => 'Search results found'; + @override String get searchWebButtonLabel => 'Sesha Iwebhu'; diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_af.arb b/packages/flutter_localizations/lib/src/l10n/widgets_af.arb index 84d974930cdce..fd168c7613c90 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_af.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_af.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Deursoek web", "shareButtonLabel": "Deel", "pasteButtonLabel": "Plak", - "selectAllButtonLabel": "Kies alles" + "selectAllButtonLabel": "Kies alles", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_am.arb b/packages/flutter_localizations/lib/src/l10n/widgets_am.arb index 713426c2c0c1e..277a24fe2ba80 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_am.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_am.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ድርን ፈልግ", "shareButtonLabel": "አጋራ", "pasteButtonLabel": "ለጥፍ", - "selectAllButtonLabel": "ሁሉንም ምረጥ" + "selectAllButtonLabel": "ሁሉንም ምረጥ", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ar.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ar.arb index 26dd4388cf43b..6ddb1fe0a0dac 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ar.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ar.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "البحث على الويب", "shareButtonLabel": "مشاركة", "pasteButtonLabel": "لصق", - "selectAllButtonLabel": "اختيار الكل" + "selectAllButtonLabel": "اختيار الكل", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_as.arb b/packages/flutter_localizations/lib/src/l10n/widgets_as.arb index 020cdb10d8bd7..0a82b27fb641b 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_as.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_as.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ৱেবত সন্ধান কৰক", "shareButtonLabel": "শ্বেয়াৰ কৰক", "pasteButtonLabel": "পে'ষ্ট কৰক", - "selectAllButtonLabel": "সকলো বাছনি কৰক" + "selectAllButtonLabel": "সকলো বাছনি কৰক", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_az.arb b/packages/flutter_localizations/lib/src/l10n/widgets_az.arb index 152c332e292db..b462d53ed2e2a 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_az.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_az.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Vebdə axtarın", "shareButtonLabel": "Paylaşın", "pasteButtonLabel": "Yerləşdirin", - "selectAllButtonLabel": "Hamısını seçin" + "selectAllButtonLabel": "Hamısını seçin", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_be.arb b/packages/flutter_localizations/lib/src/l10n/widgets_be.arb index 025af98c12380..983d33b539a9c 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_be.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_be.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Пошук у сетцы", "shareButtonLabel": "Абагуліць", "pasteButtonLabel": "Уставіць", - "selectAllButtonLabel": "Выбраць усе" + "selectAllButtonLabel": "Выбраць усе", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_bg.arb b/packages/flutter_localizations/lib/src/l10n/widgets_bg.arb index aa318a0a60a64..f71ca03b3f599 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_bg.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_bg.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Търсене в мрежата", "shareButtonLabel": "Споделяне", "pasteButtonLabel": "Поставяне", - "selectAllButtonLabel": "Избиране на всички" + "selectAllButtonLabel": "Избиране на всички", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_bn.arb b/packages/flutter_localizations/lib/src/l10n/widgets_bn.arb index e75401fda3f3f..f2ae4022e7dad 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_bn.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_bn.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ওয়েবে সার্চ করুন", "shareButtonLabel": "শেয়ার করুন", "pasteButtonLabel": "পেস্ট করুন", - "selectAllButtonLabel": "সব বেছে নিন" + "selectAllButtonLabel": "সব বেছে নিন", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_bs.arb b/packages/flutter_localizations/lib/src/l10n/widgets_bs.arb index 0a16f92b4502c..104705f40f464 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_bs.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_bs.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Pretraži Web", "shareButtonLabel": "Dijeli", "pasteButtonLabel": "Zalijepi", - "selectAllButtonLabel": "Odaberi sve" + "selectAllButtonLabel": "Odaberi sve", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ca.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ca.arb index c35d4b0bcd505..758f3810f4f3b 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ca.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ca.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Cerca al web", "shareButtonLabel": "Comparteix", "pasteButtonLabel": "Enganxa", - "selectAllButtonLabel": "Selecciona-ho tot" + "selectAllButtonLabel": "Selecciona-ho tot", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_cs.arb b/packages/flutter_localizations/lib/src/l10n/widgets_cs.arb index 762fd3e767d1e..9e026378c6096 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_cs.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_cs.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Vyhledávat na webu", "shareButtonLabel": "Sdílet", "pasteButtonLabel": "Vložit", - "selectAllButtonLabel": "Vybrat vše" + "selectAllButtonLabel": "Vybrat vše", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_cy.arb b/packages/flutter_localizations/lib/src/l10n/widgets_cy.arb index a827c0cf899e0..633dea567d32e 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_cy.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_cy.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Chwilio'r We", "shareButtonLabel": "Rhannu", "pasteButtonLabel": "Gludo", - "selectAllButtonLabel": "Dewis y Cyfan" + "selectAllButtonLabel": "Dewis y Cyfan", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_da.arb b/packages/flutter_localizations/lib/src/l10n/widgets_da.arb index 3af378ea2910b..59ac66557bc5c 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_da.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_da.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Søg på nettet", "shareButtonLabel": "Del", "pasteButtonLabel": "Indsæt", - "selectAllButtonLabel": "Markér alt" + "selectAllButtonLabel": "Markér alt", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_de.arb b/packages/flutter_localizations/lib/src/l10n/widgets_de.arb index add0d1b7e74ef..e1381d9e125b7 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_de.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_de.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Im Web suchen", "shareButtonLabel": "Teilen", "pasteButtonLabel": "Einsetzen", - "selectAllButtonLabel": "Alle auswählen" + "selectAllButtonLabel": "Alle auswählen", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_el.arb b/packages/flutter_localizations/lib/src/l10n/widgets_el.arb index 11d077ad30f13..90b2806bc8bf1 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_el.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_el.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Αναζήτηση στον ιστό", "shareButtonLabel": "Κοινή χρήση", "pasteButtonLabel": "Επικόλληση", - "selectAllButtonLabel": "Επιλογή όλων" + "selectAllButtonLabel": "Επιλογή όλων", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_en.arb b/packages/flutter_localizations/lib/src/l10n/widgets_en.arb index f23acd3830230..43bacced03955 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_en.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_en.arb @@ -11,6 +11,10 @@ "@reorderItemLeft": {"description":"The audio announcement to move an item in a Reorderable List left in the list when it is oriented horizontally."}, "reorderItemRight": "Move right", "@reorderItemRight": {"description":"The audio announcement to move an item in a Reorderable List right in the list when it is oriented horizontally."}, + "searchResultsFound": "Search results found", + "@searchResultsFound": {"description":"The audio announcement when Autocomplete search results become available."}, + "noResultsFound": "No results found", + "@noResultsFound": {"description":"The audio announcement when Autocomplete search results become unavailable."}, "copyButtonLabel": "Copy", "@copyButtonLabel": {"description":"The label for copy buttons and menu items."}, "cutButtonLabel": "Cut", diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_es.arb b/packages/flutter_localizations/lib/src/l10n/widgets_es.arb index 63a2b15006ea9..c089b33ae4315 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_es.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_es.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Buscar en la Web", "shareButtonLabel": "Compartir", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo" + "selectAllButtonLabel": "Seleccionar todo", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_et.arb b/packages/flutter_localizations/lib/src/l10n/widgets_et.arb index 6ddd8a681d461..be1d0a43f1da0 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_et.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_et.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Otsi veebist", "shareButtonLabel": "Jagamine", "pasteButtonLabel": "Kleebi", - "selectAllButtonLabel": "Vali kõik" + "selectAllButtonLabel": "Vali kõik", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_eu.arb b/packages/flutter_localizations/lib/src/l10n/widgets_eu.arb index a6f2bf5c53e2e..abc645842ff95 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_eu.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_eu.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Bilatu sarean", "shareButtonLabel": "Partekatu", "pasteButtonLabel": "Itsatsi", - "selectAllButtonLabel": "Hautatu guztiak" + "selectAllButtonLabel": "Hautatu guztiak", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_fa.arb b/packages/flutter_localizations/lib/src/l10n/widgets_fa.arb index f73d1f66fd34e..e398c8a981a45 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_fa.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_fa.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "جستجو در وب", "shareButtonLabel": "هم‌رسانی کردن", "pasteButtonLabel": "جای‌گذاری", - "selectAllButtonLabel": "انتخاب همه" + "selectAllButtonLabel": "انتخاب همه", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_fi.arb b/packages/flutter_localizations/lib/src/l10n/widgets_fi.arb index ea8f1b50de51c..2df7f360dfb3b 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_fi.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_fi.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Hae verkosta", "shareButtonLabel": "Jaa", "pasteButtonLabel": "Liitä", - "selectAllButtonLabel": "Valitse kaikki" + "selectAllButtonLabel": "Valitse kaikki", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_fil.arb b/packages/flutter_localizations/lib/src/l10n/widgets_fil.arb index b566bb5e3ba60..e18e01cab9eff 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_fil.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_fil.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Maghanap sa Web", "shareButtonLabel": "I-share", "pasteButtonLabel": "I-paste", - "selectAllButtonLabel": "Piliin lahat" + "selectAllButtonLabel": "Piliin lahat", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_fr.arb b/packages/flutter_localizations/lib/src/l10n/widgets_fr.arb index 8c656b35791bd..959b4ac5a7609 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_fr.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_fr.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Rechercher sur le Web", "shareButtonLabel": "Partager", "pasteButtonLabel": "Coller", - "selectAllButtonLabel": "Tout sélectionner" + "selectAllButtonLabel": "Tout sélectionner", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_gl.arb b/packages/flutter_localizations/lib/src/l10n/widgets_gl.arb index 286051df4eb43..cae496aae1aff 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_gl.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_gl.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Buscar na Web", "shareButtonLabel": "Compartir", "pasteButtonLabel": "Pegar", - "selectAllButtonLabel": "Seleccionar todo" + "selectAllButtonLabel": "Seleccionar todo", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_gsw.arb b/packages/flutter_localizations/lib/src/l10n/widgets_gsw.arb index add0d1b7e74ef..e1381d9e125b7 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_gsw.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_gsw.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Im Web suchen", "shareButtonLabel": "Teilen", "pasteButtonLabel": "Einsetzen", - "selectAllButtonLabel": "Alle auswählen" + "selectAllButtonLabel": "Alle auswählen", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_gu.arb b/packages/flutter_localizations/lib/src/l10n/widgets_gu.arb index a6adb2d5b8f46..113c0bfb3bfd0 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_gu.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_gu.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "વેબ પર શોધો", "shareButtonLabel": "શેર કરો", "pasteButtonLabel": "પેસ્ટ કરો", - "selectAllButtonLabel": "બધા પસંદ કરો" + "selectAllButtonLabel": "બધા પસંદ કરો", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_he.arb b/packages/flutter_localizations/lib/src/l10n/widgets_he.arb index c3dcd5beccf43..be9ed03429642 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_he.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_he.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "חיפוש באינטרנט", "shareButtonLabel": "שיתוף", "pasteButtonLabel": "הדבקה", - "selectAllButtonLabel": "בחירת הכול" + "selectAllButtonLabel": "בחירת הכול", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_hi.arb b/packages/flutter_localizations/lib/src/l10n/widgets_hi.arb index 756909a1cda64..60d3f6d4fdaf8 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_hi.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_hi.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "वेब पर खोजें", "shareButtonLabel": "शेयर करें", "pasteButtonLabel": "चिपकाएं", - "selectAllButtonLabel": "सभी को चुनें" + "selectAllButtonLabel": "सभी को चुनें", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_hr.arb b/packages/flutter_localizations/lib/src/l10n/widgets_hr.arb index a8a229fa5b4db..654214813e67d 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_hr.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_hr.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Pretraži web", "shareButtonLabel": "Dijeli", "pasteButtonLabel": "Zalijepi", - "selectAllButtonLabel": "Odaberi sve" + "selectAllButtonLabel": "Odaberi sve", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_hu.arb b/packages/flutter_localizations/lib/src/l10n/widgets_hu.arb index 3509d742f5273..b8206e5486c9e 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_hu.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_hu.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Keresés az interneten", "shareButtonLabel": "Megosztás", "pasteButtonLabel": "Beillesztés", - "selectAllButtonLabel": "Összes kijelölése" + "selectAllButtonLabel": "Összes kijelölése", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_hy.arb b/packages/flutter_localizations/lib/src/l10n/widgets_hy.arb index 0128ee1149291..a608c050e28a7 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_hy.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_hy.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Որոնել համացանցում", "shareButtonLabel": "Կիսվել", "pasteButtonLabel": "Տեղադրել", - "selectAllButtonLabel": "Նշել բոլորը" + "selectAllButtonLabel": "Նշել բոլորը", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_id.arb b/packages/flutter_localizations/lib/src/l10n/widgets_id.arb index a43523bb22e98..e6e21869c0782 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_id.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_id.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Telusuri di Web", "shareButtonLabel": "Bagikan", "pasteButtonLabel": "Tempel", - "selectAllButtonLabel": "Pilih semua" + "selectAllButtonLabel": "Pilih semua", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_is.arb b/packages/flutter_localizations/lib/src/l10n/widgets_is.arb index a85afe02a70a0..2670e9e7bacb2 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_is.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_is.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Leita á vefnum", "shareButtonLabel": "Deila", "pasteButtonLabel": "Líma", - "selectAllButtonLabel": "Velja allt" + "selectAllButtonLabel": "Velja allt", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_it.arb b/packages/flutter_localizations/lib/src/l10n/widgets_it.arb index 5e6235218e3b2..ee59fe281fa0a 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_it.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_it.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Cerca sul web", "shareButtonLabel": "Condividi", "pasteButtonLabel": "Incolla", - "selectAllButtonLabel": "Seleziona tutto" + "selectAllButtonLabel": "Seleziona tutto", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ja.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ja.arb index 640ce147e79fb..b34d90b49c676 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ja.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ja.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ウェブを検索", "shareButtonLabel": "共有", "pasteButtonLabel": "貼り付け", - "selectAllButtonLabel": "すべてを選択" + "selectAllButtonLabel": "すべてを選択", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ka.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ka.arb index e48f53fdf5480..2d875240dd9b5 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ka.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ka.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ვებში ძიება", "shareButtonLabel": "გაზიარება", "pasteButtonLabel": "ჩასმა", - "selectAllButtonLabel": "ყველას არჩევა" + "selectAllButtonLabel": "ყველას არჩევა", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_kk.arb b/packages/flutter_localizations/lib/src/l10n/widgets_kk.arb index 86b6636f4acdb..f84b350eaa931 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_kk.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_kk.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Интернеттен іздеу", "shareButtonLabel": "Бөлісу", "pasteButtonLabel": "Қою", - "selectAllButtonLabel": "Барлығын таңдау" + "selectAllButtonLabel": "Барлығын таңдау", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_km.arb b/packages/flutter_localizations/lib/src/l10n/widgets_km.arb index 90468610ea94d..58a89f8e5b895 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_km.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_km.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ស្វែងរក​លើបណ្ដាញ", "shareButtonLabel": "ចែករំលែក", "pasteButtonLabel": "ដាក់​ចូល", - "selectAllButtonLabel": "ជ្រើសរើស​ទាំងអស់" + "selectAllButtonLabel": "ជ្រើសរើស​ទាំងអស់", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_kn.arb b/packages/flutter_localizations/lib/src/l10n/widgets_kn.arb index e0639b1ba52a2..ed8ff5ef7d81a 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_kn.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_kn.arb @@ -11,5 +11,9 @@ "searchWebButtonLabel": "\u0cb5\u0cc6\u0cac\u0ccd\u200c\u0ca8\u0cb2\u0ccd\u0cb2\u0cbf\u0020\u0cb9\u0cc1\u0ca1\u0cc1\u0c95\u0cbf", "shareButtonLabel": "\u0cb9\u0c82\u0c9a\u0cbf\u0c95\u0cca\u0cb3\u0ccd\u0cb3\u0cbf", "pasteButtonLabel": "\u0c85\u0c82\u0c9f\u0cbf\u0cb8\u0cbf", - "selectAllButtonLabel": "\u0c8e\u0cb2\u0ccd\u0cb2\u0cb5\u0ca8\u0ccd\u0ca8\u0cc2\u0020\u0c86\u0caf\u0ccd\u0c95\u0cc6\u0020\u0cae\u0cbe\u0ca1\u0cbf" + "selectAllButtonLabel": "\u0c8e\u0cb2\u0ccd\u0cb2\u0cb5\u0ca8\u0ccd\u0ca8\u0cc2\u0020\u0c86\u0caf\u0ccd\u0c95\u0cc6\u0020\u0cae\u0cbe\u0ca1\u0cbf", + "searchResultsAvailable": "\u0053\u0065\u0061\u0072\u0063\u0068\u0020\u0072\u0065\u0073\u0075\u006c\u0074\u0073\u0020\u0061\u0076\u0061\u0069\u006c\u0061\u0062\u006c\u0065", + "searchResultsUnavailable": "\u0053\u0065\u0061\u0072\u0063\u0068\u0020\u0072\u0065\u0073\u0075\u006c\u0074\u0073\u0020\u006e\u006f\u0074\u0020\u0061\u0076\u0061\u0069\u006c\u0061\u0062\u006c\u0065", + "searchResultsFound": "\u0053\u0065\u0061\u0072\u0063\u0068\u0020\u0072\u0065\u0073\u0075\u006c\u0074\u0073\u0020\u0066\u006f\u0075\u006e\u0064", + "noResultsFound": "\u004e\u006f\u0020\u0072\u0065\u0073\u0075\u006c\u0074\u0073\u0020\u0066\u006f\u0075\u006e\u0064" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ko.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ko.arb index 50c89e8f68cae..9deed6186a2e3 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ko.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ko.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "웹 검색", "shareButtonLabel": "공유", "pasteButtonLabel": "붙여넣기", - "selectAllButtonLabel": "전체 선택" + "selectAllButtonLabel": "전체 선택", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ky.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ky.arb index 22c168d49cfab..b14a2e3752c32 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ky.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ky.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Интернеттен издөө", "shareButtonLabel": "Бөлүшүү", "pasteButtonLabel": "Чаптоо", - "selectAllButtonLabel": "Баарын тандоо" + "selectAllButtonLabel": "Баарын тандоо", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_lo.arb b/packages/flutter_localizations/lib/src/l10n/widgets_lo.arb index 7ae02a952898f..ca8f6bf073027 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_lo.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_lo.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ຊອກຫາຢູ່ອິນເຕີເນັດ", "shareButtonLabel": "ແບ່ງປັນ", "pasteButtonLabel": "ວາງ", - "selectAllButtonLabel": "ເລືອກທັງໝົດ" + "selectAllButtonLabel": "ເລືອກທັງໝົດ", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_lt.arb b/packages/flutter_localizations/lib/src/l10n/widgets_lt.arb index 79c5aecc7c316..4966c13af6704 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_lt.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_lt.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Ieškoti žiniatinklyje", "shareButtonLabel": "Bendrinti", "pasteButtonLabel": "Įklijuoti", - "selectAllButtonLabel": "Pasirinkti viską" + "selectAllButtonLabel": "Pasirinkti viską", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_lv.arb b/packages/flutter_localizations/lib/src/l10n/widgets_lv.arb index f0fba2f7cd5b3..9abffb0dde6ca 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_lv.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_lv.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Meklēt tīmeklī", "shareButtonLabel": "Kopīgot", "pasteButtonLabel": "Ielīmēt", - "selectAllButtonLabel": "Atlasīt visu" + "selectAllButtonLabel": "Atlasīt visu", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_mk.arb b/packages/flutter_localizations/lib/src/l10n/widgets_mk.arb index 1964b6fd29e42..e3a604ec0c162 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_mk.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_mk.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Пребарајте на интернет", "shareButtonLabel": "Сподели", "pasteButtonLabel": "Залепи", - "selectAllButtonLabel": "Избери ги сите" + "selectAllButtonLabel": "Избери ги сите", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ml.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ml.arb index 46aef5961518a..945f7ba42cb4d 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ml.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ml.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "വെബിൽ തിരയുക", "shareButtonLabel": "പങ്കിടുക", "pasteButtonLabel": "ഒട്ടിക്കുക", - "selectAllButtonLabel": "എല്ലാം തിരഞ്ഞെടുക്കുക" + "selectAllButtonLabel": "എല്ലാം തിരഞ്ഞെടുക്കുക", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_mn.arb b/packages/flutter_localizations/lib/src/l10n/widgets_mn.arb index 8003eaad8308d..d7fcee2bbf3c9 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_mn.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_mn.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Вебээс хайх", "shareButtonLabel": "Хуваалцах", "pasteButtonLabel": "Буулгах", - "selectAllButtonLabel": "Бүгдийг сонгох" + "selectAllButtonLabel": "Бүгдийг сонгох", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_mr.arb b/packages/flutter_localizations/lib/src/l10n/widgets_mr.arb index 3c131890c95d7..55659f2949aa1 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_mr.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_mr.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "वेबवर शोधा", "shareButtonLabel": "शेअर करा", "pasteButtonLabel": "पेस्ट करा", - "selectAllButtonLabel": "सर्व निवडा" + "selectAllButtonLabel": "सर्व निवडा", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ms.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ms.arb index c852d61a9df39..db591b78df341 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ms.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ms.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Buat carian pada Web", "shareButtonLabel": "Kongsi", "pasteButtonLabel": "Tampal", - "selectAllButtonLabel": "Pilih semua" + "selectAllButtonLabel": "Pilih semua", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_my.arb b/packages/flutter_localizations/lib/src/l10n/widgets_my.arb index e5cac86a8f5f5..9d97190a54cfd 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_my.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_my.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ဝဘ်တွင်ရှာရန်", "shareButtonLabel": "မျှဝေရန်", "pasteButtonLabel": "ကူးထည့်ရန်", - "selectAllButtonLabel": "အားလုံး ရွေးရန်" + "selectAllButtonLabel": "အားလုံး ရွေးရန်", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_nb.arb b/packages/flutter_localizations/lib/src/l10n/widgets_nb.arb index 4904ad168b95e..87fbee82aefda 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_nb.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_nb.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Søk på nettet", "shareButtonLabel": "Del", "pasteButtonLabel": "Lim inn", - "selectAllButtonLabel": "Velg alle" + "selectAllButtonLabel": "Velg alle", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ne.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ne.arb index 62aa699ba64a2..9c6585c55d2f3 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ne.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ne.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "वेबमा खोज्नुहोस्", "shareButtonLabel": "सेयर गर्नुहोस्", "pasteButtonLabel": "टाँस्नुहोस्", - "selectAllButtonLabel": "सबै बटनहरू चयन गर्नुहोस्" + "selectAllButtonLabel": "सबै बटनहरू चयन गर्नुहोस्", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_nl.arb b/packages/flutter_localizations/lib/src/l10n/widgets_nl.arb index cbd8a91ff4417..d64c99b49e51c 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_nl.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_nl.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Op internet zoeken", "shareButtonLabel": "Delen", "pasteButtonLabel": "Plakken", - "selectAllButtonLabel": "Alles selecteren" + "selectAllButtonLabel": "Alles selecteren", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_no.arb b/packages/flutter_localizations/lib/src/l10n/widgets_no.arb index 4904ad168b95e..87fbee82aefda 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_no.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_no.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Søk på nettet", "shareButtonLabel": "Del", "pasteButtonLabel": "Lim inn", - "selectAllButtonLabel": "Velg alle" + "selectAllButtonLabel": "Velg alle", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_or.arb b/packages/flutter_localizations/lib/src/l10n/widgets_or.arb index a0ab09fac4abf..5243ef0d1a2ed 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_or.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_or.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ୱେବ ସର୍ଚ୍ଚ କରନ୍ତୁ", "shareButtonLabel": "ସେୟାର କରନ୍ତୁ", "pasteButtonLabel": "ପେଷ୍ଟ କରନ୍ତୁ", - "selectAllButtonLabel": "ସବୁ ଚୟନ କରନ୍ତୁ" + "selectAllButtonLabel": "ସବୁ ଚୟନ କରନ୍ତୁ", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_pa.arb b/packages/flutter_localizations/lib/src/l10n/widgets_pa.arb index 4a58293306563..ebc13e96c413b 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_pa.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_pa.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ਵੈੱਬ 'ਤੇ ਖੋਜੋ", "shareButtonLabel": "ਸਾਂਝਾ ਕਰੋ", "pasteButtonLabel": "ਪੇਸਟ ਕਰੋ", - "selectAllButtonLabel": "ਸਭ ਚੁਣੋ" + "selectAllButtonLabel": "ਸਭ ਚੁਣੋ", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_pl.arb b/packages/flutter_localizations/lib/src/l10n/widgets_pl.arb index d4955bf836b83..a8040c5b0bbf5 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_pl.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_pl.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Szukaj w internecie", "shareButtonLabel": "Udostępnij", "pasteButtonLabel": "Wklej", - "selectAllButtonLabel": "Zaznacz wszystko" + "selectAllButtonLabel": "Zaznacz wszystko", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ps.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ps.arb index a7ba79d5bb57f..3f193edb7c946 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ps.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ps.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Search Web", "shareButtonLabel": "Share...", "pasteButtonLabel": "پیټ کړئ", - "selectAllButtonLabel": "غوره کړئ" + "selectAllButtonLabel": "غوره کړئ", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_pt.arb b/packages/flutter_localizations/lib/src/l10n/widgets_pt.arb index 114b613a1a7b3..3a36859d676d3 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_pt.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_pt.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Pesquisar na Web", "shareButtonLabel": "Compartilhar", "pasteButtonLabel": "Colar", - "selectAllButtonLabel": "Selecionar tudo" + "selectAllButtonLabel": "Selecionar tudo", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ro.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ro.arb index b3c8194ffc41f..73043f96efee8 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ro.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ro.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Căutați pe web", "shareButtonLabel": "Trimiteți", "pasteButtonLabel": "Inserați", - "selectAllButtonLabel": "Selectați tot" + "selectAllButtonLabel": "Selectați tot", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ru.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ru.arb index 7db9d8302b6b8..05d61c01096ad 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ru.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ru.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Искать в интернете", "shareButtonLabel": "Поделиться", "pasteButtonLabel": "Вставить", - "selectAllButtonLabel": "Выбрать все" + "selectAllButtonLabel": "Выбрать все", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_si.arb b/packages/flutter_localizations/lib/src/l10n/widgets_si.arb index c7cc543f83075..791059355a685 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_si.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_si.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "වෙබය සොයන්න", "shareButtonLabel": "බෙදා ගන්න", "pasteButtonLabel": "අලවන්න", - "selectAllButtonLabel": "සියල්ල තෝරන්න" + "selectAllButtonLabel": "සියල්ල තෝරන්න", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_sk.arb b/packages/flutter_localizations/lib/src/l10n/widgets_sk.arb index 693c744f38725..3df13a85eac89 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_sk.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_sk.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Hľadať na webe", "shareButtonLabel": "Zdieľať", "pasteButtonLabel": "Prilepiť", - "selectAllButtonLabel": "Vybrať všetko" + "selectAllButtonLabel": "Vybrať všetko", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_sl.arb b/packages/flutter_localizations/lib/src/l10n/widgets_sl.arb index 9ba4978c89d3f..05bdbec2e11c9 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_sl.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_sl.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Iskanje v spletu", "shareButtonLabel": "Deli", "pasteButtonLabel": "Prilepi", - "selectAllButtonLabel": "Izberi vse" + "selectAllButtonLabel": "Izberi vse", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_sq.arb b/packages/flutter_localizations/lib/src/l10n/widgets_sq.arb index d0696f8583f1f..1e3998580ec4e 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_sq.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_sq.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Kërko në ueb", "shareButtonLabel": "Ndaj", "pasteButtonLabel": "Ngjit", - "selectAllButtonLabel": "Zgjidh të gjitha" + "selectAllButtonLabel": "Zgjidh të gjitha", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_sr.arb b/packages/flutter_localizations/lib/src/l10n/widgets_sr.arb index c73af4e8ee0d6..d2f2f3ad58998 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_sr.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_sr.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Претражи веб", "shareButtonLabel": "Дели", "pasteButtonLabel": "Налепи", - "selectAllButtonLabel": "Изабери све" + "selectAllButtonLabel": "Изабери све", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_sv.arb b/packages/flutter_localizations/lib/src/l10n/widgets_sv.arb index d2413b6a90f45..126262682b8e8 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_sv.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_sv.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Sök på webben", "shareButtonLabel": "Dela", "pasteButtonLabel": "Klistra in", - "selectAllButtonLabel": "Markera allt" + "selectAllButtonLabel": "Markera allt", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_sw.arb b/packages/flutter_localizations/lib/src/l10n/widgets_sw.arb index 39768d24aa40b..86a340e8ac489 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_sw.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_sw.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Tafuta kwenye Wavuti", "shareButtonLabel": "Tuma", "pasteButtonLabel": "Bandika", - "selectAllButtonLabel": "Chagua vyote" + "selectAllButtonLabel": "Chagua vyote", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ta.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ta.arb index 95938d222dd2d..5e73bf133a139 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ta.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ta.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "இணையத்தில் தேடு", "shareButtonLabel": "பகிர்", "pasteButtonLabel": "ஒட்டு", - "selectAllButtonLabel": "அனைத்தையும் தேர்ந்தெடு" + "selectAllButtonLabel": "அனைத்தையும் தேர்ந்தெடு", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_te.arb b/packages/flutter_localizations/lib/src/l10n/widgets_te.arb index 407f592c391ce..837576fc8297a 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_te.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_te.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "వెబ్‌లో సెర్చ్ చేయండి", "shareButtonLabel": "షేర్ చేయండి", "pasteButtonLabel": "పేస్ట్ చేయండి", - "selectAllButtonLabel": "అన్నింటినీ ఎంచుకోండి" + "selectAllButtonLabel": "అన్నింటినీ ఎంచుకోండి", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_th.arb b/packages/flutter_localizations/lib/src/l10n/widgets_th.arb index e9236b5064bcf..d37a0f40d4c14 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_th.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_th.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ค้นหาบนอินเทอร์เน็ต", "shareButtonLabel": "แชร์", "pasteButtonLabel": "วาง", - "selectAllButtonLabel": "เลือกทั้งหมด" + "selectAllButtonLabel": "เลือกทั้งหมด", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_tl.arb b/packages/flutter_localizations/lib/src/l10n/widgets_tl.arb index b566bb5e3ba60..e18e01cab9eff 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_tl.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_tl.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Maghanap sa Web", "shareButtonLabel": "I-share", "pasteButtonLabel": "I-paste", - "selectAllButtonLabel": "Piliin lahat" + "selectAllButtonLabel": "Piliin lahat", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_tr.arb b/packages/flutter_localizations/lib/src/l10n/widgets_tr.arb index ffa7f72fb7c8d..4d6d1b272f6e2 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_tr.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_tr.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Web'de Ara", "shareButtonLabel": "Paylaş", "pasteButtonLabel": "Yapıştır", - "selectAllButtonLabel": "Tümünü seç" + "selectAllButtonLabel": "Tümünü seç", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_uk.arb b/packages/flutter_localizations/lib/src/l10n/widgets_uk.arb index 9a7d59b756297..e685898d4931d 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_uk.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_uk.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Пошук в Інтернеті", "shareButtonLabel": "Поділитися", "pasteButtonLabel": "Вставити", - "selectAllButtonLabel": "Вибрати всі" + "selectAllButtonLabel": "Вибрати всі", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_ur.arb b/packages/flutter_localizations/lib/src/l10n/widgets_ur.arb index e7bae3b02deba..5bc2ad830b058 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_ur.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_ur.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "ویب تلاش کریں", "shareButtonLabel": "اشتراک کریں", "pasteButtonLabel": "پیسٹ کریں", - "selectAllButtonLabel": "سبھی کو منتخب کریں" + "selectAllButtonLabel": "سبھی کو منتخب کریں", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_uz.arb b/packages/flutter_localizations/lib/src/l10n/widgets_uz.arb index 5a314605580aa..cae0b430e47d8 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_uz.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_uz.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Internetdan qidirish", "shareButtonLabel": "Ulashish", "pasteButtonLabel": "Joylash", - "selectAllButtonLabel": "Hammasi" + "selectAllButtonLabel": "Hammasi", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_vi.arb b/packages/flutter_localizations/lib/src/l10n/widgets_vi.arb index 2de10788ef5b1..21beef831ea89 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_vi.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_vi.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Tìm kiếm trên web", "shareButtonLabel": "Chia sẻ", "pasteButtonLabel": "Dán", - "selectAllButtonLabel": "Chọn tất cả" + "selectAllButtonLabel": "Chọn tất cả", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_zh.arb b/packages/flutter_localizations/lib/src/l10n/widgets_zh.arb index 542c1e719a7d3..78bc58788c220 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_zh.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_zh.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "搜索", "shareButtonLabel": "分享", "pasteButtonLabel": "粘贴", - "selectAllButtonLabel": "全选" + "selectAllButtonLabel": "全选", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } diff --git a/packages/flutter_localizations/lib/src/l10n/widgets_zu.arb b/packages/flutter_localizations/lib/src/l10n/widgets_zu.arb index e93485da5c51f..e2711cefcf7ed 100644 --- a/packages/flutter_localizations/lib/src/l10n/widgets_zu.arb +++ b/packages/flutter_localizations/lib/src/l10n/widgets_zu.arb @@ -11,5 +11,7 @@ "searchWebButtonLabel": "Sesha Iwebhu", "shareButtonLabel": "Yabelana", "pasteButtonLabel": "Namathisela", - "selectAllButtonLabel": "Khetha konke" + "selectAllButtonLabel": "Khetha konke", + "searchResultsFound": "Search results found", + "noResultsFound": "No results found" } From 20563f943f44edf6187a614a4ff903089e92ca66 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 14 Aug 2025 11:08:59 -0700 Subject: [PATCH 043/720] Sync `CHANGELOG.md` (3.35 -> `master`) (#173790) No changes, just syncing from the 3.35 branch. --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe70b596eef9..f92a713a72cb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,17 @@ Our goal is to make the list easy for them to scan. More information and tips: docs/releases/Hotfix-Documentation-Best-Practices.md --> + +## Flutter 3.35 Changes + +### [3.35.1](https://github.com/flutter/flutter/releases/tag/3.35.1) + +- [flutter/173785](https://github.com/flutter/flutter/issues/173785) - Fixes an issue that prevented downloading the Flutter SDK for Windows from `flutter.dev`. + +### [3.35.0](https://github.com/flutter/flutter/releases/tag/3.35.0) + +Initial stable release. + ## Flutter 3.32 Changes ### [3.32.8](https://github.com/flutter/flutter/releases/tag/3.32.8) From 03736a282ebb4bc319bcdde5205cbd81ab7bbd14 Mon Sep 17 00:00:00 2001 From: Dev TtangKong Date: Fri, 15 Aug 2025 03:13:54 +0900 Subject: [PATCH 044/720] Implements the Android native stretch effect as a fragment shader (Impeller-only). (#169293) > Sorry, Closing PR #169196 and reopening this in a new PR for clarity and a cleaner commit history. Fixes #82906 In the existing Flutter implementation, the Android stretch overscroll effect is achieved using Transform. However, this approach does not evenly stretch the screen as it does in the native Android environment. Therefore, in the Impeller environment, add or modify files to implement the effect using a fragment shader identical to the one used in native Android. > [!NOTE] > - The addition of a separate test file for StretchOverscrollEffect was not included because it would likely duplicate coverage already provided by the changes made in overscroll_stretch_indicator_test.dart within this PR. >> However, since determining whether stretching occurred by referencing global coordinates is currently considered impossible with the new Fragment Shader approach, the code was modified to partially exclude the relevant test logic in the Impeller. >> >> For reference, in the page_view_test.dart test, there was an issue with referencing the child Transform widget, but because the StretchOverscrollEffect widget is defined instead of the Transform widget in the Impeller environment, the code logic was adjusted accordingly. > > - Golden image tests were updated as the visual effect changes. ## Reference Source - https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/effects/StretchEffect.cpp ## `Old` Skia (Using Transform) https://github.com/user-attachments/assets/22d8ff96-d875-4722-bf6f-f0ad15b9077d ## `New` Impeller (Using fragment shader) https://github.com/user-attachments/assets/73b6ef18-06b2-42ea-9793-c391ec2ce282 ## 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 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 [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: Tong Mu Co-authored-by: Kate Lovett --- .../src/material/shaders/stretch_effect.frag | 159 ++ .../lib/src/widgets/overscroll_indicator.dart | 45 +- .../lib/src/widgets/stretch_effect.dart | 254 ++++ packages/flutter/lib/widgets.dart | 1 + .../overscroll_stretch_indicator_test.dart | 1319 +++++++++-------- .../flutter/test/widgets/page_view_test.dart | 20 +- .../test/widgets/stretch_effect_test.dart | 56 + packages/flutter_tools/lib/src/asset.dart | 2 +- .../test/general.shard/asset_bundle_test.dart | 44 +- .../test/general.shard/asset_test.dart | 60 +- 10 files changed, 1250 insertions(+), 710 deletions(-) create mode 100644 packages/flutter/lib/src/material/shaders/stretch_effect.frag create mode 100644 packages/flutter/lib/src/widgets/stretch_effect.dart create mode 100644 packages/flutter/test/widgets/stretch_effect_test.dart diff --git a/packages/flutter/lib/src/material/shaders/stretch_effect.frag b/packages/flutter/lib/src/material/shaders/stretch_effect.frag new file mode 100644 index 0000000000000..53be5a6971e1c --- /dev/null +++ b/packages/flutter/lib/src/material/shaders/stretch_effect.frag @@ -0,0 +1,159 @@ +#version 320 es +// 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. + +// This shader was created based on or with reference to the implementation found at: +// https://cs.android.com/android/platform/superproject/main/+/512046e84bcc51cc241bc6599f83ab345e93ab12:frameworks/base/libs/hwui/effects/StretchEffect.cpp + +#include + +uniform vec2 u_size; +uniform sampler2D u_texture; + +// Multiplier to apply to scale effect. +uniform float u_max_stretch_intensity; + +// Normalized overscroll amount in the horizontal direction. +uniform float u_overscroll_x; + +// Normalized overscroll amount in the vertical direction. +uniform float u_overscroll_y; + +// u_interpolation_strength is the intensity of the interpolation. +uniform float u_interpolation_strength; + +float ease_in(float t, float d) { + return t * d; +} + +float compute_overscroll_start( + float in_pos, + float overscroll, + float u_stretch_affected_dist, + float u_inverse_stretch_affected_dist, + float distance_stretched, + float interpolation_strength +) { + float offset_pos = u_stretch_affected_dist - in_pos; + float pos_based_variation = mix( + 1.0, + ease_in(offset_pos, u_inverse_stretch_affected_dist), + interpolation_strength + ); + float stretch_intensity = overscroll * pos_based_variation; + return distance_stretched - (offset_pos / (1.0 + stretch_intensity)); +} + +float compute_overscroll_end( + float in_pos, + float overscroll, + float reverse_stretch_dist, + float u_stretch_affected_dist, + float u_inverse_stretch_affected_dist, + float distance_stretched, + float interpolation_strength, + float viewport_dimension +) { + float offset_pos = in_pos - reverse_stretch_dist; + float pos_based_variation = mix( + 1.0, + ease_in(offset_pos, u_inverse_stretch_affected_dist), + interpolation_strength + ); + float stretch_intensity = (-overscroll) * pos_based_variation; + return viewport_dimension - (distance_stretched - (offset_pos / (1.0 + stretch_intensity))); +} + +float compute_streched_effect( + float in_pos, + float overscroll, + float u_stretch_affected_dist, + float u_inverse_stretch_affected_dist, + float distance_stretched, + float distance_diff, + float interpolation_strength, + float viewport_dimension +) { + if (overscroll > 0.0) { + if (in_pos <= u_stretch_affected_dist) { + return compute_overscroll_start( + in_pos, overscroll, u_stretch_affected_dist, + u_inverse_stretch_affected_dist, distance_stretched, + interpolation_strength + ); + } else { + return distance_diff + in_pos; + } + } else if (overscroll < 0.0) { + float stretch_affected_dist_calc = viewport_dimension - u_stretch_affected_dist; + if (in_pos >= stretch_affected_dist_calc) { + return compute_overscroll_end( + in_pos, + overscroll, + stretch_affected_dist_calc, + u_stretch_affected_dist, + u_inverse_stretch_affected_dist, + distance_stretched, + interpolation_strength, + viewport_dimension + ); + } else { + return -distance_diff + in_pos; + } + } else { + return in_pos; + } +} + +out vec4 frag_color; + +void main() { + vec2 uv = FlutterFragCoord().xy / u_size; + float in_u_norm = uv.x; + float in_v_norm = uv.y; + + float out_u_norm; + float out_v_norm; + + bool isVertical = u_overscroll_y != 0; + float overscroll = isVertical ? u_overscroll_y : u_overscroll_x; + + float norm_distance_stretched = 1.0 / (1.0 + abs(overscroll)); + float norm_dist_diff = norm_distance_stretched - 1.0; + + const float norm_viewport = 1.0; + const float norm_stretch_affected_dist = 1.0; + const float norm_inverse_stretch_affected_dist = 1.0; + + out_u_norm = isVertical ? in_u_norm : compute_streched_effect( + in_u_norm, + overscroll, + norm_stretch_affected_dist, + norm_inverse_stretch_affected_dist, + norm_distance_stretched, + norm_dist_diff, + u_interpolation_strength, + norm_viewport + ); + + out_v_norm = isVertical ? compute_streched_effect( + in_v_norm, + overscroll, + norm_stretch_affected_dist, + norm_inverse_stretch_affected_dist, + norm_distance_stretched, + norm_dist_diff, + u_interpolation_strength, + norm_viewport + ) : in_v_norm; + + uv.x = out_u_norm; + #ifdef IMPELLER_TARGET_OPENGLES + uv.y = 1.0 - out_v_norm; + #else + uv.y = out_v_norm; + #endif + + frag_color = texture(u_texture, uv); +} diff --git a/packages/flutter/lib/src/widgets/overscroll_indicator.dart b/packages/flutter/lib/src/widgets/overscroll_indicator.dart index 7e1751a5cc202..952214bfd4e6a 100644 --- a/packages/flutter/lib/src/widgets/overscroll_indicator.dart +++ b/packages/flutter/lib/src/widgets/overscroll_indicator.dart @@ -23,6 +23,7 @@ import 'framework.dart'; import 'media_query.dart'; import 'notification_listener.dart'; import 'scroll_notification.dart'; +import 'stretch_effect.dart'; import 'ticker_provider.dart'; import 'transitions.dart'; @@ -774,20 +775,6 @@ class _StretchingOverscrollIndicatorState extends State widget.axisDirection, - _StretchDirection.leading => flipAxisDirection(widget.axisDirection), - }; - return switch (direction) { - AxisDirection.up => AlignmentDirectional.topCenter, - AxisDirection.down => AlignmentDirectional.bottomCenter, - AxisDirection.left => Alignment.centerLeft, - AxisDirection.right => Alignment.centerRight, - }; - } - @override void dispose() { _stretchController.dispose(); @@ -802,30 +789,34 @@ class _StretchingOverscrollIndicatorState extends State= -1.0 && stretchStrength <= 1.0, + 'stretchStrength must be between -1.0 and 1.0', + ); + + /// The overscroll strength applied for the stretching effect. + /// + /// The value should be between -1.0 and 1.0 inclusive. + /// + /// For the horizontal axis: + /// - Positive values apply a pull/stretch from left to right, + /// where 1.0 represents the maximum stretch to the right. + /// - Negative values apply a pull/stretch from right to left, + /// where -1.0 represents the maximum stretch to the left. + /// + /// For the vertical axis: + /// - Positive values apply a pull/stretch from top to bottom, + /// where 1.0 represents the maximum stretch downward. + /// - Negative values apply a pull/stretch from bottom to top, + /// where -1.0 represents the maximum stretch upward. + /// + /// {@tool snippet} + /// This example shows how to set the horizontal stretch strength to pull right. + /// + /// ```dart + /// const StretchEffect( + /// stretchStrength: 0.5, + /// axis: Axis.horizontal, + /// child: Text('Hello, World!'), + /// ); + /// ``` + /// {@end-tool} + final double stretchStrength; + + /// The axis along which the stretching overscroll effect is applied. + /// + /// Determines the direction of the stretch, either horizontal or vertical. + final Axis axis; + + /// The child widget that the stretching overscroll effect applies to. + final Widget child; + + AlignmentGeometry _getAlignment(TextDirection direction) { + final bool isForward = stretchStrength > 0; + + if (axis == Axis.vertical) { + return isForward ? AlignmentDirectional.topCenter : AlignmentDirectional.bottomCenter; + } + + // RTL horizontal. + if (direction == TextDirection.rtl) { + return isForward ? AlignmentDirectional.centerEnd : AlignmentDirectional.centerStart; + } else { + return isForward ? AlignmentDirectional.centerStart : AlignmentDirectional.centerEnd; + } + } + + @override + Widget build(BuildContext context) { + if (ui.ImageFilter.isShaderFilterSupported) { + return _StretchOverscrollEffect(stretchStrength: stretchStrength, axis: axis, child: child); + } + + final TextDirection textDirection = Directionality.of(context); + double x = 1.0; + double y = 1.0; + + switch (axis) { + case Axis.horizontal: + x += stretchStrength.abs(); + case Axis.vertical: + y += stretchStrength.abs(); + } + + return Transform( + alignment: _getAlignment(textDirection), + transform: Matrix4.diagonal3Values(x, y, 1.0), + filterQuality: stretchStrength == 0 ? null : FilterQuality.medium, + child: child, + ); + } +} + +/// A widget that replicates the native Android stretch overscroll effect. +/// +/// This widget is used in the [StretchEffect] widget and creates +/// a stretch visual feedback when the user overscrolls at the edges. +/// +/// Only supported when using the Impeller rendering engine. +class _StretchOverscrollEffect extends StatefulWidget { + /// Creates a [_StretchOverscrollEffect] widget that applies a stretch + /// effect when the user overscrolls horizontally or vertically. + const _StretchOverscrollEffect({ + this.stretchStrength = 0.0, + required this.axis, + required this.child, + }) : assert( + stretchStrength >= -1.0 && stretchStrength <= 1.0, + 'stretchStrength must be between -1.0 and 1.0', + ); + + /// The overscroll strength applied for the stretching effect. + /// + /// The value should be between -1.0 and 1.0 inclusive. + /// For horizontal axis, Positive values apply a pull from + /// left to right, while negative values pull from right to left. + final double stretchStrength; + + /// The axis along which the stretching overscroll effect is applied. + /// + /// Determines the direction of the stretch, either horizontal or vertical. + final Axis axis; + + /// The child widget that the stretching overscroll effect applies to. + final Widget child; + + @override + State<_StretchOverscrollEffect> createState() => _StretchOverscrollEffectState(); +} + +class _StretchOverscrollEffectState extends State<_StretchOverscrollEffect> { + ui.FragmentShader? _fragmentShader; + + /// The maximum scale multiplier applied during a stretch effect. + static const double maxStretchIntensity = 1.0; + + /// The strength of the interpolation used for smoothing the effect. + static const double interpolationStrength = 0.7; + + /// A no-op [ui.ImageFilter] that uses the identity matrix. + static final ui.ImageFilter _emptyFilter = ui.ImageFilter.matrix(Matrix4.identity().storage); + + @override + void dispose() { + _fragmentShader?.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _StretchEffectShader.initializeShader(); + } + + @override + Widget build(BuildContext context) { + final bool isShaderNeeded = widget.stretchStrength.abs() > precisionErrorTolerance; + + final ui.ImageFilter imageFilter; + + if (_StretchEffectShader._initialized) { + _fragmentShader?.dispose(); + _fragmentShader = _StretchEffectShader._program!.fragmentShader(); + _fragmentShader!.setFloat(2, maxStretchIntensity); + if (widget.axis == Axis.vertical) { + _fragmentShader!.setFloat(3, 0.0); + _fragmentShader!.setFloat(4, widget.stretchStrength); + } else { + _fragmentShader!.setFloat(3, widget.stretchStrength); + _fragmentShader!.setFloat(4, 0.0); + } + _fragmentShader!.setFloat(5, interpolationStrength); + + imageFilter = ui.ImageFilter.shader(_fragmentShader!); + } else { + _fragmentShader?.dispose(); + _fragmentShader = null; + + imageFilter = _emptyFilter; + } + + return ImageFiltered( + imageFilter: imageFilter, + enabled: isShaderNeeded, + // A nearly-transparent pixels is used to ensure the shader gets applied, + // even when the child is visually transparent or has no paint operations. + child: CustomPaint( + painter: isShaderNeeded ? _StretchEffectPainter() : null, + child: widget.child, + ), + ); + } +} + +/// A [CustomPainter] that draws nearly transparent pixels at the four corners. +/// +/// This ensures the fragment shader covers the entire canvas by forcing +/// painting operations on all edges, preventing shader optimization skips. +class _StretchEffectPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final Paint paint = Paint() + ..color = const Color.fromARGB(1, 0, 0, 0) + ..style = PaintingStyle.fill; + + canvas.drawPoints(ui.PointMode.points, [ + Offset.zero, + Offset(size.width - 1, 0), + Offset(0, size.height - 1), + Offset(size.width - 1, size.height - 1), + ], paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class _StretchEffectShader { + static bool _initCalled = false; + static bool _initialized = false; + static ui.FragmentProgram? _program; + + static void initializeShader() { + if (!_initCalled) { + ui.FragmentProgram.fromAsset('shaders/stretch_effect.frag').then(( + ui.FragmentProgram program, + ) { + _program = program; + _initialized = true; + }); + _initCalled = true; + } + } +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index bcb359732a1a3..81486a12f6ca0 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -154,6 +154,7 @@ export 'src/widgets/spacer.dart'; export 'src/widgets/spell_check.dart'; export 'src/widgets/standard_component_type.dart'; export 'src/widgets/status_transitions.dart'; +export 'src/widgets/stretch_effect.dart'; export 'src/widgets/system_context_menu.dart'; export 'src/widgets/table.dart'; export 'src/widgets/tap_region.dart'; diff --git a/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart b/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart index eec79a4e941bc..5a4e6854440e1 100644 --- a/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart +++ b/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart @@ -7,11 +7,18 @@ @Tags(['reduced-test-set']) library; +import 'dart:ui' as ui; + import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { + // `StretchingOverscrollIndicator` uses a different algorithm when + // shader is available, therefore the tests must be different depending + // on shader support. + final bool shaderSupported = ui.ImageFilter.isShaderFilterSupported; + Widget buildTest( GlobalKey box1Key, GlobalKey box2Key, @@ -146,373 +153,401 @@ void main() { expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); }); - testWidgets('Stretch overscroll vertically', (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets( + 'Stretch overscroll vertically', + (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.start.png'), - ); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.start.png'), + ); - TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(0.0, 200.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(255.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(510.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.start.stretched.png'), - ); + TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(0.0, 200.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(255.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(510.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.start.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back to the start - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + // Stretch released back to the start + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - // Jump to end of the list - controller.jumpTo(controller.position.maxScrollExtent); - await tester.pumpAndSettle(); - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.end.png'), - ); + // Jump to end of the list + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pumpAndSettle(); + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.end.png'), + ); - gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); - // Overscroll the end - await gesture.moveBy(const Offset(0.0, -200.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(-165)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(90.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(350.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.end.stretched.png'), - ); + gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); + // Overscroll the end + await gesture.moveBy(const Offset(0.0, -200.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(-165)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(90.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(350.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.end.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); - }); + // Stretch released back + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); - testWidgets('Stretch overscroll works in reverse - vertical', (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets( + 'Stretch overscroll works in reverse - vertical', + (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller, reverse: true)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller, reverse: true)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), const Offset(0.0, 350.0)); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, -150.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), const Offset(0.0, 350.0)); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, -150.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll - await gesture.moveBy(const Offset(0.0, -200.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(350.0)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(100.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(-150.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.reverse.png'), - ); - }); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll + await gesture.moveBy(const Offset(0.0, -200.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(350.0)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(100.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(-150.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.reverse.png'), + ); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); - testWidgets('Stretch overscroll works in reverse - horizontal', (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets( + 'Stretch overscroll works in reverse - horizontal', + (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal, reverse: true), - ); + await tester.pumpWidget( + buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal, reverse: true), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); - expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); + expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.png'), - ); - }); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.png'), + ); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); - testWidgets('Stretch overscroll works in reverse - horizontal - RTL', ( - WidgetTester tester, - ) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets( + 'Stretch overscroll works in reverse - horizontal - RTL', + (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - axis: Axis.horizontal, - reverse: true, - textDirection: TextDirection.rtl, - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + axis: Axis.horizontal, + reverse: true, + textDirection: TextDirection.rtl, + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.png'), - ); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.png'), + ); - TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(200.0, 0.0)); - await tester.pumpAndSettle(); + TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(200.0, 0.0)); + await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.stretched.png'), - ); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back to the start - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + // Stretch released back to the start + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - // Jump to end of the list - controller.jumpTo(controller.position.maxScrollExtent); - await tester.pumpAndSettle(); - expect(controller.offset, 100.0); - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.png'), - ); + // Jump to end of the list + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pumpAndSettle(); + expect(controller.offset, 100.0); + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.png'), + ); - gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); - // Overscroll the end - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.stretched.png'), - ); + gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); + // Overscroll the end + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - }); + // Stretch released back + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); - testWidgets('Stretch overscroll horizontally', (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets( + 'Stretch overscroll horizontally', + (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal), - ); + await tester.pumpWidget( + buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.start.png'), - ); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.start.png'), + ); - TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.start.stretched.png'), - ); + TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.start.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back to the start - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + // Stretch released back to the start + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - // Jump to end of the list - controller.jumpTo(controller.position.maxScrollExtent); - await tester.pumpAndSettle(); - expect(controller.offset, 100.0); - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.end.png'), - ); + // Jump to end of the list + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pumpAndSettle(); + expect(controller.offset, 100.0); + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.end.png'), + ); - gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); - // Overscroll the end - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.end.stretched.png'), - ); + gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); + // Overscroll the end + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.end.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - }); + // Stretch released back + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); - testWidgets('Stretch overscroll horizontally RTL', (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets( + 'Stretch overscroll horizontally RTL', + (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - axis: Axis.horizontal, - textDirection: TextDirection.rtl, - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + axis: Axis.horizontal, + textDirection: TextDirection.rtl, + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); - expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); + expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.rtl.png'), - ); - }); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.rtl.png'), + ); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); testWidgets('Disallow stretching overscroll', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); @@ -733,373 +768,403 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('Stretch limit', (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/issues/99264 - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: ScrollConfiguration( - behavior: const ScrollBehavior().copyWith(overscroll: false), - child: StretchingOverscrollIndicator( - axisDirection: AxisDirection.down, - child: SizedBox( - height: 300, - child: ListView.builder( - itemCount: 20, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Text('Index $index'), - ); - }, + testWidgets( + 'Stretch limit', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/99264 + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(overscroll: false), + child: StretchingOverscrollIndicator( + axisDirection: AxisDirection.down, + child: SizedBox( + height: 300, + child: ListView.builder( + itemCount: 20, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Text('Index $index'), + ); + }, + ), ), ), ), ), ), - ), - ); - const double maxStretchLocation = 52.63178407049861; - - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, 51.0); + ); + const double maxStretchLocation = 52.63178407049861; - TestGesture pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Overscroll beyond the limit (the viewport is 600.0). - await pointer.moveBy(const Offset(0.0, 610.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, 51.0); - pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Overscroll way way beyond the limit - await pointer.moveBy(const Offset(0.0, 1000.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); + TestGesture pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Overscroll beyond the limit (the viewport is 600.0). + await pointer.moveBy(const Offset(0.0, 610.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); - await pointer.up(); - await tester.pumpAndSettle(); - }); + pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Overscroll way way beyond the limit + await pointer.moveBy(const Offset(0.0, 1000.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); - testWidgets('Multiple pointers will not exceed stretch limit', (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/issues/99264 - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: ScrollConfiguration( - behavior: const ScrollBehavior().copyWith(overscroll: false), - child: StretchingOverscrollIndicator( - axisDirection: AxisDirection.down, - child: SizedBox( - height: 300, - child: ListView.builder( - itemCount: 20, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Text('Index $index'), - ); - }, + await pointer.up(); + await tester.pumpAndSettle(); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); + + testWidgets( + 'Multiple pointers will not exceed stretch limit', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/99264 + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(overscroll: false), + child: StretchingOverscrollIndicator( + axisDirection: AxisDirection.down, + child: SizedBox( + height: 300, + child: ListView.builder( + itemCount: 20, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Text('Index $index'), + ); + }, + ), ), ), ), ), ), - ), - ); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, 51.0); + ); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, 51.0); - final TestGesture pointer1 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Overscroll the start. - await pointer1.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - double lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; - expect(lastStretchedLocation, greaterThan(51.0)); + final TestGesture pointer1 = await tester.startGesture( + tester.getCenter(find.text('Index 1')), + ); + // Overscroll the start. + await pointer1.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + double lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; + expect(lastStretchedLocation, greaterThan(51.0)); - final TestGesture pointer2 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Add overscroll from an additional pointer - await pointer2.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); - lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; + final TestGesture pointer2 = await tester.startGesture( + tester.getCenter(find.text('Index 1')), + ); + // Add overscroll from an additional pointer + await pointer2.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); + lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; - final TestGesture pointer3 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Add overscroll from an additional pointer, exceeding the max stretch (600) - await pointer3.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); - lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; + final TestGesture pointer3 = await tester.startGesture( + tester.getCenter(find.text('Index 1')), + ); + // Add overscroll from an additional pointer, exceeding the max stretch (600) + await pointer3.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); + lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; - final TestGesture pointer4 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Since we have maxed out the overscroll, it should not have stretched - // further, regardless of the number of pointers. - await pointer4.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, lastStretchedLocation); + final TestGesture pointer4 = await tester.startGesture( + tester.getCenter(find.text('Index 1')), + ); + // Since we have maxed out the overscroll, it should not have stretched + // further, regardless of the number of pointers. + await pointer4.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, lastStretchedLocation); - await pointer1.up(); - await pointer2.up(); - await pointer3.up(); - await pointer4.up(); - await tester.pumpAndSettle(); - }); + await pointer1.up(); + await pointer2.up(); + await pointer3.up(); + await pointer4.up(); + await tester.pumpAndSettle(); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); - testWidgets('Stretch overscroll vertically, change direction mid scroll', ( - WidgetTester tester, - ) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets( + 'Stretch overscroll vertically, change direction mid scroll', + (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - // Setting the `boxHeight` to 100.0 will make the boxes fit in the - // scrollable viewport. - boxHeight: 100, - // To make the scroll view in the test still scrollable, we need to add - // the `AlwaysScrollableScrollPhysics`. - physics: const AlwaysScrollableScrollPhysics(), - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + // Setting the `boxHeight` to 100.0 will make the boxes fit in the + // scrollable viewport. + boxHeight: 100, + // To make the scroll view in the test still scrollable, we need to add + // the `AlwaysScrollableScrollPhysics`. + physics: const AlwaysScrollableScrollPhysics(), + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(0.0, 600.0)); - await tester.pumpAndSettle(); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(0.0, 600.0)); + await tester.pumpAndSettle(); - // The boxes should now be at different locations because of the scaling. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); + // The boxes should now be at different locations because of the scaling. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); - // Move the pointer up a miniscule amount to trigger a directional change. - await gesture.moveBy(const Offset(0.0, -20.0)); - await tester.pumpAndSettle(); + // Move the pointer up a miniscule amount to trigger a directional change. + await gesture.moveBy(const Offset(0.0, -20.0)); + await tester.pumpAndSettle(); - // The boxes should remain roughly at the same locations, since the pointer - // didn't move far. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); + // The boxes should remain roughly at the same locations, since the pointer + // didn't move far. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); - // Now make the pointer overscroll to the end - await gesture.moveBy(const Offset(0.0, -1200.0)); - await tester.pumpAndSettle(); + // Now make the pointer overscroll to the end + await gesture.moveBy(const Offset(0.0, -1200.0)); + await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(-19.0)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(85.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(188.0)); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(-19.0)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(85.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(188.0)); - // Release the pointer - await gesture.up(); - await tester.pumpAndSettle(); + // Release the pointer + await gesture.up(); + await tester.pumpAndSettle(); - // Now the boxes should be back to their original locations. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); - }); + // Now the boxes should be back to their original locations. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); - testWidgets('Stretch overscroll horizontally, change direction mid scroll', ( - WidgetTester tester, - ) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets( + 'Stretch overscroll horizontally, change direction mid scroll', + (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - // Setting the `boxWidth` to 100.0 will make the boxes fit in the - // scrollable viewport. - boxWidth: 100, - // To make the scroll view in the test still scrollable, we need to add - // the `AlwaysScrollableScrollPhysics`. - physics: const AlwaysScrollableScrollPhysics(), - axis: Axis.horizontal, - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + // Setting the `boxWidth` to 100.0 will make the boxes fit in the + // scrollable viewport. + boxWidth: 100, + // To make the scroll view in the test still scrollable, we need to add + // the `AlwaysScrollableScrollPhysics`. + physics: const AlwaysScrollableScrollPhysics(), + axis: Axis.horizontal, + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(600.0, 0.0)); - await tester.pumpAndSettle(); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(600.0, 0.0)); + await tester.pumpAndSettle(); - // The boxes should now be at different locations because of the scaling. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); + // The boxes should now be at different locations because of the scaling. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); - // Move the pointer up a miniscule amount to trigger a directional change. - await gesture.moveBy(const Offset(-20.0, 0.0)); - await tester.pumpAndSettle(); + // Move the pointer up a miniscule amount to trigger a directional change. + await gesture.moveBy(const Offset(-20.0, 0.0)); + await tester.pumpAndSettle(); - // The boxes should remain roughly at the same locations, since the pointer - // didn't move far. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); + // The boxes should remain roughly at the same locations, since the pointer + // didn't move far. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); - // Now make the pointer overscroll to the end - await gesture.moveBy(const Offset(-1200.0, 0.0)); - await tester.pumpAndSettle(); + // Now make the pointer overscroll to the end + await gesture.moveBy(const Offset(-1200.0, 0.0)); + await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(-19.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(85.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(188.0)); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(-19.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(85.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(188.0)); - // Release the pointer - await gesture.up(); - await tester.pumpAndSettle(); + // Release the pointer + await gesture.up(); + await tester.pumpAndSettle(); - // Now the boxes should be back to their original locations. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - }); + // Now the boxes should be back to their original locations. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); - testWidgets('Fling toward the trailing edge causes stretch toward the leading edge', ( - WidgetTester tester, - ) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets( + 'Fling toward the trailing edge causes stretch toward the leading edge', + (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); + await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); - // The boxes should now be at different locations because of the scaling. - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(-160.0)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(93.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(347.0)); + // The boxes should now be at different locations because of the scaling. + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(-160.0)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(93.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(347.0)); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - // The boxes should now be at their final position. - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); - }); + // The boxes should now be at their final position. + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); - testWidgets('Fling toward the leading edge causes stretch toward the trailing edge', ( - WidgetTester tester, - ) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets( + 'Fling toward the leading edge causes stretch toward the trailing edge', + (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - // We fling to the trailing edge and let it settle. - await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); - await tester.pumpAndSettle(); + // We fling to the trailing edge and let it settle. + await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); + await tester.pumpAndSettle(); - // We are now at the trailing edge - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); + // We are now at the trailing edge + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); - // Now fling to the leading edge - await tester.fling(find.byType(CustomScrollView), const Offset(0.0, 50.0), 10000.0); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); + // Now fling to the leading edge + await tester.fling(find.byType(CustomScrollView), const Offset(0.0, 50.0), 10000.0); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); - // The boxes should now be at different locations because of the scaling. - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero).dy, 0.0); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(254.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(508.0)); + // The boxes should now be at different locations because of the scaling. + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero).dy, 0.0); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(254.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(508.0)); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - // The boxes should now be at their final position. - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero).dy, 0.0); - expect(box2.localToGlobal(Offset.zero).dy, 250.0); - expect(box3.localToGlobal(Offset.zero).dy, 500.0); - }); + // The boxes should now be at their final position. + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero).dy, 0.0); + expect(box2.localToGlobal(Offset.zero).dy, 250.0); + expect(box3.localToGlobal(Offset.zero).dy, 500.0); + }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, + ); testWidgets( 'changing scroll direction during recede animation will not change the stretch direction', @@ -1166,6 +1231,8 @@ void main() { await gesture.up(); }, + // Skips this test when fragment shaders are used. + skip: shaderSupported, ); testWidgets('Stretch overscroll only uses image filter during stretch effect', ( @@ -1233,7 +1300,7 @@ void main() { // We fling to the trailing edge and let it settle. await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(const Duration(milliseconds: 300)); // We are now at the trailing edge expect(overscrollNotification.velocity, lessThan(25)); diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart index 92aa4509efb6b..3d127de0b4243 100644 --- a/packages/flutter/test/widgets/page_view_test.dart +++ b/packages/flutter/test/widgets/page_view_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/foundation.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; @@ -1264,19 +1269,10 @@ void main() { controller.animateToPage(2, duration: const Duration(milliseconds: 300), curve: Curves.ease); await tester.pumpAndSettle(); - final Finder transformFinder = find.descendant( - of: find.byType(PageView), - matching: find.byType(Transform), - ); - expect(transformFinder, findsOneWidget); - - // Get the Transform widget that stretches the PageView. - final Transform transform = tester.firstWidget( - find.descendant(of: find.byType(PageView), matching: find.byType(Transform)), + await expectLater( + find.byType(PageView), + matchesGoldenFile('page_view_no_stretch_precision_error.png'), ); - - // Check the stretch factor in the first element of the transform matrix. - expect(transform.transform.storage.first, 1.0); }); testWidgets('PageController onAttach, onDetach', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/stretch_effect_test.dart b/packages/flutter/test/widgets/stretch_effect_test.dart new file mode 100644 index 0000000000000..9f519c1e42f47 --- /dev/null +++ b/packages/flutter/test/widgets/stretch_effect_test.dart @@ -0,0 +1,56 @@ +// 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. + +// This file is run as part of a reduced test set in CI on Mac and Windows +// machines. +@Tags(['reduced-test-set']) +library; + +import 'dart:ui' as ui; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + // `StretchingOverscrollIndicator` uses a different algorithm when + // shader is available, therefore the tests must be different depending + // on shader support. + final bool shaderSupported = ui.ImageFilter.isShaderFilterSupported; + + testWidgets( + 'Stretch effect covers full viewport', + (WidgetTester tester) async { + // This test verifies that when the stretch effect is applied to a scrollable widget, + // it should cover the entire scrollable area (e.g., full height of the scroll view), + // even if the actual content inside has a smaller height. + // + // Without this behavior, the shader is clipped only to the content area, + // causing the stretch effect to render incorrectly or be invisible + // when the content doesn't fill the scroll view. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: StretchEffect( + stretchStrength: 1, + axis: Axis.vertical, + child: Stack( + alignment: Alignment.topCenter, + children: [ + Container(height: 100), + Container(height: 50, color: const Color.fromRGBO(255, 0, 0, 1)), + ], + ), + ), + ), + ); + + await expectLater( + find.byType(StretchEffect), + matchesGoldenFile('stretch_effect_covers_full_viewport.png'), + ); + }, + // Skips this test when fragment shaders are not used. + skip: !shaderSupported, + ); +} diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 393f882f38183..406ae53d82529 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -50,7 +50,7 @@ const kMaterialFonts = >[ }, ]; -const kMaterialShaders = ['shaders/ink_sparkle.frag']; +const kMaterialShaders = ['shaders/ink_sparkle.frag', 'shaders/stretch_effect.frag']; /// Injected factory class for spawning [AssetBundle] instances. abstract class AssetBundleFactory { diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart index 4edbc1b89cb97..39c11fded0dc1 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart @@ -1017,26 +1017,30 @@ flutter: materialDir.childFile(shader).createSync(recursive: true); } - (globals.processManager as FakeProcessManager).addCommand( - FakeCommand( - command: [ - impellerc, - '--sksl', - '--iplr', - '--json', - '--sl=${fileSystem.path.join(output.path, 'shaders', 'ink_sparkle.frag')}', - '--spirv=${fileSystem.path.join(output.path, 'shaders', 'ink_sparkle.frag.spirv')}', - '--input=${fileSystem.path.join(materialDir.path, 'shaders', 'ink_sparkle.frag')}', - '--input-type=frag', - '--include=${fileSystem.path.join(materialDir.path, 'shaders')}', - '--include=$shaderLibDir', - ], - onRun: (_) { - fileSystem.file(outputPath).createSync(recursive: true); - fileSystem.file('$outputPath.spirv').createSync(recursive: true); - }, - ), - ); + final testShaders = ['ink_sparkle.frag', 'stretch_effect.frag']; + + for (final shader in testShaders) { + (globals.processManager as FakeProcessManager).addCommand( + FakeCommand( + command: [ + impellerc, + '--sksl', + '--iplr', + '--json', + '--sl=${fileSystem.path.join(output.path, 'shaders', shader)}', + '--spirv=${fileSystem.path.join(output.path, 'shaders', '$shader.spirv')}', + '--input=${fileSystem.path.join(materialDir.path, 'shaders', shader)}', + '--input-type=frag', + '--include=${fileSystem.path.join(materialDir.path, 'shaders')}', + '--include=$shaderLibDir', + ], + onRun: (_) { + fileSystem.file(outputPath).createSync(recursive: true); + fileSystem.file('$outputPath.spirv').createSync(recursive: true); + }, + ), + ); + } fileSystem.file('pubspec.yaml') ..createSync() diff --git a/packages/flutter_tools/test/general.shard/asset_test.dart b/packages/flutter_tools/test/general.shard/asset_test.dart index f64c64f623445..3e1f99ccedb84 100644 --- a/packages/flutter_tools/test/general.shard/asset_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_test.dart @@ -304,18 +304,23 @@ flutter: expect(assetBundle.inputFiles.map((File f) => f.path), []); }); + final testShaders = ['ink_sparkle.frag', 'stretch_effect.frag']; + testWithoutContext('bundles material shaders on non-web platforms', () async { - final String shaderPath = fileSystem.path.join( - flutterRoot, - 'packages', - 'flutter', - 'lib', - 'src', - 'material', - 'shaders', - 'ink_sparkle.frag', - ); - fileSystem.file(shaderPath).createSync(recursive: true); + for (final shader in testShaders) { + final String shaderPath = fileSystem.path.join( + flutterRoot, + 'packages', + 'flutter', + 'lib', + 'src', + 'material', + 'shaders', + shader, + ); + fileSystem.file(shaderPath).createSync(recursive: true); + } + writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_package'); fileSystem.file('pubspec.yaml').writeAsStringSync('name: my_package'); final assetBundle = ManifestAssetBundle( @@ -331,21 +336,26 @@ flutter: flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), ); - expect(assetBundle.entries.keys, contains('shaders/ink_sparkle.frag')); + for (final shader in testShaders) { + expect(assetBundle.entries.keys, contains('shaders/$shader')); + } }); testWithoutContext('bundles material shaders on web platforms', () async { - final String shaderPath = fileSystem.path.join( - flutterRoot, - 'packages', - 'flutter', - 'lib', - 'src', - 'material', - 'shaders', - 'ink_sparkle.frag', - ); - fileSystem.file(shaderPath).createSync(recursive: true); + for (final shader in testShaders) { + final String shaderPath = fileSystem.path.join( + flutterRoot, + 'packages', + 'flutter', + 'lib', + 'src', + 'material', + 'shaders', + shader, + ); + fileSystem.file(shaderPath).createSync(recursive: true); + } + writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_package'); fileSystem.file('pubspec.yaml').writeAsStringSync('name: my_package'); final assetBundle = ManifestAssetBundle( @@ -361,7 +371,9 @@ flutter: flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), ); - expect(assetBundle.entries.keys, contains('shaders/ink_sparkle.frag')); + for (final shader in testShaders) { + expect(assetBundle.entries.keys, contains('shaders/$shader')); + } }); }); } From 38eae2bf098808e0d1d063b034c295b478ac536a Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 14 Aug 2025 14:26:33 -0400 Subject: [PATCH 045/720] Roll Packages from 6cb9113d5fa4 to 09533b7d5d66 (4 revisions) (#173789) https://github.com/flutter/packages/compare/6cb9113d5fa4...09533b7d5d66 2025-08-14 engine-flutter-autoroll@skia.org Roll Flutter from 34c2a3b158b2 to f4334d27934b (18 revisions) (flutter/packages#9807) 2025-08-14 stuartmorgan@google.com Add review agent style guidelines to .gemini/styleguide.md (flutter/packages#9805) 2025-08-13 koji.wakamiya@gmail.com [go_router_builder] Support extension types (flutter/packages#9458) 2025-08-13 engine-flutter-autoroll@skia.org Roll Flutter from e2a347b14a18 to 34c2a3b158b2 (41 revisions) (flutter/packages#9803) 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 5e06b9179cb6e..4b87b74f1636f 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -6cb9113d5fa49a35774e4582898fae3ea4aec958 +09533b7d5d66b66b75b43ec89c116312838f94a7 From 450401dfec79c052ce36432b141c6fe81ac09f97 Mon Sep 17 00:00:00 2001 From: Hannah Jin Date: Thu, 14 Aug 2025 11:27:51 -0700 Subject: [PATCH 046/720] [Range slider] Tap on active range, the thumb closest to the mouse cursor should move to the cursor position. (#173725) Fix: https://github.com/flutter/flutter/issues/172923 Internal issue: b/434778923 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] 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]. **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 [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/material/range_slider.dart | 18 ++++---- .../test/material/range_slider_test.dart | 41 ++++++++++--------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/packages/flutter/lib/src/material/range_slider.dart b/packages/flutter/lib/src/material/range_slider.dart index 7d220ccece04b..1b9658ed27593 100644 --- a/packages/flutter/lib/src/material/range_slider.dart +++ b/packages/flutter/lib/src/material/range_slider.dart @@ -599,10 +599,13 @@ class _RangeSliderState extends State with TickerProviderStateMixin return RangeValues(_unlerp(values.start), _unlerp(values.end)); } - // Finds closest thumb. If the thumbs are close to each other, no thumb is - // immediately selected while the drag displacement is zero. If the first - // non-zero displacement is negative, then the left thumb is selected, and if its - // positive, then the right thumb is selected. + // Finds the closest thumb. If both thumbs are close to each other and within + // the touch radius, neither is selected immediately while the drag + // displacement is zero. The first non-zero displacement determines which + // thumb is selected: a negative displacement selects the left thumb, + // a positive one selects the right thumb. + // If only one or zero thumbs are within the touch radius, + // the closest one is selected. Thumb? _defaultRangeThumbSelector( TextDirection textDirection, RangeValues values, @@ -632,11 +635,10 @@ class _RangeSliderState extends State with TickerProviderStateMixin return Thumb.end; } } else { - // Snap position on the track if its in the inactive range. - if (tapValue < values.start || inStartTouchTarget) { + // Choose the closest thumb and snap position. + if (tapValue * 2 < values.start + values.end) { return Thumb.start; - } - if (tapValue > values.end || inEndTouchTarget) { + } else { return Thumb.end; } } diff --git a/packages/flutter/test/material/range_slider_test.dart b/packages/flutter/test/material/range_slider_test.dart index 05016ed02d2df..fd9944dd0e0f5 100644 --- a/packages/flutter/test/material/range_slider_test.dart +++ b/packages/flutter/test/material/range_slider_test.dart @@ -125,7 +125,7 @@ void main() { }); testWidgets('Range Slider can move when tapped (continuous LTR)', (WidgetTester tester) async { - RangeValues values = const RangeValues(0.3, 0.7); + RangeValues values = const RangeValues(0.3, 0.8); await tester.pumpWidget( MaterialApp( @@ -151,13 +151,13 @@ void main() { ), ); - // No thumbs get select when tapping between the thumbs outside the touch + // The closest thumb is selected when tapping between the thumbs outside the touch // boundaries - expect(values, equals(const RangeValues(0.3, 0.7))); + expect(values, equals(const RangeValues(0.3, 0.8))); // taps at 0.5 await tester.tap(find.byType(RangeSlider)); await tester.pump(); - expect(values, equals(const RangeValues(0.3, 0.7))); + expect(values, equals(const RangeValues(0.5, 0.8))); // Get the bounds of the track by finding the slider edges and translating // inwards by the overlay radius. @@ -168,7 +168,7 @@ void main() { final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; await tester.tapAt(leftTarget); expect(values.start, moreOrLessEquals(0.1, epsilon: 0.01)); - expect(values.end, equals(0.7)); + expect(values.end, equals(0.8)); // The end thumb is selected when tapping the right inactive track. await tester.pump(); @@ -179,7 +179,7 @@ void main() { }); testWidgets('Range Slider can move when tapped (continuous RTL)', (WidgetTester tester) async { - RangeValues values = const RangeValues(0.3, 0.7); + RangeValues values = const RangeValues(0.3, 1.0); await tester.pumpWidget( MaterialApp( @@ -205,13 +205,13 @@ void main() { ), ); - // No thumbs get select when tapping between the thumbs outside the touch + // The closest thumb is selected when tapping between the thumbs outside the touch // boundaries - expect(values, equals(const RangeValues(0.3, 0.7))); + expect(values, equals(const RangeValues(0.3, 1.0))); // taps at 0.5 await tester.tap(find.byType(RangeSlider)); await tester.pump(); - expect(values, equals(const RangeValues(0.3, 0.7))); + expect(values, equals(const RangeValues(0.5, 1.0))); // Get the bounds of the track by finding the slider edges and translating // inwards by the overlay radius. @@ -221,7 +221,7 @@ void main() { // The end thumb is selected when tapping the left inactive track. final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; await tester.tapAt(leftTarget); - expect(values.start, 0.3); + expect(values.start, 0.5); expect(values.end, moreOrLessEquals(0.9, epsilon: 0.01)); // The start thumb is selected when tapping the right inactive track. @@ -233,7 +233,7 @@ void main() { }); testWidgets('Range Slider can move when tapped (discrete LTR)', (WidgetTester tester) async { - RangeValues values = const RangeValues(30, 70); + RangeValues values = const RangeValues(30, 80); await tester.pumpWidget( MaterialApp( @@ -261,13 +261,13 @@ void main() { ), ); - // No thumbs get select when tapping between the thumbs outside the touch + // The closest thumb is selected when tapping between the thumbs outside the touch // boundaries - expect(values, equals(const RangeValues(30, 70))); + expect(values, equals(const RangeValues(30, 80))); // taps at 0.5 await tester.tap(find.byType(RangeSlider)); await tester.pumpAndSettle(); - expect(values, equals(const RangeValues(30, 70))); + expect(values, equals(const RangeValues(50, 80))); // Get the bounds of the track by finding the slider edges and translating // inwards by the overlay radius. @@ -279,7 +279,7 @@ void main() { await tester.tapAt(leftTarget); await tester.pumpAndSettle(); expect(values.start.round(), equals(10)); - expect(values.end.round(), equals(70)); + expect(values.end.round(), equals(80)); // The end thumb is selected when tapping the right inactive track. await tester.pump(); @@ -291,7 +291,7 @@ void main() { }); testWidgets('Range Slider can move when tapped (discrete RTL)', (WidgetTester tester) async { - RangeValues values = const RangeValues(30, 70); + RangeValues values = const RangeValues(30, 80); await tester.pumpWidget( MaterialApp( @@ -319,13 +319,13 @@ void main() { ), ); - // No thumbs get select when tapping between the thumbs outside the touch + // The closest thumb is selected when tapping between the thumbs outside the touch // boundaries - expect(values, equals(const RangeValues(30, 70))); + expect(values, equals(const RangeValues(30, 80))); // taps at 0.5 await tester.tap(find.byType(RangeSlider)); await tester.pumpAndSettle(); - expect(values, equals(const RangeValues(30, 70))); + expect(values, equals(const RangeValues(50, 80))); // Get the bounds of the track by finding the slider edges and translating // inwards by the overlay radius. @@ -336,7 +336,7 @@ void main() { final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; await tester.tapAt(leftTarget); await tester.pumpAndSettle(); - expect(values.start.round(), equals(30)); + expect(values.start.round(), equals(50)); expect(values.end.round(), equals(90)); // The end thumb is selected when tapping the right inactive track. @@ -744,6 +744,7 @@ void main() { final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; await tester.dragFrom(rightTarget, middle - rightTarget); expect(values.start, moreOrLessEquals(50, epsilon: 0.01)); + expect(values.end, moreOrLessEquals(50, epsilon: 0.01)); // Drag the start thumb apart. await tester.pumpAndSettle(); From ad7ee1cf04036d6aaa62c1ac9cddc85d9316e7d9 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 14 Aug 2025 14:40:26 -0400 Subject: [PATCH 047/720] Roll Fuchsia Linux SDK from I1TfNmsqTp7t3rO8e... to zWRpLglb48zC1vZLv... (#173784) 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 chinmaygarde@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 a88564a19bbd9..b08ac1334dfce 100644 --- a/DEPS +++ b/DEPS @@ -810,7 +810,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'I1TfNmsqTp7t3rO8e3nDRysmyG1yHzZ7Mac7X5Pw9hkC' + 'version': 'zWRpLglb48zC1vZLvXqnKtqDdcLIVvlZnG_SsGz8L54C' } ], 'condition': 'download_fuchsia_deps and not download_fuchsia_sdk', From 2fcda047903fbb12ff989be3f58e72398c846318 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:53:20 -0700 Subject: [PATCH 048/720] Add error handling for `Element` lifecycle user callbacks (#173148) This is for #172289, but since this fix is speculative so I'll wait for the confirmation from the original reporters before closing the issue. As a bonus this fixes #65655 The framework Element rebuild logic relies heavily on `Element._lifecycleState` being correct. When a user-supplied lifecycle callback (e.g., `State.deactivate`) fails the framework currently only ensures that every `Element` in the tree has the right lifecycle state, so an out-of-tree `Element` that is supposed to be disposed may still have an `active` state and continue being rebuilt by the BuildScope (because it's in the dirty list). See the comments in #172289 Also related: #100777 Internal: b/425298525 b/431537277 b/300829376 b/415724119 b/283614822 # TODO (in a different PR) The original issue could also be caused by incorrect `Element.updateChild` calls. If an `Element` subclass calls `Element.updateChild` to add child but forgets to update its child list accordingly (such that `visitChildren` misses that child), you'll get a child Element that thinks it's a child of the parent but the parent doesn't recognize the child so won't take that child into account during reparenting or unmounting. This is a programmer error that we should try to catch in the framework. ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] 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]. **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 [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/object.dart | 2 +- .../flutter/lib/src/widgets/framework.dart | 194 +++++++++++++----- packages/flutter/test/cupertino/tab_test.dart | 4 - packages/flutter/test/material/app_test.dart | 4 - .../flutter/test/widgets/framework_test.dart | 69 ++++++- .../test/widgets/memory_allocations_test.dart | 2 + .../widgets/navigator_restoration_test.dart | 13 -- .../flutter/test/widgets/tree_shape_test.dart | 43 ++-- .../widgets/memory_allocations_test.dart | 2 + 9 files changed, 242 insertions(+), 91 deletions(-) diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 3ce62d3c8db99..ac7ef2103de47 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -2059,7 +2059,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge assert(child._parent == this); assert(child.attached == attached); assert(child.parentData != null); - if (!(_isRelayoutBoundary ?? true)) { + if (!(child._isRelayoutBoundary ?? true)) { child._isRelayoutBoundary = null; } child.parentData!.detach(); diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 90483526cbdea..95f4bb07cbd5b 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -2053,13 +2053,54 @@ abstract class MultiChildRenderObjectWidget extends RenderObjectWidget { // ELEMENTS -enum _ElementLifecycle { initial, active, inactive, defunct } +enum _ElementLifecycle { + /// The [Element] is created but has not yet been incorporated into the element + /// tree. + initial, + + /// The [Element] is incorporated into the Element tree, either via + /// [Element.mount] or [Element.activate]. + active, + + /// The previously `active` [Element] is removed from the Element tree via + /// [Element.deactivate]. + /// + /// This [Element] may become `active` again if a parent reclaims it using + /// a [GlobalKey], or `defunct` if no parent reclaims it at the end of the + /// build phase. + inactive, + + /// The [Element] encountered an unrecoverable error while being rebuilt when it + /// was `active` or while being incorporated in the tree. + /// + /// This indicates the [Element]'s subtree is in an inconsistent state and must + /// not be re-incorporated into the tree again. + /// + /// When an unrecoverable error is encountered, the framework calls + /// [Element.deactivate] on this [Element] and sets its state to `failed`. This + /// process is done on a best-effort basis and does not surface any additional + /// errors. + /// + /// This is one of the two final stages of the element lifecycle and is not + /// reversible. Reaching this state typically means that a widget implementation + /// is throwing unhandled exceptions that need to be properly handled. + failed, + + /// The [Element] is disposed and should not be interacted with. + /// + /// The [Element] must be `inactive` before transitioning into this state, + /// and the state transition occurs in [BuildOwner.finalizeTree] which signals + /// the end of the build phase. + /// + /// This is the final stage of the element lifecycle and is not reversible. + defunct, +} class _InactiveElements { bool _locked = false; final Set _elements = HashSet(); - void _unmount(Element element) { + static void _unmount(Element element) { assert(element._lifecycleState == _ElementLifecycle.inactive); assert(() { if (debugPrintGlobalKeyedWidgetLifecycle) { @@ -2091,8 +2132,12 @@ class _InactiveElements { static void _deactivateRecursively(Element element) { assert(element._lifecycleState == _ElementLifecycle.active); - element.deactivate(); - assert(element._lifecycleState == _ElementLifecycle.inactive); + try { + element.deactivate(); + } catch (_) { + Element._deactivateFailedSubtreeRecursively(element); + rethrow; + } element.visitChildren(_deactivateRecursively); assert(() { element.debugDeactivated(); @@ -2104,10 +2149,18 @@ class _InactiveElements { assert(!_locked); assert(!_elements.contains(element)); assert(element._parent == null); - if (element._lifecycleState == _ElementLifecycle.active) { - _deactivateRecursively(element); + + switch (element._lifecycleState) { + case _ElementLifecycle.active: + _deactivateRecursively(element); + // This element is only added to _elements if the whole subtree is + // successfully deactivated. + _elements.add(element); + case _ElementLifecycle.inactive: + _elements.add(element); + case _ElementLifecycle.initial || _ElementLifecycle.failed || _ElementLifecycle.defunct: + assert(false, '$element must not be deactivated when in ${element._lifecycleState} state.'); } - _elements.add(element); } void remove(Element element) { @@ -2115,7 +2168,7 @@ class _InactiveElements { assert(_elements.contains(element)); assert(element._parent == null); _elements.remove(element); - assert(element._lifecycleState != _ElementLifecycle.active); + assert(element._lifecycleState == _ElementLifecycle.inactive); } bool debugContains(Element element) { @@ -2934,7 +2987,7 @@ class BuildOwner { buildScope._scheduleBuildFor(element); assert(() { if (debugPrintScheduleBuildForStacks) { - debugPrint("...the build scope's dirty list is now: $buildScope._dirtyElements"); + debugPrint("...the build scope's dirty list is now: ${buildScope._dirtyElements}"); } return true; }()); @@ -4516,39 +4569,32 @@ abstract class Element extends DiagnosticableTree implements BuildContext { try { final Key? key = newWidget.key; - if (key is GlobalKey) { - final Element? newChild = _retakeInactiveElement(key, newWidget); - if (newChild != null) { - assert(newChild._parent == null); - assert(() { - _debugCheckForCycles(newChild); - return true; - }()); - try { - newChild._activateWithParent(this, newSlot); - } catch (_) { - // Attempt to do some clean-up if activation fails to leave tree in a reasonable state. - try { - deactivateChild(newChild); - } catch (_) { - // Clean-up failed. Only surface original exception. - } - rethrow; - } - final Element? updatedChild = updateChild(newChild, newWidget, newSlot); - assert(newChild == updatedChild); - return updatedChild!; - } - } - final Element newChild = newWidget.createElement(); + final Element? inactiveChild = key is GlobalKey + ? _retakeInactiveElement(key, newWidget) + : null; + final Element newChild = inactiveChild ?? newWidget.createElement(); assert(() { _debugCheckForCycles(newChild); return true; }()); - newChild.mount(this, newSlot); - assert(newChild._lifecycleState == _ElementLifecycle.active); - - return newChild; + try { + if (inactiveChild != null) { + assert(inactiveChild._parent == null); + inactiveChild._activateWithParent(this, newSlot); + final Element? updatedChild = updateChild(inactiveChild, newWidget, newSlot); + assert(inactiveChild == updatedChild); + return updatedChild!; + } else { + newChild.mount(this, newSlot); + assert(newChild._lifecycleState == _ElementLifecycle.active); + return newChild; + } + } catch (_) { + // Attempt to do some clean-up if activation or mount fails + // to leave tree in a reasonable state. + _deactivateFailedChildSilently(newChild); + rethrow; + } } finally { if (isTimelineTracked) { FlutterTimeline.finishSync(); @@ -4583,6 +4629,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { /// parent proactively calls the old parent's [deactivateChild], first using /// [forgetChild] to cause the old parent to update its child model. @protected + @mustCallSuper void deactivateChild(Element child) { assert(child._parent == this); child._parent = null; @@ -4598,6 +4645,38 @@ abstract class Element extends DiagnosticableTree implements BuildContext { }()); } + void _deactivateFailedChildSilently(Element child) { + try { + child._parent = null; + child.detachRenderObject(); + _deactivateFailedSubtreeRecursively(child); + } catch (_) { + // Do not rethrow: + // The subtree has already thrown a different error and the framework is + // cleaning up on a best-effort basis. + } + } + + // This method calls _ensureDeactivated for the subtree rooted at `element`, + // supressing all exceptions thrown. + // + // This method will attempt to keep doing treewalk even one of the nodes + // failed to deactivate. + // + // The subtree has already thrown a different error and the framework is + // cleaning up on a best-effort basis. + static void _deactivateFailedSubtreeRecursively(Element element) { + try { + element.deactivate(); + } catch (_) { + element._ensureDeactivated(); + } + element._lifecycleState = _ElementLifecycle.failed; + try { + element.visitChildren(_deactivateFailedSubtreeRecursively); + } catch (_) {} + } + // The children that have been forgotten by forgetChild. This will be used in // [update] to remove the global key reservations of forgotten children. // @@ -4672,6 +4751,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { /// Implementations of this method should start with a call to the inherited /// method, as in `super.activate()`. @mustCallSuper + @visibleForOverriding void activate() { assert(_lifecycleState == _ElementLifecycle.inactive); assert(owner != null); @@ -4701,18 +4781,34 @@ abstract class Element extends DiagnosticableTree implements BuildContext { /// animation frame, if the element has not be reactivated, the framework will /// unmount the element. /// - /// This is (indirectly) called by [deactivateChild]. + /// In case of an uncaught exception when rebuild a widget subtree, the + /// framework also calls this method on the failing subtree to make sure the + /// widget tree is in a relatively consistent state. The deactivation of such + /// subtrees are performed only on a best-effort basis, and the errors thrown + /// during deactivation will not be rethrown. + /// + /// This is indirectly called by [deactivateChild]. /// /// See the lifecycle documentation for [Element] for additional information. /// /// Implementations of this method should end with a call to the inherited /// method, as in `super.deactivate()`. @mustCallSuper + @visibleForOverriding void deactivate() { assert(_lifecycleState == _ElementLifecycle.active); assert(_widget != null); // Use the private property to avoid a CastError during hot reload. - if (_dependencies?.isNotEmpty ?? false) { - for (final InheritedElement dependency in _dependencies!) { + _ensureDeactivated(); + } + + /// Removes dependencies and sets the lifecycle state of this [Element] to + /// inactive. + /// + /// This method is immediately called after [Element.deactivate], even if that + /// call throws an exception. + void _ensureDeactivated() { + if (_dependencies case final Set dependencies? when dependencies.isNotEmpty) { + for (final InheritedElement dependency in dependencies) { dependency.removeDependent(this); } // For expediency, we don't actually clear the list here, even though it's @@ -4977,8 +5073,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { @override InheritedWidget dependOnInheritedElement(InheritedElement ancestor, {Object? aspect}) { - _dependencies ??= HashSet(); - _dependencies!.add(ancestor); + (_dependencies ??= HashSet()).add(ancestor); ancestor.updateDependencies(this, aspect); return ancestor.widget as InheritedWidget; } @@ -5714,7 +5809,7 @@ abstract class ComponentElement extends Element { @override @pragma('vm:notify-debugger-on-exception') void performRebuild() { - Widget? built; + Widget built; try { assert(() { _debugDoingBuild = true; @@ -5757,6 +5852,10 @@ abstract class ComponentElement extends Element { ], ), ); + // _Make sure the old child subtree are deactivated and disposed. + try { + _child?.deactivate(); + } catch (_) {} _child = updateChild(null, built, slot); } } @@ -6167,10 +6266,7 @@ class InheritedElement extends ProxyElement { @override void debugDeactivated() { - assert(() { - assert(_dependents.isEmpty); - return true; - }()); + assert(_dependents.isEmpty); super.debugDeactivated(); } @@ -6748,6 +6844,7 @@ abstract class RenderObjectElement extends Element { } @override + @visibleForOverriding void deactivate() { super.deactivate(); assert( @@ -6758,6 +6855,7 @@ abstract class RenderObjectElement extends Element { } @override + @visibleForOverriding void unmount() { assert( !renderObject.debugDisposed!, diff --git a/packages/flutter/test/cupertino/tab_test.dart b/packages/flutter/test/cupertino/tab_test.dart index 8fe9fd2605eac..82aafb83206d1 100644 --- a/packages/flutter/test/cupertino/tab_test.dart +++ b/packages/flutter/test/cupertino/tab_test.dart @@ -95,10 +95,6 @@ void main() { expect(tester.takeException(), isFlutterError); expect(unknownForRouteCalled, '/'); - - // Work-around for https://github.com/flutter/flutter/issues/65655. - await tester.pumpWidget(Container()); - expect(tester.takeException(), isAssertionError); }, ); diff --git a/packages/flutter/test/material/app_test.dart b/packages/flutter/test/material/app_test.dart index 302b387f871d4..9d06b2eb594f6 100644 --- a/packages/flutter/test/material/app_test.dart +++ b/packages/flutter/test/material/app_test.dart @@ -347,10 +347,6 @@ void main() { ); expect(tester.takeException(), isFlutterError); expect(log, ['onGenerateRoute /', 'onUnknownRoute /']); - - // Work-around for https://github.com/flutter/flutter/issues/65655. - await tester.pumpWidget(Container()); - expect(tester.takeException(), isAssertionError); }, ); diff --git a/packages/flutter/test/widgets/framework_test.dart b/packages/flutter/test/widgets/framework_test.dart index 98b141cddbc28..6da409f7d60c8 100644 --- a/packages/flutter/test/widgets/framework_test.dart +++ b/packages/flutter/test/widgets/framework_test.dart @@ -2075,6 +2075,71 @@ The findRenderObject() method was called for the following element: ), ); }); + + testWidgets( + 'widget is not active if throw in deactivated', + experimentalLeakTesting: LeakTesting.settings + .withIgnoredAll(), // leaking by design because of exception + (WidgetTester tester) async { + final FlutterExceptionHandler? onError = FlutterError.onError; + FlutterError.onError = (_) {}; + const Widget child = Placeholder(); + await tester.pumpWidget( + StatefulWidgetSpy(onDeactivate: (_) => throw StateError('kaboom'), child: child), + ); + final Element element = tester.element(find.byWidget(child)); + assert(element.debugIsActive); + + await tester.pumpWidget(const SizedBox()); + FlutterError.onError = onError; + expect(element.debugIsActive, false); + expect(element.debugIsDefunct, false); + }, + ); + + testWidgets( + 'widget is not active if throw in activated', + experimentalLeakTesting: LeakTesting.settings + .withIgnoredAll(), // leaking by design because of exception + (WidgetTester tester) async { + final FlutterExceptionHandler? onError = FlutterError.onError; + FlutterError.onError = (_) {}; + const Widget child = Placeholder(); + final Widget widget = StatefulWidgetSpy( + key: GlobalKey(), + onActivate: (_) => throw StateError('kaboom'), + child: child, + ); + await tester.pumpWidget(widget); + final Element element = tester.element(find.byWidget(child)); + + await tester.pumpWidget(MetaData(child: widget)); + FlutterError.onError = onError; + expect(element.debugIsActive, false); + expect(element.debugIsDefunct, false); + }, + ); + + testWidgets( + 'widget is unmounted if throw in dispose', + experimentalLeakTesting: LeakTesting.settings + .withIgnoredAll(), // leaking by design because of exception + (WidgetTester tester) async { + final FlutterExceptionHandler? onError = FlutterError.onError; + FlutterError.onError = (_) {}; + const Widget child = Placeholder(); + final Widget widget = StatefulWidgetSpy( + onDispose: (_) => throw StateError('kaboom'), + child: child, + ); + await tester.pumpWidget(widget); + final Element element = tester.element(find.byWidget(child)); + await tester.pumpWidget(child); + + FlutterError.onError = onError; + expect(element.debugIsDefunct, true); + }, + ); } class _TestInheritedElement extends InheritedElement { @@ -2323,6 +2388,7 @@ class StatefulWidgetSpy extends StatefulWidget { this.onDeactivate, this.onActivate, this.onDidUpdateWidget, + this.child = const SizedBox(), }); final void Function(BuildContext)? onBuild; @@ -2332,6 +2398,7 @@ class StatefulWidgetSpy extends StatefulWidget { final void Function(BuildContext)? onDeactivate; final void Function(BuildContext)? onActivate; final void Function(BuildContext)? onDidUpdateWidget; + final Widget child; @override State createState() => _StatefulWidgetSpyState(); @@ -2377,7 +2444,7 @@ class _StatefulWidgetSpyState extends State { @override Widget build(BuildContext context) { widget.onBuild?.call(context); - return Container(); + return widget.child; } } diff --git a/packages/flutter/test/widgets/memory_allocations_test.dart b/packages/flutter/test/widgets/memory_allocations_test.dart index 7cdbefe55368f..83f6b9585418f 100644 --- a/packages/flutter/test/widgets/memory_allocations_test.dart +++ b/packages/flutter/test/widgets/memory_allocations_test.dart @@ -80,6 +80,7 @@ class _TestElement extends RenderObjectElement with RootElementMixin { final FocusManager newFocusManager = FocusManager(); assignOwner(BuildOwner(focusManager: newFocusManager)); mount(null, null); + // ignore: invalid_use_of_visible_for_overriding_member deactivate(); } @@ -145,6 +146,7 @@ Future<_EventStats> _activateFlutterObjectsAndReturnCountOfEvents() async { element.makeInactive(); result.creations += 4; // 1 for the new BuildOwner, 1 for the new FocusManager, 1 for the new FocusScopeNode, 1 for the new _HighlightModeManager + // ignore: invalid_use_of_visible_for_overriding_member element.unmount(); result.disposals += 2; // 1 for the old BuildOwner, 1 for the element renderObject.dispose(); diff --git a/packages/flutter/test/widgets/navigator_restoration_test.dart b/packages/flutter/test/widgets/navigator_restoration_test.dart index 8e7b900fdc421..3e0abfecfd449 100644 --- a/packages/flutter/test/widgets/navigator_restoration_test.dart +++ b/packages/flutter/test/widgets/navigator_restoration_test.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; @@ -1095,18 +1094,6 @@ void main() { (exception as AssertionError).message, contains('All routes returned by onGenerateInitialRoutes are not restorable.'), ); - - // The previous assert leaves the widget tree in a broken state, so the - // following code catches any remaining exceptions from attempting to build - // new widget tree. - final FlutterExceptionHandler? oldHandler = FlutterError.onError; - dynamic remainingException; - FlutterError.onError = (FlutterErrorDetails details) { - remainingException ??= details.exception; - }; - await tester.pumpWidget(Container(key: UniqueKey())); - FlutterError.onError = oldHandler; - expect(remainingException, isAssertionError); }, ); } diff --git a/packages/flutter/test/widgets/tree_shape_test.dart b/packages/flutter/test/widgets/tree_shape_test.dart index 76dabaa83bbe6..d32ac4901ba79 100644 --- a/packages/flutter/test/widgets/tree_shape_test.dart +++ b/packages/flutter/test/widgets/tree_shape_test.dart @@ -94,29 +94,32 @@ void main() { }, ); - testWidgets('A View can not be moved via GlobalKey to be a child of a RenderObject', ( - WidgetTester tester, - ) async { - final Widget globalKeyedView = View( - key: GlobalKey(), - view: FakeView(tester.view), - child: const ColoredBox(color: Colors.red), - ); + testWidgets( + 'A View can not be moved via GlobalKey to be a child of a RenderObject', + experimentalLeakTesting: LeakTesting.settings + .withIgnoredAll(), // leaking by design because of exception + (WidgetTester tester) async { + final Widget globalKeyedView = View( + key: GlobalKey(), + view: FakeView(tester.view), + child: const ColoredBox(color: Colors.red), + ); - await tester.pumpWidget(wrapWithView: false, globalKeyedView); - expect(tester.takeException(), isNull); + await tester.pumpWidget(wrapWithView: false, globalKeyedView); + expect(tester.takeException(), isNull); - await tester.pumpWidget(wrapWithView: false, View(view: tester.view, child: globalKeyedView)); + await tester.pumpWidget(wrapWithView: false, View(view: tester.view, child: globalKeyedView)); - expect( - tester.takeException(), - isFlutterError.having( - (FlutterError error) => error.message, - 'message', - contains('cannot maintain an independent render tree at its current location.'), - ), - ); - }); + expect( + tester.takeException(), + isFlutterError.having( + (FlutterError error) => error.message, + 'message', + contains('cannot maintain an independent render tree at its current location.'), + ), + ); + }, + ); testWidgets('The view property of a ViewAnchor cannot be a render object widget', ( WidgetTester tester, diff --git a/packages/flutter/test_release/widgets/memory_allocations_test.dart b/packages/flutter/test_release/widgets/memory_allocations_test.dart index 659c6ccbf4352..b901c0ab3a807 100644 --- a/packages/flutter/test_release/widgets/memory_allocations_test.dart +++ b/packages/flutter/test_release/widgets/memory_allocations_test.dart @@ -60,6 +60,7 @@ class _TestElement extends RenderTreeRootElement with RootElementMixin { void makeInactive() { assignOwner(BuildOwner(focusManager: FocusManager())); mount(null, null); + // ignore: invalid_use_of_visible_for_overriding_member deactivate(); } @@ -95,6 +96,7 @@ class _MyStatefulWidgetState extends State<_MyStatefulWidget> { Future _activateFlutterObjects(WidgetTester tester) async { final _TestElement element = _TestElement(); element.makeInactive(); + // ignore: invalid_use_of_visible_for_overriding_member element.unmount(); // Create and dispose State: From c10f10596ee61f50db2385c8f7d9f32dbfdbbf12 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Thu, 14 Aug 2025 20:07:54 -0400 Subject: [PATCH 049/720] Writing a few solit initial integration tests --- .../windowing_test/.gitignore | 45 ++++++ .../windowing_test/.metadata | 27 ++++ .../windowing_test/README.md | 16 ++ .../windowing_test/analysis_options.yaml | 28 ++++ .../windowing_test/lib/main.dart | 147 ++++++++++++++++++ .../windowing_test/pubspec.yaml | 22 +++ .../test_driver/windowing_test.dart | 115 ++++++++++++++ .../windowing_test/windows/.gitignore | 17 ++ .../windowing_test/windows/CMakeLists.txt | 108 +++++++++++++ .../windows/flutter/CMakeLists.txt | 109 +++++++++++++ .../windows/runner/CMakeLists.txt | 37 +++++ .../windowing_test/windows/runner/Runner.rc | 111 +++++++++++++ .../windowing_test/windows/runner/main.cpp | 44 ++++++ .../windowing_test/windows/runner/resource.h | 19 +++ .../windows/runner/runner.exe.manifest | 14 ++ .../windowing_test/windows/runner/utils.cpp | 68 ++++++++ .../windowing_test/windows/runner/utils.h | 23 +++ 17 files changed, 950 insertions(+) create mode 100644 dev/integration_tests/windowing_test/.gitignore create mode 100644 dev/integration_tests/windowing_test/.metadata create mode 100644 dev/integration_tests/windowing_test/README.md create mode 100644 dev/integration_tests/windowing_test/analysis_options.yaml create mode 100644 dev/integration_tests/windowing_test/lib/main.dart create mode 100644 dev/integration_tests/windowing_test/pubspec.yaml create mode 100644 dev/integration_tests/windowing_test/test_driver/windowing_test.dart create mode 100644 dev/integration_tests/windowing_test/windows/.gitignore create mode 100644 dev/integration_tests/windowing_test/windows/CMakeLists.txt create mode 100644 dev/integration_tests/windowing_test/windows/flutter/CMakeLists.txt create mode 100644 dev/integration_tests/windowing_test/windows/runner/CMakeLists.txt create mode 100644 dev/integration_tests/windowing_test/windows/runner/Runner.rc create mode 100644 dev/integration_tests/windowing_test/windows/runner/main.cpp create mode 100644 dev/integration_tests/windowing_test/windows/runner/resource.h create mode 100644 dev/integration_tests/windowing_test/windows/runner/runner.exe.manifest create mode 100644 dev/integration_tests/windowing_test/windows/runner/utils.cpp create mode 100644 dev/integration_tests/windowing_test/windows/runner/utils.h diff --git a/dev/integration_tests/windowing_test/.gitignore b/dev/integration_tests/windowing_test/.gitignore new file mode 100644 index 0000000000000..3820a95c65c3e --- /dev/null +++ b/dev/integration_tests/windowing_test/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/dev/integration_tests/windowing_test/.metadata b/dev/integration_tests/windowing_test/.metadata new file mode 100644 index 0000000000000..56eedc2b5141e --- /dev/null +++ b/dev/integration_tests/windowing_test/.metadata @@ -0,0 +1,27 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "e27f0a6943e07b6d3420fb185432705b87101fa8" + channel: "[user-branch]" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: windows + create_revision: e27f0a6943e07b6d3420fb185432705b87101fa8 + base_revision: e27f0a6943e07b6d3420fb185432705b87101fa8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/dev/integration_tests/windowing_test/README.md b/dev/integration_tests/windowing_test/README.md new file mode 100644 index 0000000000000..6c3a63778aa3f --- /dev/null +++ b/dev/integration_tests/windowing_test/README.md @@ -0,0 +1,16 @@ +# windowing_test + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/dev/integration_tests/windowing_test/analysis_options.yaml b/dev/integration_tests/windowing_test/analysis_options.yaml new file mode 100644 index 0000000000000..0d2902135caec --- /dev/null +++ b/dev/integration_tests/windowing_test/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/dev/integration_tests/windowing_test/lib/main.dart b/dev/integration_tests/windowing_test/lib/main.dart new file mode 100644 index 0000000000000..42dda89385425 --- /dev/null +++ b/dev/integration_tests/windowing_test/lib/main.dart @@ -0,0 +1,147 @@ +// 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. + +// ignore_for_file: invalid_use_of_internal_member + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/src/foundation/_features.dart'; +import 'package:flutter/src/widgets/_window.dart'; +import 'package:flutter_driver/driver_extension.dart'; + +class _MainRegularWindowControllerDelegate + extends RegularWindowControllerDelegate { + @override + void onWindowDestroyed() { + super.onWindowDestroyed(); + + exit(0); + } +} + +late final RegularWindowController controller; + +void main() { + enableFlutterDriverExtension( + handler: (String? message) async { + if (message == null) { + return ''; + } + + final jsonMap = jsonDecode(message); + + if (jsonMap['type'] == 'get_size') { + return jsonEncode({ + 'width': controller.contentSize.width, + 'height': controller.contentSize.height, + }); + } else if (jsonMap['type'] == 'set_size') { + final Size size = Size( + jsonMap['width'].toDouble(), + jsonMap['height'].toDouble(), + ); + controller.setSize(size); + } else if (jsonMap['type'] == 'set_fullscreen') { + controller.setFullscreen(true); + } else if (jsonMap['type'] == 'unset_fullscreen') { + controller.setFullscreen(false); + } else if (jsonMap['type'] == 'get_fullscreen') { + return jsonEncode({'isFullscreen': controller.isFullscreen}); + } else if (jsonMap['type'] == 'set_maximized') { + controller.setMaximized(true); + } else if (jsonMap['type'] == 'unset_maximized') { + controller.setMaximized(false); + } else if (jsonMap['type'] == 'get_maximized') { + return jsonEncode({'isMaximized': controller.isMaximized}); + } else if (jsonMap['type'] == 'set_minimized') { + controller.setMinimized(true); + } else if (jsonMap['type'] == 'unset_minimized') { + controller.setMinimized(false); + } else if (jsonMap['type'] == 'get_minimized') { + return jsonEncode({'isMinimized': controller.isMinimized}); + } else if (jsonMap['type'] == 'set_title') { + controller.setTitle(jsonMap['title']); + } else if (jsonMap['type'] == 'get_title') { + return jsonEncode({'title': controller.title}); + } else if (jsonMap['type'] == 'set_activated') { + controller.activate(); + } else if (jsonMap['type'] == 'get_activated') { + return jsonEncode({'isActivated': controller.isActivated}); + } + + return ''; + }, + ); + isWindowingEnabled = true; + controller = RegularWindowController( + preferredSize: Size(640, 480), + title: 'Integration Test', + delegate: _MainRegularWindowControllerDelegate(), + ); + + runWidget(RegularWindow(controller: controller, child: MyApp())); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(widget.title), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('You have pushed the button this many times:'), + Text( + '$_counter', + style: Theme.of(context).textTheme.headlineMedium, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/dev/integration_tests/windowing_test/pubspec.yaml b/dev/integration_tests/windowing_test/pubspec.yaml new file mode 100644 index 0000000000000..b05793ba8dd40 --- /dev/null +++ b/dev/integration_tests/windowing_test/pubspec.yaml @@ -0,0 +1,22 @@ +name: windowing_test +description: Flutter windowing integration tests. + +environment: + sdk: ^3.8.0-0 + +dependencies: + flutter: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + test: any + + path: any + +dev_dependencies: + flutter_test: + sdk: flutter + +# PUBSPEC CHECKSUM: 77jv2r diff --git a/dev/integration_tests/windowing_test/test_driver/windowing_test.dart b/dev/integration_tests/windowing_test/test_driver/windowing_test.dart new file mode 100644 index 0000000000000..744de79fd8e63 --- /dev/null +++ b/dev/integration_tests/windowing_test/test_driver/windowing_test.dart @@ -0,0 +1,115 @@ +// 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:convert'; +import 'dart:io'; + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; + +void main() { + group('end-to-end test', () { + late FlutterDriver driver; + + setUpAll(() async { + driver = await FlutterDriver.connect(); + + // TODO(mattkae): I am unsure as to why this sleep is necessary + sleep(Duration(seconds: 1)); + }); + + tearDownAll(() async { + await driver.close(); + }); + + test('Can set and get title', () async { + await driver.requestData( + jsonEncode({'type': 'set_title', 'title': 'Hello World'}), + ); + final response = await driver.requestData( + jsonEncode({'type': 'get_title'}), + ); + final data = jsonDecode(response); + expect(data['title'], 'Hello World'); + }); + + test('Initial controller size is correct', () async { + final response = await driver.requestData( + jsonEncode({'type': 'get_size'}), + ); + final data = jsonDecode(response); + expect(data["width"], 640); + expect(data["height"], 480); + }); + + test('Can set and get size', () async { + await driver.requestData( + jsonEncode({'type': 'set_size', 'width': 800, 'height': 600}), + ); + final response = await driver.requestData( + jsonEncode({'type': 'get_size'}), + ); + final data = jsonDecode(response); + expect(data["width"], 800); + expect(data["height"], 600); + }); + + test('Can set and get fullscreen', () async { + await driver.requestData(jsonEncode({'type': 'set_fullscreen'})); + var response = await driver.requestData( + jsonEncode({'type': 'get_fullscreen'}), + ); + var data = jsonDecode(response); + expect(data["isFullscreen"], true); + + await driver.requestData(jsonEncode({'type': 'unset_fullscreen'})); + response = await driver.requestData( + jsonEncode({'type': 'get_fullscreen'}), + ); + data = jsonDecode(response); + expect(data["isFullscreen"], false); + }); + + test('Can set and get maximized', () async { + await driver.requestData(jsonEncode({'type': 'set_maximized'})); + var response = await driver.requestData( + jsonEncode({'type': 'get_maximized'}), + ); + var data = jsonDecode(response); + expect(data["isMaximized"], true); + + await driver.requestData(jsonEncode({'type': 'unset_maximized'})); + response = await driver.requestData( + jsonEncode({'type': 'get_maximized'}), + ); + data = jsonDecode(response); + expect(data["isMaximized"], false); + }); + + test('Can set and get minimized', () async { + await driver.requestData(jsonEncode({'type': 'set_minimized'})); + var response = await driver.requestData( + jsonEncode({'type': 'get_minimized'}), + ); + var data = jsonDecode(response); + expect(data["isMinimized"], true); + + await driver.requestData(jsonEncode({'type': 'unset_minimized'})); + response = await driver.requestData( + jsonEncode({'type': 'get_minimized'}), + ); + data = jsonDecode(response); + expect(data["isMinimized"], false); + }); + + test('Can set and get activated', () async { + await driver.requestData(jsonEncode({'type': 'set_activated'})); + final response = await driver.requestData( + jsonEncode({'type': 'get_activated'}), + ); + final data = jsonDecode(response); + expect(data["isActivated"], true); + }); + }); +} diff --git a/dev/integration_tests/windowing_test/windows/.gitignore b/dev/integration_tests/windowing_test/windows/.gitignore new file mode 100644 index 0000000000000..d492d0d98c8fd --- /dev/null +++ b/dev/integration_tests/windowing_test/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/dev/integration_tests/windowing_test/windows/CMakeLists.txt b/dev/integration_tests/windowing_test/windows/CMakeLists.txt new file mode 100644 index 0000000000000..4450980427578 --- /dev/null +++ b/dev/integration_tests/windowing_test/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(multi_window_ref_app LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "multi_window_ref_app") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/dev/integration_tests/windowing_test/windows/flutter/CMakeLists.txt b/dev/integration_tests/windowing_test/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000000000..a71c6e2c5e4f3 --- /dev/null +++ b/dev/integration_tests/windowing_test/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") +set(CMAKE_CXX_STANDARD 20) + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/dev/integration_tests/windowing_test/windows/runner/CMakeLists.txt b/dev/integration_tests/windowing_test/windows/runner/CMakeLists.txt new file mode 100644 index 0000000000000..697f43451ac08 --- /dev/null +++ b/dev/integration_tests/windowing_test/windows/runner/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "main.cpp" + "utils.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/dev/integration_tests/windowing_test/windows/runner/Runner.rc b/dev/integration_tests/windowing_test/windows/runner/Runner.rc new file mode 100644 index 0000000000000..909820ff45c09 --- /dev/null +++ b/dev/integration_tests/windowing_test/windows/runner/Runner.rc @@ -0,0 +1,111 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "The Flutter Authors" "\0" + VALUE "FileDescription", "A reference application demonstrating Flutter's multi-window API." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "Flutter Multi-Window Reference App" "\0" + VALUE "LegalCopyright", "Copyright 2014 The Flutter Authors. All rights reserved." "\0" + VALUE "OriginalFilename", "multi_window_ref_app.exe" "\0" + VALUE "ProductName", "Flutter Multi-Window Reference App" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/dev/integration_tests/windowing_test/windows/runner/main.cpp b/dev/integration_tests/windowing_test/windows/runner/main.cpp new file mode 100644 index 0000000000000..aae235e2b2baa --- /dev/null +++ b/dev/integration_tests/windowing_test/windows/runner/main.cpp @@ -0,0 +1,44 @@ +// 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. + +#include +#include +#include + +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) +{ + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) + { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + auto command_line_arguments{GetCommandLineArguments()}; + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + auto const engine{std::make_shared(project)}; + RegisterPlugins(engine.get()); + engine->Run(); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) + { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/dev/integration_tests/windowing_test/windows/runner/resource.h b/dev/integration_tests/windowing_test/windows/runner/resource.h new file mode 100644 index 0000000000000..69cacf3cead96 --- /dev/null +++ b/dev/integration_tests/windowing_test/windows/runner/resource.h @@ -0,0 +1,19 @@ +// 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. + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/dev/integration_tests/windowing_test/windows/runner/runner.exe.manifest b/dev/integration_tests/windowing_test/windows/runner/runner.exe.manifest new file mode 100644 index 0000000000000..153653e8d67f8 --- /dev/null +++ b/dev/integration_tests/windowing_test/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/dev/integration_tests/windowing_test/windows/runner/utils.cpp b/dev/integration_tests/windowing_test/windows/runner/utils.cpp new file mode 100644 index 0000000000000..6abcd65042070 --- /dev/null +++ b/dev/integration_tests/windowing_test/windows/runner/utils.cpp @@ -0,0 +1,68 @@ +// 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. + +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/dev/integration_tests/windowing_test/windows/runner/utils.h b/dev/integration_tests/windowing_test/windows/runner/utils.h new file mode 100644 index 0000000000000..54414c989ba71 --- /dev/null +++ b/dev/integration_tests/windowing_test/windows/runner/utils.h @@ -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. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ From 63626bf065d4f1759949c767fb9dddd9b257d21d Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Thu, 14 Aug 2025 20:16:02 -0400 Subject: [PATCH 050/720] Updating readme + adding more tests --- .../windowing_test/README.md | 18 +++++-------- .../windowing_test/analysis_options.yaml | 26 +------------------ .../windowing_test/lib/main.dart | 8 ++++++ .../test_driver/windowing_test.dart | 18 +++++++++++++ 4 files changed, 33 insertions(+), 37 deletions(-) diff --git a/dev/integration_tests/windowing_test/README.md b/dev/integration_tests/windowing_test/README.md index 6c3a63778aa3f..941b7d2659454 100644 --- a/dev/integration_tests/windowing_test/README.md +++ b/dev/integration_tests/windowing_test/README.md @@ -1,16 +1,10 @@ # windowing_test -A new Flutter project. +Integration tests for windowing. -## Getting Started +## Run +From this directory, run: -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +```sh +flutter drive -t .\lib\main.dart --driver .\test_driver\windowing_test.dart +``` diff --git a/dev/integration_tests/windowing_test/analysis_options.yaml b/dev/integration_tests/windowing_test/analysis_options.yaml index 0d2902135caec..f16eeada921c7 100644 --- a/dev/integration_tests/windowing_test/analysis_options.yaml +++ b/dev/integration_tests/windowing_test/analysis_options.yaml @@ -1,28 +1,4 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + rules: \ No newline at end of file diff --git a/dev/integration_tests/windowing_test/lib/main.dart b/dev/integration_tests/windowing_test/lib/main.dart index 42dda89385425..3e9bbb6966ce5 100644 --- a/dev/integration_tests/windowing_test/lib/main.dart +++ b/dev/integration_tests/windowing_test/lib/main.dart @@ -44,6 +44,14 @@ void main() { jsonMap['height'].toDouble(), ); controller.setSize(size); + } else if (jsonMap['type'] == 'set_constraints') { + final BoxConstraints constraints = BoxConstraints( + minWidth: jsonMap['min_width'].toDouble(), + minHeight: jsonMap['min_height'].toDouble(), + maxWidth: jsonMap['max_width'].toDouble(), + maxHeight: jsonMap['max_height'].toDouble(), + ); + controller.setConstraints(constraints); } else if (jsonMap['type'] == 'set_fullscreen') { controller.setFullscreen(true); } else if (jsonMap['type'] == 'unset_fullscreen') { diff --git a/dev/integration_tests/windowing_test/test_driver/windowing_test.dart b/dev/integration_tests/windowing_test/test_driver/windowing_test.dart index 744de79fd8e63..50a4ea26a1629 100644 --- a/dev/integration_tests/windowing_test/test_driver/windowing_test.dart +++ b/dev/integration_tests/windowing_test/test_driver/windowing_test.dart @@ -55,6 +55,24 @@ void main() { expect(data["height"], 600); }); + test('Can set constraints and see the resize', () async { + await driver.requestData( + jsonEncode({ + 'type': 'set_constraints', + 'min_width': 0, + 'min_height': 0, + 'max_width': 500, + 'max_height': 501, + }), + ); + final response = await driver.requestData( + jsonEncode({'type': 'get_size'}), + ); + final data = jsonDecode(response); + expect(data["width"], 500); + expect(data["height"], 501); + }); + test('Can set and get fullscreen', () async { await driver.requestData(jsonEncode({'type': 'set_fullscreen'})); var response = await driver.requestData( From 8dd7ba047373b5701d8ed96065e82d28fc7001f4 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 14 Aug 2025 16:19:24 -0400 Subject: [PATCH 051/720] Fixing errors in _window_win32.dart --- .../lib/src/widgets/_window_win32.dart | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index 9c0e81cd55616..09487a1168652 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -16,6 +16,7 @@ import 'dart:ffi' as ffi; import 'dart:io'; +import 'dart:typed_data'; import 'dart:ui' show Display, FlutterView; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -629,16 +630,6 @@ final class _Utf16 extends ffi.Opaque {} /// Extension method for converting a`Pointer` to a [String]. extension _Utf16Pointer on ffi.Pointer<_Utf16> { - /// The number of UTF-16 code units in this zero-terminated UTF-16 string. - /// - /// The UTF-16 code units of the strings are the non-zero code units up to - /// the first zero code unit. - int get length { - _ensureNotNullptr('length'); - final ffi.Pointer codeUnits = cast(); - return _length(codeUnits); - } - /// Converts this UTF-16 encoded string to a Dart string. /// /// Decodes the UTF-16 code units of this zero-terminated code unit array as @@ -677,14 +668,6 @@ extension _Utf16Pointer on ffi.Pointer<_Utf16> { } } - static int _length(ffi.Pointer codeUnits) { - int length = 0; - while (codeUnits[length] != 0) { - length++; - } - return length; - } - void _ensureNotNullptr(String operation) { if (this == ffi.nullptr) { throw UnsupportedError("Operation '$operation' not allowed on a 'nullptr'."); @@ -702,18 +685,22 @@ extension _StringUtf16Pointer on String { /// /// Returns an [allocator]-allocated pointer to the result. ffi.Pointer<_Utf16> toNativeUtf16({required ffi.Allocator allocator}) { - final units = codeUnits; - final result = allocator(units.length + 1); - final nativeString = result.asTypedList(units.length + 1); + final List units = codeUnits; + final ffi.Pointer result = allocator(units.length + 1); + final Uint16List nativeString = result.asTypedList(units.length + 1); nativeString.setRange(0, units.length, units); nativeString[units.length] = 0; return result.cast(); } } +// ignore: always_specify_types typedef _WinCoTaskMemAllocNative = ffi.Pointer Function(ffi.Size); +// ignore: always_specify_types typedef _WinCoTaskMemAlloc = ffi.Pointer Function(int); +// ignore: always_specify_types typedef _WinCoTaskMemFreeNative = ffi.Void Function(ffi.Pointer); +// ignore: always_specify_types typedef _WinCoTaskMemFree = void Function(ffi.Pointer); final class _CallocAllocator implements ffi.Allocator { @@ -732,15 +719,16 @@ final class _CallocAllocator implements ffi.Allocator { late final _WinCoTaskMemFree _winCoTaskMemFree; /// Fills a block of memory with a specified value. + // ignore: always_specify_types void _fillMemory(ffi.Pointer destination, int length, int fill) { - final ptr = destination.cast(); + final ffi.Pointer ptr = destination.cast(); for (int i = 0; i < length; i++) { ptr[i] = fill; } } /// Fills a block of memory with zeros. - /// + // ignore: always_specify_types void _zeroMemory(ffi.Pointer destination, int length) => _fillMemory(destination, length, 0); /// Allocates [byteCount] bytes of zero-initialized of memory on the native @@ -760,6 +748,7 @@ final class _CallocAllocator implements ffi.Allocator { /// Releases memory allocated on the native heap. @override + // ignore: always_specify_types void free(ffi.Pointer pointer) { _winCoTaskMemFree(pointer); } From d8186f7045f539a1e33708dc9ed78121e62eb7b4 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 14 Aug 2025 16:21:00 -0400 Subject: [PATCH 052/720] Roll Dart SDK from 214a7f829913 to c7faab270f27 (1 revision) (#173792) https://dart.googlesource.com/sdk.git/+log/214a7f829913..c7faab270f27 2025-08-14 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-99.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 chinmaygarde@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 b08ac1334dfce..c81c66cfbf1b6 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '214a7f8299135e56d01f3fd32596636f1fb6ba94', + 'dart_revision': 'c7faab270f275c030ea45804c222127f16ef21cf', # 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 8c02cf053c13d1ae5dc2940a10d79aa36f4519f0 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 14 Aug 2025 16:22:27 -0400 Subject: [PATCH 053/720] Removing non-windows platforms --- .../windows_startup_test/.metadata | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/dev/integration_tests/windows_startup_test/.metadata b/dev/integration_tests/windows_startup_test/.metadata index 7ec30051bc232..3624e0eb369cf 100644 --- a/dev/integration_tests/windows_startup_test/.metadata +++ b/dev/integration_tests/windows_startup_test/.metadata @@ -12,24 +12,6 @@ project_type: app # Tracks metadata for the flutter migrate command migration: platforms: - - platform: root - create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - - platform: android - create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - - platform: ios - create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - - platform: linux - create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - - platform: macos - create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - - platform: web - create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - platform: windows create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 @@ -42,4 +24,3 @@ migration: # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' From aad37619ba44918212fcbf41759771ddbefb105f Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 14 Aug 2025 16:23:25 -0400 Subject: [PATCH 054/720] Revert "Removing non-windows platforms" This reverts commit 8c02cf053c13d1ae5dc2940a10d79aa36f4519f0. --- .../windows_startup_test/.metadata | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dev/integration_tests/windows_startup_test/.metadata b/dev/integration_tests/windows_startup_test/.metadata index 3624e0eb369cf..7ec30051bc232 100644 --- a/dev/integration_tests/windows_startup_test/.metadata +++ b/dev/integration_tests/windows_startup_test/.metadata @@ -12,6 +12,24 @@ project_type: app # Tracks metadata for the flutter migrate command migration: platforms: + - platform: root + create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + - platform: android + create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + - platform: ios + create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + - platform: linux + create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + - platform: macos + create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + - platform: web + create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 + base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 - platform: windows create_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 base_revision: c865207540a1eb960aa89ac61ba89d0f0fa7bd17 @@ -24,3 +42,4 @@ migration: # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' From 414bbe65cb756aaf845f062459fa9c9acbe55af7 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 14 Aug 2025 16:24:09 -0400 Subject: [PATCH 055/720] Remove useless metadata line --- dev/integration_tests/windowing_test/.metadata | 1 - 1 file changed, 1 deletion(-) diff --git a/dev/integration_tests/windowing_test/.metadata b/dev/integration_tests/windowing_test/.metadata index 56eedc2b5141e..873e7828de703 100644 --- a/dev/integration_tests/windowing_test/.metadata +++ b/dev/integration_tests/windowing_test/.metadata @@ -24,4 +24,3 @@ migration: # Files that are not part of the templates will be ignored by default. unmanaged_files: - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' From 8914c91428486ce5d6fc1d2fc144329cf8dafd13 Mon Sep 17 00:00:00 2001 From: Houssem Eddine Fadhli Date: Thu, 14 Aug 2025 21:31:02 +0100 Subject: [PATCH 056/720] feat: add onLongPressUp callback to InkWell widget (#173221) This PR introduces a new optional callback, onLongPressUp, to the InkWell widget. It allows developers to respond specifically to the moment when a long press gesture is released, which previously was not directly exposed. **Before** There was no way to distinguish between the long press being held and the moment it ended. **After** InkWell now accepts an onLongPressUp callback, which fires when the user lifts their finger after a long press gesture. This change enhances gesture handling granularity for widgets that require different behaviors for long press hold and release. **Related Issue** #173390 **Tests** - Added a new test to ink_well_test.dart verifying that onLongPressUp is correctly triggered after a long press is released. **Checklist** - I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - I read the [Tree Hygiene] wiki page. - I followed the [Flutter Style Guide]. - I signed the [CLA]. - I updated/added relevant documentation (/// doc comments). - I added new tests to check the change I am making. - All existing and new tests are passing. --------- Co-authored-by: Victor Sanni Co-authored-by: engine-flutter-autoroll Co-authored-by: Reid Baker <1063596+reidbaker@users.noreply.github.com> Co-authored-by: Jon Ihlas Co-authored-by: Matthew Kosarek Co-authored-by: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Co-authored-by: Micael Cid Co-authored-by: Alexander Aprelev Co-authored-by: Tong Mu --- .../flutter/lib/src/material/ink_well.dart | 29 ++++++++- .../flutter/test/material/ink_well_test.dart | 59 +++++++++++++++++-- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index 2e8d6f41af848..e8d07db072940 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -311,6 +311,7 @@ class InkResponse extends StatelessWidget { this.onTapCancel, this.onDoubleTap, this.onLongPress, + this.onLongPressUp, this.onSecondaryTap, this.onSecondaryTapUp, this.onSecondaryTapDown, @@ -364,6 +365,19 @@ class InkResponse extends StatelessWidget { /// Called when the user long-presses on this part of the material. final GestureLongPressCallback? onLongPress; + /// Called when the user lifts their finger after a long press on the button. + /// + /// This callback is triggered at the end of a long press gesture, specifically + /// after the user holds a long press and then releases it. It does not include + /// position details. + /// + /// Common use cases include performing an action only after the long press completes, + /// such as displaying a context menu or confirming a held gesture. + /// + /// See also: + /// * [onLongPress], which is triggered when the long press gesture is first recognized. + final GestureLongPressUpCallback? onLongPressUp; + /// Called when the user taps this part of the material with a secondary button. /// /// See also: @@ -404,7 +418,7 @@ class InkResponse extends StatelessWidget { /// become highlighted and false if this part of the material has stopped /// being highlighted. /// - /// If all of [onTap], [onDoubleTap], and [onLongPress] become null while a + /// If all of [onTap], [onDoubleTap], [onLongPress], and [onLongPressUp] become null while a /// gesture is ongoing, then [onTapCancel] will be fired and /// [onHighlightChanged] will be fired with the value false _during the /// build_. This means, for instance, that in that scenario [State.setState] @@ -659,6 +673,7 @@ class InkResponse extends StatelessWidget { onTapCancel: onTapCancel, onDoubleTap: onDoubleTap, onLongPress: onLongPress, + onLongPressUp: onLongPressUp, onSecondaryTap: onSecondaryTap, onSecondaryTapUp: onSecondaryTapUp, onSecondaryTapDown: onSecondaryTapDown, @@ -716,6 +731,7 @@ class _InkResponseStateWidget extends StatefulWidget { this.onTapCancel, this.onDoubleTap, this.onLongPress, + this.onLongPressUp, this.onSecondaryTap, this.onSecondaryTapUp, this.onSecondaryTapDown, @@ -754,6 +770,7 @@ class _InkResponseStateWidget extends StatefulWidget { final GestureTapCallback? onTapCancel; final GestureTapCallback? onDoubleTap; final GestureLongPressCallback? onLongPress; + final GestureLongPressUpCallback? onLongPressUp; final GestureTapCallback? onSecondaryTap; final GestureTapUpCallback? onSecondaryTapUp; final GestureTapDownCallback? onSecondaryTapDown; @@ -794,6 +811,7 @@ class _InkResponseStateWidget extends StatefulWidget { if (onTap != null) 'tap', if (onDoubleTap != null) 'double tap', if (onLongPress != null) 'long press', + if (onLongPressUp != null) 'long press up', if (onTapDown != null) 'tap down', if (onTapUp != null) 'tap up', if (onTapCancel != null) 'tap cancel', @@ -1230,6 +1248,12 @@ class _InkResponseState extends State<_InkResponseStateWidget> } } + void handleLongPressUp() { + _currentSplash?.confirm(); + _currentSplash = null; + widget.onLongPressUp?.call(); + } + void handleSecondaryTap() { _currentSplash?.confirm(); _currentSplash = null; @@ -1271,6 +1295,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null || + widget.onLongPressUp != null || widget.onTapUp != null || widget.onTapDown != null; } @@ -1392,6 +1417,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> onTapCancel: _primaryEnabled ? handleTapCancel : null, onDoubleTap: widget.onDoubleTap != null ? handleDoubleTap : null, onLongPress: widget.onLongPress != null ? handleLongPress : null, + onLongPressUp: widget.onLongPressUp != null ? handleLongPressUp : null, onSecondaryTapDown: _secondaryEnabled ? handleSecondaryTapDown : null, onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp : null, onSecondaryTap: _secondaryEnabled ? handleSecondaryTap : null, @@ -1497,6 +1523,7 @@ class InkWell extends InkResponse { super.onTap, super.onDoubleTap, super.onLongPress, + super.onLongPressUp, super.onTapDown, super.onTapUp, super.onTapCancel, diff --git a/packages/flutter/test/material/ink_well_test.dart b/packages/flutter/test/material/ink_well_test.dart index 28e529755fe65..d46165eee4c36 100644 --- a/packages/flutter/test/material/ink_well_test.dart +++ b/packages/flutter/test/material/ink_well_test.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/src/services/keyboard_key.g.dart'; import 'package:flutter_test/flutter_test.dart'; + import '../widgets/feedback_tester.dart'; import '../widgets/semantics_tester.dart'; @@ -35,6 +36,9 @@ void main() { onLongPress: () { log.add('long-press'); }, + onLongPressUp: () { + log.add('long-press-up'); + }, onTapDown: (TapDownDetails details) { log.add('tap-down'); }, @@ -68,7 +72,7 @@ void main() { await tester.longPress(find.byType(InkWell), pointer: 4); - expect(log, equals(['tap-down', 'tap-cancel', 'long-press'])); + expect(log, equals(['tap-down', 'tap-cancel', 'long-press', 'long-press-up'])); log.clear(); TestGesture gesture = await tester.startGesture(tester.getRect(find.byType(InkWell)).center); @@ -142,6 +146,31 @@ void main() { expect(log, equals(['tap'])); }); + testWidgets('InkWell onLongPressUp callback is triggered', (WidgetTester tester) async { + bool wasCalled = false; + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: InkWell( + onLongPress: () {}, + onLongPressUp: () { + wasCalled = true; + }, + child: const SizedBox(width: 100, height: 100), + ), + ), + ), + ); + + final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(InkWell))); + await tester.pump(const Duration(seconds: 1)); + await gesture.up(); + await tester.pumpAndSettle(); + + expect(wasCalled, isTrue); + }); + testWidgets('long-press and tap on disabled should not throw', (WidgetTester tester) async { await tester.pumpWidget( const Material( @@ -173,6 +202,7 @@ void main() { highlightColor: const Color(0xf00fffff), onTap: () {}, onLongPress: () {}, + onLongPressUp: () {}, onHover: (bool hover) {}, ), ), @@ -220,6 +250,7 @@ void main() { }), onTap: () {}, onLongPress: () {}, + onLongPressUp: () {}, onHover: (bool hover) {}, ), ), @@ -260,6 +291,7 @@ void main() { highlightColor: const Color(0xf00fffff), onTap: () {}, onLongPress: () {}, + onLongPressUp: () {}, onHover: (bool hover) {}, ), ), @@ -312,6 +344,7 @@ void main() { highlightColor: const Color(0xf00fffff), onTap: () {}, onLongPress: () {}, + onLongPressUp: () {}, onHover: (bool hover) {}, ), ), @@ -495,6 +528,7 @@ void main() { highlightColor: const Color(0xf00fffff), onTap: () {}, onLongPress: () {}, + onLongPressUp: () {}, onHover: (bool hover) {}, ), ), @@ -553,6 +587,7 @@ void main() { }), onTap: () {}, onLongPress: () {}, + onLongPressUp: () {}, onHover: (bool hover) {}, ), ), @@ -1110,6 +1145,7 @@ void main() { highlightColor: const Color(0xf00fffff), onTap: () {}, onLongPress: () {}, + onLongPressUp: () {}, onHover: (bool hover) {}, ), ), @@ -1263,7 +1299,7 @@ void main() { child: Directionality( textDirection: TextDirection.ltr, child: Center( - child: InkWell(onTap: () {}, onLongPress: () {}), + child: InkWell(onTap: () {}, onLongPress: () {}, onLongPressUp: () {}), ), ), ), @@ -1290,7 +1326,12 @@ void main() { child: Directionality( textDirection: TextDirection.ltr, child: Center( - child: InkWell(onTap: () {}, onLongPress: () {}, enableFeedback: false), + child: InkWell( + onTap: () {}, + onLongPress: () {}, + onLongPressUp: () {}, + enableFeedback: false, + ), ), ), ), @@ -1411,6 +1452,7 @@ void main() { autofocus: true, onTap: () {}, onLongPress: () {}, + onLongPressUp: () {}, onHover: (bool hover) {}, focusNode: focusNode, child: Container(key: childKey), @@ -1452,6 +1494,7 @@ void main() { autofocus: true, onTap: () {}, onLongPress: () {}, + onLongPressUp: () {}, onHover: (bool hover) {}, focusNode: focusNode, child: Container(key: childKey), @@ -2175,7 +2218,7 @@ void main() { textDirection: TextDirection.ltr, child: Material( child: Center( - child: InkWell(onLongPress: () {}, child: const Text('Foo')), + child: InkWell(onLongPress: () {}, onLongPressUp: () {}, child: const Text('Foo')), ), ), ), @@ -2198,7 +2241,12 @@ void main() { textDirection: TextDirection.ltr, child: Material( child: Center( - child: InkWell(onLongPress: () {}, onTap: () {}, child: const Text('Foo')), + child: InkWell( + onLongPress: () {}, + onLongPressUp: () {}, + onTap: () {}, + child: const Text('Foo'), + ), ), ), ), @@ -2382,6 +2430,7 @@ void main() { }), onTap: () {}, onLongPress: () {}, + onLongPressUp: () {}, onHover: (bool hover) {}, ), ), From d1e2019a947690248a368ee37b5164e668cf6333 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Thu, 14 Aug 2025 13:39:12 -0700 Subject: [PATCH 057/720] Predictive back route transitions by default (#165832) This PR turns on predictive back route transitions by default on supported Android devices. With https://github.com/flutter/flutter/pull/154718, the default (PredictiveBackPageTransitionsBuilder) is the [shared element transition](https://developer.android.com/design/ui/mobile/guides/patterns/predictive-back#shared-element-transition). The [full screen transition](https://developer.android.com/design/ui/mobile/guides/patterns/predictive-back#full-screen-surfaces) is also available by using PredictiveBackFullScreenPageTransitionsBuilder. Original PR: https://github.com/flutter/flutter/pull/146788 Depends on: https://github.com/flutter/flutter/pull/154718 When this lands in stable, the docs should be updated: https://docs.flutter.dev/platform-integration/android/predictive-back --- .../test/material/divider/divider.0_test.dart | 6 +- .../test/widgets/basic/listener.0_test.dart | 9 +- .../widgets/basic/mouse_region.0_test.dart | 19 +- .../api/test/widgets/heroes/hero.0_test.dart | 14 +- .../transitions/align_transition.0_test.dart | 8 +- .../transitions/fade_transition.0_test.dart | 9 +- .../transitions/slide_transition.0_test.dart | 8 +- .../src/material/page_transitions_theme.dart | 10 +- .../test/material/bottom_app_bar_test.dart | 4 +- .../test/material/expansion_tile_test.dart | 8 +- .../material/flexible_space_bar_test.dart | 13 +- .../test/material/icon_button_test.dart | 4 +- .../test/material/navigation_drawer_test.dart | 2 +- .../navigation_drawer_theme_test.dart | 2 +- packages/flutter/test/material/page_test.dart | 23 +- .../material/page_transitions_theme_test.dart | 249 +++++++++++++++++- ...ve_back_page_transitions_builder_test.dart | 8 +- .../flutter/test/material/snack_bar_test.dart | 2 +- .../flutter/test/material/stepper_test.dart | 6 +- .../test/material/text_field_test.dart | 11 +- .../flutter/test/widgets/heroes_test.dart | 1 + .../flutter/test/widgets/navigator_test.dart | 4 +- .../test/widgets/slivers_evil_test.dart | 16 +- 23 files changed, 388 insertions(+), 48 deletions(-) diff --git a/examples/api/test/material/divider/divider.0_test.dart b/examples/api/test/material/divider/divider.0_test.dart index 5e9ffe36327fb..eb353e81a699f 100644 --- a/examples/api/test/material/divider/divider.0_test.dart +++ b/examples/api/test/material/divider/divider.0_test.dart @@ -13,7 +13,11 @@ void main() { expect(find.byType(Divider), findsOneWidget); // Divider is positioned horizontally. - final Offset container = tester.getBottomLeft(find.byType(ColoredBox).first); + final Offset container = tester.getBottomLeft( + find + .descendant(of: find.byType(example.DividerExample), matching: find.byType(ColoredBox)) + .first, + ); expect(container.dy, tester.getTopLeft(find.byType(Divider)).dy); final Offset subheader = tester.getTopLeft(find.text('Subheader')); diff --git a/examples/api/test/widgets/basic/listener.0_test.dart b/examples/api/test/widgets/basic/listener.0_test.dart index fbdc1f808103e..f27ebc56b0536 100644 --- a/examples/api/test/widgets/basic/listener.0_test.dart +++ b/examples/api/test/widgets/basic/listener.0_test.dart @@ -16,7 +16,14 @@ void main() { final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); - await gesture.down(tester.getCenter(find.byType(ColoredBox))); + await gesture.down( + tester.getCenter( + find.descendant( + of: find.byType(example.ListenerExample), + matching: find.byType(ColoredBox), + ), + ), + ); await tester.pump(); expect(find.text('1 presses\n0 releases'), findsOneWidget); diff --git a/examples/api/test/widgets/basic/mouse_region.0_test.dart b/examples/api/test/widgets/basic/mouse_region.0_test.dart index 2f2276a175433..4c2db4daa3de6 100644 --- a/examples/api/test/widgets/basic/mouse_region.0_test.dart +++ b/examples/api/test/widgets/basic/mouse_region.0_test.dart @@ -18,13 +18,28 @@ void main() { final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); - await gesture.moveTo(tester.getCenter(find.byType(ColoredBox))); + await gesture.moveTo( + tester.getCenter( + find.descendant( + of: find.byType(example.MouseRegionExample), + matching: find.byType(ColoredBox), + ), + ), + ); await tester.pump(); expect(find.text('1 Entries\n0 Exits'), findsOneWidget); expect(find.text('The cursor is here: (400.00, 328.00)'), findsOneWidget); - await gesture.moveTo(tester.getCenter(find.byType(ColoredBox)) + const Offset(50.0, 30.0)); + await gesture.moveTo( + tester.getCenter( + find.descendant( + of: find.byType(example.MouseRegionExample), + matching: find.byType(ColoredBox), + ), + ) + + const Offset(50.0, 30.0), + ); await tester.pump(); expect(find.text('The cursor is here: (450.00, 358.00)'), findsOneWidget); diff --git a/examples/api/test/widgets/heroes/hero.0_test.dart b/examples/api/test/widgets/heroes/hero.0_test.dart index 1b766748bff44..702b1607c2be3 100644 --- a/examples/api/test/widgets/heroes/hero.0_test.dart +++ b/examples/api/test/widgets/heroes/hero.0_test.dart @@ -24,19 +24,19 @@ void main() { expect(heroSize.height.roundToDouble(), 60.0); // Jump to 50% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 189.0); expect(heroSize.height.roundToDouble(), 146.0); // Jump to 75% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 199.0); expect(heroSize.height.roundToDouble(), 190.0); // Jump to 100% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize, const Size(200.0, 200.0)); @@ -45,25 +45,25 @@ void main() { await tester.pump(); // Jump 25% into the transition (total length = 300ms) - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 199.0); expect(heroSize.height.roundToDouble(), 190.0); // Jump to 50% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 189.0); expect(heroSize.height.roundToDouble(), 146.0); // Jump to 75% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 103.0); expect(heroSize.height.roundToDouble(), 60.0); // Jump to 100% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize, const Size(50.0, 50.0)); }); diff --git a/examples/api/test/widgets/transitions/align_transition.0_test.dart b/examples/api/test/widgets/transitions/align_transition.0_test.dart index 9cb1745424391..86f1813f892f4 100644 --- a/examples/api/test/widgets/transitions/align_transition.0_test.dart +++ b/examples/api/test/widgets/transitions/align_transition.0_test.dart @@ -9,7 +9,13 @@ import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Shows flutter logo in transition', (WidgetTester tester) async { await tester.pumpWidget(const example.AlignTransitionExampleApp()); - expect(find.byType(ColoredBox), findsOneWidget); + expect( + find.descendant( + of: find.byType(example.AlignTransitionExample), + matching: find.byType(ColoredBox), + ), + findsOneWidget, + ); expect( find.byWidgetPredicate( (Widget padding) => padding is Padding && padding.padding == const EdgeInsets.all(8.0), diff --git a/examples/api/test/widgets/transitions/fade_transition.0_test.dart b/examples/api/test/widgets/transitions/fade_transition.0_test.dart index 87d13827d21e1..1a04ca801f827 100644 --- a/examples/api/test/widgets/transitions/fade_transition.0_test.dart +++ b/examples/api/test/widgets/transitions/fade_transition.0_test.dart @@ -13,7 +13,10 @@ void main() { await tester.pumpWidget(const example.FadeTransitionExampleApp()); expect( - find.ancestor(of: find.byType(FlutterLogo), matching: find.byType(FadeTransition)), + find.descendant( + of: find.byType(example.FadeTransitionExample), + matching: find.byType(FadeTransition), + ), findsOneWidget, ); }); @@ -21,8 +24,8 @@ void main() { testWidgets('FadeTransition animates', (WidgetTester tester) async { await tester.pumpWidget(const example.FadeTransitionExampleApp()); - final Finder fadeTransitionFinder = find.ancestor( - of: find.byType(FlutterLogo), + final Finder fadeTransitionFinder = find.descendant( + of: find.byType(example.FadeTransitionExample), matching: find.byType(FadeTransition), ); diff --git a/examples/api/test/widgets/transitions/slide_transition.0_test.dart b/examples/api/test/widgets/transitions/slide_transition.0_test.dart index 18b279a964120..7dd51699174cc 100644 --- a/examples/api/test/widgets/transitions/slide_transition.0_test.dart +++ b/examples/api/test/widgets/transitions/slide_transition.0_test.dart @@ -13,7 +13,13 @@ void main() { expect(find.byType(Center), findsOneWidget); expect(find.byType(FlutterLogo), findsOneWidget); expect(find.byType(Padding), findsAtLeast(1)); - expect(find.byType(SlideTransition), findsOneWidget); + expect( + find.descendant( + of: find.byType(example.SlideTransitionExample), + matching: find.byType(SlideTransition), + ), + findsOneWidget, + ); }); testWidgets('Animates repeatedly every 2 seconds', (WidgetTester tester) async { diff --git a/packages/flutter/lib/src/material/page_transitions_theme.dart b/packages/flutter/lib/src/material/page_transitions_theme.dart index c7051b1c09582..14cc8000c1913 100644 --- a/packages/flutter/lib/src/material/page_transitions_theme.dart +++ b/packages/flutter/lib/src/material/page_transitions_theme.dart @@ -17,6 +17,7 @@ import 'package:flutter/services.dart'; import 'color_scheme.dart'; import 'colors.dart'; +import 'predictive_back_page_transitions_builder.dart'; import 'theme.dart'; // Slides the page upwards and fades it in, starting from 1/4 screen @@ -759,7 +760,12 @@ class FadeForwardsPageTransitionsBuilder extends PageTransitionsBuilder { final Color? backgroundColor; /// The value of [transitionDuration] in milliseconds. - static const int kTransitionMilliseconds = 800; + /// + /// Eyeballed on a physical Pixel 9 running Android 16. This does not match + /// the actual value used by native Android, which is 800ms, because native + /// Android is using Material 3 Expressive springs that are not currently + /// supported by Flutter. So for now at least, this is an approximation. + static const int kTransitionMilliseconds = 450; @override Duration get transitionDuration => const Duration(milliseconds: kTransitionMilliseconds); @@ -1097,7 +1103,7 @@ class PageTransitionsTheme with Diagnosticable { static const Map _defaultBuilders = { - TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), TargetPlatform.windows: ZoomPageTransitionsBuilder(), diff --git a/packages/flutter/test/material/bottom_app_bar_test.dart b/packages/flutter/test/material/bottom_app_bar_test.dart index 556681a6c3d2a..704411db303cc 100644 --- a/packages/flutter/test/material/bottom_app_bar_test.dart +++ b/packages/flutter/test/material/bottom_app_bar_test.dart @@ -191,7 +191,9 @@ void main() { final BottomAppBar bottomAppBar = tester.widget(find.byType(BottomAppBar)); expect(bottomAppBar.padding, customPadding); final Rect babRect = tester.getRect(find.byType(BottomAppBar)); - final Rect childRect = tester.getRect(find.byType(ColoredBox)); + final Rect childRect = tester.getRect( + find.descendant(of: find.byType(BottomAppBar), matching: find.byType(ColoredBox)), + ); expect(childRect, const Rect.fromLTRB(250, 530, 550, 590)); expect(babRect, const Rect.fromLTRB(240, 520, 560, 600)); }); diff --git a/packages/flutter/test/material/expansion_tile_test.dart b/packages/flutter/test/material/expansion_tile_test.dart index a73a1ece581ba..98c08f1de11fa 100644 --- a/packages/flutter/test/material/expansion_tile_test.dart +++ b/packages/flutter/test/material/expansion_tile_test.dart @@ -1690,7 +1690,9 @@ void main() { ); final Size materialAppSize = tester.getSize(find.byType(MaterialApp)); - final Size titleSize = tester.getSize(find.byType(ColoredBox)); + final Size titleSize = tester.getSize( + find.descendant(of: find.byType(ExpansionTile), matching: find.byType(ColoredBox)), + ); expect(titleSize.width, materialAppSize.width); }, @@ -1713,7 +1715,9 @@ void main() { ); final Size materialAppSize = tester.getSize(find.byType(MaterialApp)); - final Size titleSize = tester.getSize(find.byType(ColoredBox)); + final Size titleSize = tester.getSize( + find.descendant(of: find.byType(ExpansionTile), matching: find.byType(ColoredBox)), + ); expect(titleSize.width, materialAppSize.width - 32.0); }, diff --git a/packages/flutter/test/material/flexible_space_bar_test.dart b/packages/flutter/test/material/flexible_space_bar_test.dart index e417b75c384b1..04449422eb0cf 100644 --- a/packages/flutter/test/material/flexible_space_bar_test.dart +++ b/packages/flutter/test/material/flexible_space_bar_test.dart @@ -1462,7 +1462,18 @@ void main() { }); testWidgets('FlexibleSpaceBar rebuilds when scrolling.', (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: SubCategoryScreenView())); + await tester.pumpWidget( + MaterialApp( + home: const SubCategoryScreenView(), + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }, + ), + ), + ), + ); expect(RenderRebuildTracker.count, 1); expect( diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index b3291731626a8..55e3b216c1208 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -3027,7 +3027,9 @@ void main() { ), ); - final Offset topLeft = tester.getTopLeft(find.byType(ColoredBox)); + final Offset topLeft = tester.getTopLeft( + find.descendant(of: find.byType(Center), matching: find.byType(ColoredBox)), + ); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(topLeft); diff --git a/packages/flutter/test/material/navigation_drawer_test.dart b/packages/flutter/test/material/navigation_drawer_test.dart index 667be62ebfa45..96ca683a7467e 100644 --- a/packages/flutter/test/material/navigation_drawer_test.dart +++ b/packages/flutter/test/material/navigation_drawer_test.dart @@ -550,7 +550,7 @@ InkWell? _getInkWell(WidgetTester tester) { ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { return tester .firstWidget( - find.descendant(of: find.byType(FadeTransition), matching: find.byType(Container)), + find.descendant(of: find.byType(NavigationIndicator), matching: find.byType(Container)), ) .decoration as ShapeDecoration?; diff --git a/packages/flutter/test/material/navigation_drawer_theme_test.dart b/packages/flutter/test/material/navigation_drawer_theme_test.dart index 4c7cf54194f43..a7b164f5be404 100644 --- a/packages/flutter/test/material/navigation_drawer_theme_test.dart +++ b/packages/flutter/test/material/navigation_drawer_theme_test.dart @@ -284,7 +284,7 @@ Material _getMaterial(WidgetTester tester) { ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { return tester .firstWidget( - find.descendant(of: find.byType(FadeTransition), matching: find.byType(Container)), + find.descendant(of: find.byType(NavigationIndicator), matching: find.byType(Container)), ) .decoration as ShapeDecoration?; diff --git a/packages/flutter/test/material/page_test.dart b/packages/flutter/test/material/page_test.dart index 7b0a4d4610d03..f72f1d799e9f0 100644 --- a/packages/flutter/test/material/page_test.dart +++ b/packages/flutter/test/material/page_test.dart @@ -190,6 +190,13 @@ void main() { await tester.pumpWidget( MaterialApp( + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }, + ), + ), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( allowSnapshotting: false, @@ -268,7 +275,14 @@ void main() { RepaintBoundary( key: key, child: MaterialApp( - theme: ThemeData(useMaterial3: false), + theme: ThemeData( + useMaterial3: false, + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }, + ), + ), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { @@ -315,6 +329,13 @@ void main() { key: key, child: MaterialApp( debugShowCheckedModeBanner: false, // https://github.com/flutter/flutter/issues/143616 + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }, + ), + ), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { diff --git a/packages/flutter/test/material/page_transitions_theme_test.dart b/packages/flutter/test/material/page_transitions_theme_test.dart index bef2440dca09a..922a435313abd 100644 --- a/packages/flutter/test/material/page_transitions_theme_test.dart +++ b/packages/flutter/test/material/page_transitions_theme_test.dart @@ -75,7 +75,7 @@ void main() { ); testWidgets( - 'Default PageTransitionsTheme builds a _ZoomPageTransition for android', + 'Default PageTransitionsTheme builds a _FadeForwardsPageTransition for android', (WidgetTester tester) async { final Map routes = { '/': (BuildContext context) => Material( @@ -91,11 +91,11 @@ void main() { await tester.pumpWidget(MaterialApp(routes: routes)); - Finder findZoomPageTransition() { + Finder findFadeForwardsPageTransition() { return find.descendant( of: find.byType(MaterialApp), matching: find.byWidgetPredicate( - (Widget w) => '${w.runtimeType}' == '_ZoomPageTransition', + (Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition', ), ); } @@ -104,12 +104,12 @@ void main() { Theme.of(tester.element(find.text('push'))).platform, debugDefaultTargetPlatformOverride, ); - expect(findZoomPageTransition(), findsOneWidget); + expect(findFadeForwardsPageTransition(), findsOneWidget); await tester.tap(find.text('push')); await tester.pumpAndSettle(); expect(find.text('page b'), findsOneWidget); - expect(findZoomPageTransition(), findsOneWidget); + expect(findFadeForwardsPageTransition(), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android), ); @@ -1034,7 +1034,18 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('push'), findsNothing); + switch (defaultTargetPlatform) { + case TargetPlatform.android: + // Shows both pages while doing the "peek" predicitve back transition. + expect(find.text('push'), findsOneWidget); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + // Does no transition yet; still shows page b only. + expect(find.text('push'), findsNothing); + } expect(find.text('page b'), findsOneWidget); // Commit the system back gesture. @@ -1047,13 +1058,237 @@ void main() { (ByteData? _) {}, ); await tester.pumpAndSettle(); - expect(find.text('push'), findsOneWidget); expect(find.text('page b'), findsNothing); }, variant: TargetPlatformVariant.all(), ); + testWidgets('predictive back is the default on Android', (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => Material( + child: TextButton( + child: const Text('push'), + onPressed: () { + Navigator.of(context).pushNamed('/b'); + }, + ), + ), + }; + await tester.pumpWidget(MaterialApp(routes: routes)); + + final ThemeData themeData = Theme.of(tester.element(find.text('push'))); + switch (defaultTargetPlatform) { + case TargetPlatform.android: + expect( + themeData.pageTransitionsTheme.builders[defaultTargetPlatform], + isA(), + ); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + expect( + themeData.pageTransitionsTheme.builders[defaultTargetPlatform], + isNot(isA()), + ); + } + }, variant: TargetPlatformVariant.all()); + + testWidgets('predictive back falls back to ZoomPageTransitionBuilder', ( + WidgetTester tester, + ) async { + Finder findPredictiveBackPageTransition() { + return find.descendant( + of: find.byType(PrimaryScrollController), + matching: find.byWidgetPredicate( + (Widget w) => '${w.runtimeType}' == '_PredictiveBackSharedElementPageTransition', + ), + ); + } + + Finder findFallbackPageTransition() { + return find.descendant( + of: find.byType(PrimaryScrollController), + matching: find.byWidgetPredicate( + (Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition', + ), + ); + } + + final Map routes = { + '/': (BuildContext context) => Material( + child: TextButton( + child: const Text('push'), + onPressed: () { + Navigator.of(context).pushNamed('/b'); + }, + ), + ), + '/b': (BuildContext context) => const Text('page b'), + }; + + await tester.pumpWidget( + MaterialApp( + routes: routes, + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.iOS: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.macOS: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.windows: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.linux: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.fuchsia: PredictiveBackPageTransitionsBuilder(), + }, + ), + ), + ), + ); + + final ThemeData themeData = Theme.of(tester.element(find.text('push'))); + expect( + themeData.pageTransitionsTheme.builders[defaultTargetPlatform], + isA(), + ); + + expect(find.text('push'), findsOneWidget); + expect(find.text('page b'), findsNothing); + + await tester.tap(find.text('push')); + await tester.pumpAndSettle(); + + expect(find.text('push'), findsNothing); + expect(find.text('page b'), findsOneWidget); + + // Only Android sends system back gestures. + if (defaultTargetPlatform == TargetPlatform.android) { + final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( + const MethodCall('startBackGesture', { + 'touchOffset': [5.0, 300.0], + 'progress': 0.0, + 'swipeEdge': 0, // left + }), + ); + await binding.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/backgesture', + startMessage, + (ByteData? _) {}, + ); + await tester.pump(); + } + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + expect(findPredictiveBackPageTransition(), findsOneWidget); + expect(findFallbackPageTransition(), findsNothing); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + expect(findPredictiveBackPageTransition(), findsNothing); + expect(findFallbackPageTransition(), findsOneWidget); + } + + expect(find.text('push'), findsNothing); + expect(find.text('page b'), findsOneWidget); + + // Drag the system back gesture far enough to commit. + if (defaultTargetPlatform == TargetPlatform.android) { + final ByteData updateMessage = const StandardMethodCodec().encodeMethodCall( + const MethodCall('updateBackGestureProgress', { + 'x': 100.0, + 'y': 300.0, + 'progress': 0.35, + 'swipeEdge': 0, // left + }), + ); + await binding.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/backgesture', + updateMessage, + (ByteData? _) {}, + ); + await tester.pumpAndSettle(); + expect(find.text('push'), findsOneWidget); + } else { + expect(find.text('push'), findsNothing); + } + + expect(find.text('page b'), findsOneWidget); + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + expect(findPredictiveBackPageTransition(), findsNWidgets(2)); + expect(findFallbackPageTransition(), findsNothing); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + expect(findPredictiveBackPageTransition(), findsNothing); + expect(findFallbackPageTransition(), findsOneWidget); + } + + if (defaultTargetPlatform == TargetPlatform.android) { + // Commit the system back gesture on Android. + final ByteData commitMessage = const StandardMethodCodec().encodeMethodCall( + const MethodCall('commitBackGesture'), + ); + await binding.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/backgesture', + commitMessage, + (ByteData? _) {}, + ); + } else { + // On other platforms, send a one-off system pop. + final ByteData popMessage = const JSONMethodCodec().encodeMethodCall( + const MethodCall('popRoute'), + ); + await binding.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/navigation', + popMessage, + (ByteData? _) {}, + ); + } + await tester.pump(); + + expect(find.text('push'), findsOneWidget); + expect(find.text('page b'), findsOneWidget); + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + expect(findPredictiveBackPageTransition(), findsNWidgets(2)); + expect(findFallbackPageTransition(), findsNothing); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + expect(findPredictiveBackPageTransition(), findsNothing); + expect(findFallbackPageTransition(), findsNWidgets(2)); + } + + await tester.pumpAndSettle(); + + expect(find.text('push'), findsOneWidget); + expect(find.text('page b'), findsNothing); + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + expect(findPredictiveBackPageTransition(), findsNothing); + expect(findFallbackPageTransition(), findsOneWidget); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + expect(findPredictiveBackPageTransition(), findsNothing); + expect(findFallbackPageTransition(), findsOneWidget); + } + }, variant: TargetPlatformVariant.all()); + testWidgets( 'ZoomPageTransitionsBuilder uses theme color during transition effects', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart b/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart index 42200f91f4f20..e595d3ab4b686 100644 --- a/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart +++ b/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart @@ -63,7 +63,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackPageTransition for the page transition. + // _PredictiveBackSharedElementPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], @@ -173,7 +173,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackPageTransition for the page transition. + // _PredictiveBackSharedElementPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], @@ -326,7 +326,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackPageTransition for the page transition. + // _PredictiveBackSharedElementPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], @@ -488,7 +488,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackPageTransition for the page transition. + // _PredictiveBackSharedElementPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], diff --git a/packages/flutter/test/material/snack_bar_test.dart b/packages/flutter/test/material/snack_bar_test.dart index 351a6ef32811a..cf8baa0b8477f 100644 --- a/packages/flutter/test/material/snack_bar_test.dart +++ b/packages/flutter/test/material/snack_bar_test.dart @@ -2753,7 +2753,7 @@ void main() { expect(find.text(snackBarText), findsOneWidget); expect(find.text(firstHeader), findsOneWidget); expect(find.text(secondHeader), findsOneWidget); - await tester.pump(const Duration(milliseconds: 750)); + await tester.pump(const Duration(milliseconds: 1500)); expect(find.text(snackBarText), findsOneWidget); expect(find.text(firstHeader), findsNothing); expect(find.text(secondHeader), findsOneWidget); diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart index 9d150d970eea5..0f7b565eaab19 100644 --- a/packages/flutter/test/material/stepper_test.dart +++ b/packages/flutter/test/material/stepper_test.dart @@ -1298,7 +1298,11 @@ void main() { ?.color; Color lineColor() { - return tester.widget(find.byType(ColoredBox)).color; + return tester + .widget( + find.descendant(of: find.byType(Stepper), matching: find.byType(ColoredBox)), + ) + .color; } // Step 1 diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 0061595456a23..fec45654f019b 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -2122,10 +2122,15 @@ void main() { await tester.tap(find.byType(TextField)); // Wait for context menu to be built. await tester.pumpAndSettle(); - final RenderBox container = tester.renderObject( - find.descendant(of: find.byType(SnapshotWidget), matching: find.byType(SizedBox)).first, + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + final SizedBox sizedBox = tester.widget( + find.descendant( + of: find.byType(AdaptiveTextSelectionToolbar), + matching: find.byType(SizedBox), + ), ); - expect(container.size, Size.zero); + expect(sizedBox.width, 0.0); + expect(sizedBox.height, 0.0); }, variant: const TargetPlatformVariant({ TargetPlatform.android, diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index 5192603c482f8..fd2335afc6bb3 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -1449,6 +1449,7 @@ Future main() async { .text('Hero') .evaluate() .map((Element e) => e.renderObject!); + await tester.pump(const Duration(milliseconds: 1)); expect(renderObjects.where(isVisible).length, 1); // Hero BC's flight finishes normally. diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index 8ba7ddb118021..b69d131040d06 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -2976,12 +2976,12 @@ void main() { await tester.pump(kFourTenthsOfTheTransitionDuration); expect(find.text('Route: 3'), findsOneWidget); expect(find.text('Route: 4'), findsOneWidget); - expect(route4Entry.value, 0.4); + expect(route4Entry.value, moreOrLessEquals(0.4)); await tester.pump(kFourTenthsOfTheTransitionDuration); expect(find.text('Route: 3'), findsOneWidget); expect(find.text('Route: 4'), findsOneWidget); - expect(route4Entry.value, 0.8); + expect(route4Entry.value, moreOrLessEquals(0.8)); expect(find.text('Route: 2', skipOffstage: false), findsOneWidget); expect(find.text('Route: 1', skipOffstage: false), findsOneWidget); expect(find.text('Route: root', skipOffstage: false), findsOneWidget); diff --git a/packages/flutter/test/widgets/slivers_evil_test.dart b/packages/flutter/test/widgets/slivers_evil_test.dart index ef5beda4cf266..6f00fdacaabd0 100644 --- a/packages/flutter/test/widgets/slivers_evil_test.dart +++ b/packages/flutter/test/widgets/slivers_evil_test.dart @@ -241,9 +241,17 @@ void main() { await tester.drag(find.text('5'), const Offset(0.0, -500.0)); await tester.pump(); + Finder findItem(String text) { + return find.descendant( + of: find.byType(SliverFixedExtentList), + matching: find.widgetWithText(ColoredBox, text), + ); + } + // Screen is 600px high. Moved bottom item 500px up. It's now at the top. - expect(tester.getTopLeft(find.widgetWithText(ColoredBox, '5')).dy, 0.0); - expect(tester.getBottomLeft(find.widgetWithText(ColoredBox, '10')).dy, 600.0); + expect(findItem('5'), findsOneWidget); + expect(tester.getTopLeft(findItem('5')).dy, 0.0); + expect(tester.getBottomLeft(findItem('10')).dy, 600.0); // Stop returning the first 3 items. await tester.pumpWidget( @@ -271,10 +279,10 @@ void main() { // Move up by 4 items, meaning item 1 would have been at the top but // 0 through 3 no longer exist, so item 4, 3 items down, is the first one. // Item 4 is also shifted to the top. - expect(tester.getTopLeft(find.widgetWithText(ColoredBox, '4')).dy, 0.0); + expect(tester.getTopLeft(findItem('4')).dy, 0.0); // Because the screen is still 600px, item 9 is now visible at the bottom instead // of what's supposed to be item 6 had we not re-shifted. - expect(tester.getBottomLeft(find.widgetWithText(ColoredBox, '9')).dy, 600.0); + expect(tester.getBottomLeft(findItem('9')).dy, 600.0); }); } From b02b37bfd9e034f75387a99b1abd437aa4a33ca3 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 14 Aug 2025 16:50:33 -0400 Subject: [PATCH 058/720] Fixing import errors on web based platforms --- packages/flutter/lib/src/widgets/_window.dart | 8 ++--- .../flutter/lib/src/widgets/_window_ffi.dart | 35 +++++++++++++++++++ .../flutter/lib/src/widgets/_window_web.dart | 28 +++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 packages/flutter/lib/src/widgets/_window_ffi.dart create mode 100644 packages/flutter/lib/src/widgets/_window_web.dart diff --git a/packages/flutter/lib/src/widgets/_window.dart b/packages/flutter/lib/src/widgets/_window.dart index f38e022d2f260..8746bdef3faf1 100644 --- a/packages/flutter/lib/src/widgets/_window.dart +++ b/packages/flutter/lib/src/widgets/_window.dart @@ -14,14 +14,13 @@ // // See: https://github.com/flutter/flutter/issues/30701. -import 'dart:io'; import 'dart:ui' show Display, FlutterView; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import '../foundation/_features.dart'; -import '_window_win32.dart'; +import '_window_ffi.dart' if (dart.library.js_util) '_window_web.dart' as window_impl; const String _kWindowingDisabledErrorMessage = ''' Windowing APIs are not enabled. @@ -405,8 +404,9 @@ abstract class WindowingOwner { return _WindowingOwnerUnsupported(errorMessage: _kWindowingDisabledErrorMessage); } - if (Platform.isWindows) { - return WindowingOwnerWin32(); + final WindowingOwner? owner = window_impl.createDefaultOwner(); + if (owner != null) { + return owner; } return _WindowingOwnerUnsupported(errorMessage: 'Windowing is unsupported on this platform.'); diff --git a/packages/flutter/lib/src/widgets/_window_ffi.dart b/packages/flutter/lib/src/widgets/_window_ffi.dart new file mode 100644 index 0000000000000..fa10a4ba0ef1e --- /dev/null +++ b/packages/flutter/lib/src/widgets/_window_ffi.dart @@ -0,0 +1,35 @@ +// 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. + +// Do not import this file in production applications or packages published +// to pub.dev. Flutter will make breaking changes to this file, even in patch +// versions. +// +// All APIs in this file must be private or must: +// +// 1. Have the `@internal` attribute. +// 2. Throw an `UnsupportedError` if `isWindowingEnabled` +// is `false. +// +// See: https://github.com/flutter/flutter/issues/30701. + +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +import '_window.dart'; +import '_window_win32.dart'; + +/// Creates a default [WindowingOwner] for current platform. +/// Only supported on desktop platforms. +/// +/// {@macro flutter.widgets.windowing.experimental} +@internal +WindowingOwner? createDefaultOwner() { + if (Platform.isWindows) { + return WindowingOwnerWin32(); + } else { + return null; + } +} diff --git a/packages/flutter/lib/src/widgets/_window_web.dart b/packages/flutter/lib/src/widgets/_window_web.dart new file mode 100644 index 0000000000000..ff7e0170ea669 --- /dev/null +++ b/packages/flutter/lib/src/widgets/_window_web.dart @@ -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. + +// Do not import this file in production applications or packages published +// to pub.dev. Flutter will make breaking changes to this file, even in patch +// versions. +// +// All APIs in this file must be private or must: +// +// 1. Have the `@internal` attribute. +// 2. Throw an `UnsupportedError` if `isWindowingEnabled` +// is `false. +// +// See: https://github.com/flutter/flutter/issues/30701. + +import 'package:flutter/foundation.dart'; + +import '_window.dart'; + +/// Creates a default [WindowingOwner] for web. Returns `null` as web does not +/// support multiple windows. +/// +/// {@macro flutter.widgets.windowing.experimental} +@internal +WindowingOwner? createDefaultOwner() { + return null; +} From a8ae5e07fdf36d17337134450f2561ad86812052 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 14 Aug 2025 13:52:26 -0700 Subject: [PATCH 059/720] Read `bin/cache/flutter.version.json` instead of `version` for `flutter_gallery` (#173797) Towards https://github.com/flutter/flutter/issues/171900. --- .../flutter_gallery/android/app/build.gradle | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dev/integration_tests/flutter_gallery/android/app/build.gradle b/dev/integration_tests/flutter_gallery/android/app/build.gradle index fdedf7ad1ff3c..ca12c1b02a078 100644 --- a/dev/integration_tests/flutter_gallery/android/app/build.gradle +++ b/dev/integration_tests/flutter_gallery/android/app/build.gradle @@ -21,8 +21,11 @@ if (flutterRoot == null) { } // The Gallery app's version is defined by the Flutter framework's version. -def flutterVersion = file("$flutterRoot/version").text +def flutterVersionFile = file("$flutterRoot/bin/cache/flutter.version.json") +def flutterVersionJson = new groovy.json.JsonSlurper().parse(flutterVersionFile) +def flutterVersion = flutterVersionJson.frameworkVersion def flutterVersionComponents = flutterVersion.split(/[^0-9]+/) + // Let the integer version code of xx.yy.zz-pre.nnn be xxyyzznnn. int flutterVersionNumber = Math.min(flutterVersionComponents[0].toInteger(), 20) * 100000000 flutterVersionNumber += Math.min(flutterVersionComponents[1].toInteger(), 99) * 1000000 From 2e407a47e7dde76d7414bc66ce013c1c56ea1dbf Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 14 Aug 2025 16:59:12 -0400 Subject: [PATCH 060/720] Two minor PR feedback nits --- packages/flutter/lib/src/widgets/_window.dart | 2 +- packages/flutter/lib/src/widgets/_window_win32.dart | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/widgets/_window.dart b/packages/flutter/lib/src/widgets/_window.dart index 8746bdef3faf1..804224b5a161e 100644 --- a/packages/flutter/lib/src/widgets/_window.dart +++ b/packages/flutter/lib/src/widgets/_window.dart @@ -509,7 +509,7 @@ class RegularWindow extends StatelessWidget { Widget build(BuildContext context) { return ListenableBuilder( listenable: controller, - builder: (BuildContext context, Widget? _) => WindowScope( + builder: (BuildContext context, Widget? widget) => WindowScope( controller: controller, child: View(view: controller.rootView, child: child), ), diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index 09487a1168652..d889c112ae1ac 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -54,7 +54,9 @@ abstract class WindowsMessageHandler { /// Handles a window message. /// /// Returned value, if not null will be returned to the system as LRESULT - /// and will stop all other handlers from being called. + /// and will stop all other handlers from being called. See + /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-wndproc + /// for more information. /// /// {@macro flutter.widgets.windowing.experimental} @internal From 231d3466cc8363b66e93a3bd5cdda2054584003d Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Fri, 15 Aug 2025 09:00:50 +1200 Subject: [PATCH 061/720] Return result of setting OpenGL contexts back to Flutter (#173757) Also check when using them internally that they have worked. --- .../platform/linux/fl_compositor_opengl.cc | 14 ++++++++++++-- .../flutter/shell/platform/linux/fl_engine.cc | 17 +++++++++-------- .../shell/platform/linux/fl_opengl_manager.cc | 17 +++++++++-------- .../shell/platform/linux/fl_opengl_manager.h | 12 +++++++++--- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc index 4e6a10c14fdfc..e0fd95739ea9c 100644 --- a/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc +++ b/engine/src/flutter/shell/platform/linux/fl_compositor_opengl.cc @@ -113,7 +113,12 @@ static gchar* get_program_log(GLuint program) { } static void setup_shader(FlCompositorOpenGL* self) { - fl_opengl_manager_make_current(self->opengl_manager); + if (!fl_opengl_manager_make_current(self->opengl_manager)) { + g_warning( + "Failed to setup compositor shaders, unable to make OpenGL context " + "current"); + return; + } GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertex_shader, 1, &vertex_shader_src, nullptr); @@ -167,7 +172,12 @@ static void setup_shader(FlCompositorOpenGL* self) { } static void cleanup_shader(FlCompositorOpenGL* self) { - fl_opengl_manager_make_current(self->opengl_manager); + if (!fl_opengl_manager_make_current(self->opengl_manager)) { + g_warning( + "Failed to cleanup compositor shaders, unable to make OpenGL context " + "current"); + return; + } if (self->program != 0) { glDeleteProgram(self->program); diff --git a/engine/src/flutter/shell/platform/linux/fl_engine.cc b/engine/src/flutter/shell/platform/linux/fl_engine.cc index 528d42ff64381..97056423dc53a 100644 --- a/engine/src/flutter/shell/platform/linux/fl_engine.cc +++ b/engine/src/flutter/shell/platform/linux/fl_engine.cc @@ -251,7 +251,9 @@ static bool create_opengl_backing_store( FlEngine* self, const FlutterBackingStoreConfig* config, FlutterBackingStore* backing_store_out) { - fl_opengl_manager_make_current(self->opengl_manager); + if (!fl_opengl_manager_make_current(self->opengl_manager)) { + return false; + } GLint sized_format = GL_RGBA8; GLint general_format = GL_RGBA; @@ -285,7 +287,9 @@ static bool create_opengl_backing_store( static bool collect_opengl_backing_store( FlEngine* self, const FlutterBackingStore* backing_store) { - fl_opengl_manager_make_current(self->opengl_manager); + if (!fl_opengl_manager_make_current(self->opengl_manager)) { + return false; + } // OpenGL context is required when destroying #FlFramebuffer. g_object_unref(backing_store->open_gl.framebuffer.user_data); @@ -381,14 +385,12 @@ static void* fl_engine_gl_proc_resolver(void* user_data, const char* name) { static bool fl_engine_gl_make_current(void* user_data) { FlEngine* self = static_cast(user_data); - fl_opengl_manager_make_current(self->opengl_manager); - return true; + return fl_opengl_manager_make_current(self->opengl_manager); } static bool fl_engine_gl_clear_current(void* user_data) { FlEngine* self = static_cast(user_data); - fl_opengl_manager_clear_current(self->opengl_manager); - return true; + return fl_opengl_manager_clear_current(self->opengl_manager); } static uint32_t fl_engine_gl_get_fbo(void* user_data) { @@ -398,8 +400,7 @@ static uint32_t fl_engine_gl_get_fbo(void* user_data) { static bool fl_engine_gl_make_resource_current(void* user_data) { FlEngine* self = static_cast(user_data); - fl_opengl_manager_make_resource_current(self->opengl_manager); - return true; + return fl_opengl_manager_make_resource_current(self->opengl_manager); } // Called by the engine to retrieve an external texture. diff --git a/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc b/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc index 9a734d673bd16..c6d7d62d30c20 100644 --- a/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc +++ b/engine/src/flutter/shell/platform/linux/fl_opengl_manager.cc @@ -73,16 +73,17 @@ FlOpenGLManager* fl_opengl_manager_new() { return self; } -void fl_opengl_manager_make_current(FlOpenGLManager* self) { - eglMakeCurrent(self->display, EGL_NO_SURFACE, EGL_NO_SURFACE, - self->render_context); +gboolean fl_opengl_manager_make_current(FlOpenGLManager* self) { + return eglMakeCurrent(self->display, EGL_NO_SURFACE, EGL_NO_SURFACE, + self->render_context) == EGL_TRUE; } -void fl_opengl_manager_make_resource_current(FlOpenGLManager* self) { - eglMakeCurrent(self->display, EGL_NO_SURFACE, EGL_NO_SURFACE, - self->resource_context); +gboolean fl_opengl_manager_make_resource_current(FlOpenGLManager* self) { + return eglMakeCurrent(self->display, EGL_NO_SURFACE, EGL_NO_SURFACE, + self->resource_context) == EGL_TRUE; } -void fl_opengl_manager_clear_current(FlOpenGLManager* self) { - eglMakeCurrent(self->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); +gboolean fl_opengl_manager_clear_current(FlOpenGLManager* self) { + return eglMakeCurrent(self->display, EGL_NO_SURFACE, EGL_NO_SURFACE, + EGL_NO_CONTEXT) == EGL_TRUE; } diff --git a/engine/src/flutter/shell/platform/linux/fl_opengl_manager.h b/engine/src/flutter/shell/platform/linux/fl_opengl_manager.h index 1079e24f9b71d..6130594b664f6 100644 --- a/engine/src/flutter/shell/platform/linux/fl_opengl_manager.h +++ b/engine/src/flutter/shell/platform/linux/fl_opengl_manager.h @@ -29,24 +29,30 @@ FlOpenGLManager* fl_opengl_manager_new(); * @manager: an #FlOpenGLManager. * * Makes the rendering context current. + * + * Returns: %TRUE if the context made current. */ -void fl_opengl_manager_make_current(FlOpenGLManager* manager); +gboolean fl_opengl_manager_make_current(FlOpenGLManager* manager); /** * fl_opengl_manager_make_resource_current: * @manager: an #FlOpenGLManager. * * Makes the resource rendering context current. + * + * Returns: %TRUE if the context made current. */ -void fl_opengl_manager_make_resource_current(FlOpenGLManager* manager); +gboolean fl_opengl_manager_make_resource_current(FlOpenGLManager* manager); /** * fl_opengl_manager_clear_current: * @manager: an #FlOpenGLManager. * * Clears the current rendering context. + * + * Returns: %TRUE if the context cleared. */ -void fl_opengl_manager_clear_current(FlOpenGLManager* manager); +gboolean fl_opengl_manager_clear_current(FlOpenGLManager* manager); G_END_DECLS From 3c1e258864aedcdb93261cded7aa3acc8c99e375 Mon Sep 17 00:00:00 2001 From: Ahmed Mohamed Sameh Date: Fri, 15 Aug 2025 00:17:12 +0300 Subject: [PATCH 062/720] Make sure that a DatePickerDialog doesn't crash in 0x0 environment (#173677) This is my attempt to handle https://github.com/flutter/flutter/issues/6537 for the DatePickerDialog UI control. --- packages/flutter/test/material/date_picker_test.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart index e81686dff566a..a74042d27f007 100644 --- a/packages/flutter/test/material/date_picker_test.dart +++ b/packages/flutter/test/material/date_picker_test.dart @@ -2750,6 +2750,16 @@ void main() { expect(lastDayText.data, equals('28')); }); }); + + testWidgets('DatePickerDialog renders at zero area', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: SizedBox.shrink( + child: DatePickerDialog(firstDate: firstDate, lastDate: lastDate), + ), + ), + ); + }); } class _RestorableDatePickerDialogTestWidget extends StatefulWidget { From 87bac26665f20e2a1930af3cd48ec525c1140aab Mon Sep 17 00:00:00 2001 From: Ahmed Mohamed Sameh Date: Fri, 15 Aug 2025 00:19:01 +0300 Subject: [PATCH 063/720] Make sure that a TableRowInkWell doesn't crash in 0x0 environment (#173627) This is my attempt to handle https://github.com/flutter/flutter/issues/6537 for the TableRowInkWell UI control. --- .../flutter/test/material/data_table_test.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/flutter/test/material/data_table_test.dart b/packages/flutter/test/material/data_table_test.dart index 0f23c1fb7663b..34b2a9c3641c6 100644 --- a/packages/flutter/test/material/data_table_test.dart +++ b/packages/flutter/test/material/data_table_test.dart @@ -1905,6 +1905,22 @@ void main() { expect(secondaryTapped, isTrue); expect(secondaryTappedDown, isTrue); }); + + testWidgets('TableRowInkWell renders at zero area', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Material( + child: SizedBox.shrink( + child: Table( + children: const [ + TableRow(children: [TableRowInkWell(child: Text('X'))]), + ], + ), + ), + ), + ), + ); + }); }); testWidgets('Heading cell cursor resolves MaterialStateMouseCursor correctly', ( From 667da94d3c95f99f4288b225109cda5b6a49fb01 Mon Sep 17 00:00:00 2001 From: Ahmed Mohamed Sameh Date: Fri, 15 Aug 2025 00:19:01 +0300 Subject: [PATCH 064/720] Make sure that DataTable, DataColumn, DataRow, and DataCell don't crash in 0x0 environment (#173515) This is my attempt to handle https://github.com/flutter/flutter/issues/6537 for the DataTable, DataColumn, DataRow, and DataCell UI controls. --- .../flutter/test/material/data_table_test.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/flutter/test/material/data_table_test.dart b/packages/flutter/test/material/data_table_test.dart index 34b2a9c3641c6..bf4dd62a03a33 100644 --- a/packages/flutter/test/material/data_table_test.dart +++ b/packages/flutter/test/material/data_table_test.dart @@ -2331,6 +2331,23 @@ void main() { semantics.dispose(); }); + + testWidgets('DataTable, DataColumn, DataRow, and DataCell render at zero area', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: SizedBox.shrink( + child: DataTable( + columns: const [DataColumn(label: Text('X'))], + rows: const [ + DataRow(cells: [DataCell(Text('X'))]), + ], + ), + ), + ), + ); + }); } RenderParagraph _getTextRenderObject(WidgetTester tester, String text) { From 62904ba72aa397142116ce40a67be12ab5792617 Mon Sep 17 00:00:00 2001 From: Mouad Debbar Date: Thu, 14 Aug 2025 18:51:28 -0400 Subject: [PATCH 065/720] [web] Cleanup usages of deprecated `routeUpdated` message (#173782) This is a follow up to https://github.com/flutter/flutter/pull/173652 Closes https://github.com/flutter/flutter/issues/50836 --- .../lib/web_ui/lib/src/engine/window.dart | 5 -- .../lib/web_ui/test/engine/history_test.dart | 28 +++----- .../web_ui/test/engine/navigation_test.dart | 4 +- .../lib/web_ui/test/engine/routing_test.dart | 64 ++++++------------- 4 files changed, 30 insertions(+), 71 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart index d0ffdf7713d68..5e2bf758bef0b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/window.dart @@ -625,11 +625,6 @@ final class EngineFlutterWindow extends EngineFlutterView implements ui.Singleto await _useSingleEntryBrowserHistory(); return true; // the following cases assert that arguments are not null - case 'routeUpdated': // deprecated - assert(arguments != null); - await _useSingleEntryBrowserHistory(); - browserHistory.setRouteName(arguments!.tryString('routeName')); - return true; case 'routeInformationUpdated': assert(arguments != null); final String? uriString = arguments!.tryString('uri'); diff --git a/engine/src/flutter/lib/web_ui/test/engine/history_test.dart b/engine/src/flutter/lib/web_ui/test/engine/history_test.dart index 40af749fecce8..402408cbcedeb 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/history_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/history_test.dart @@ -186,7 +186,7 @@ void testMain() { expect(strategy.history, hasLength(2)); expect(strategy.currentEntry.state, flutterState); expect(strategy.currentEntry.url, '/home'); - await routeUpdated('/page1'); + await routeInformationUpdated('/page1', null); // The number of entries shouldn't change. expect(strategy.history, hasLength(2)); expect(strategy.currentEntryIndex, 1); @@ -204,7 +204,7 @@ void testMain() { expect(spy.messages[0].methodName, 'popRoute'); expect(spy.messages[0].methodArguments, isNull); // The framework responds by updating to the most current route name. - await routeUpdated('/home'); + await routeInformationUpdated('/home', null); // We still have 2 entries. expect(strategy.history, hasLength(2)); expect(strategy.currentEntryIndex, 1); @@ -219,8 +219,8 @@ void testMain() { ); await implicitView.debugInitializeHistory(strategy, useSingle: true); - await routeUpdated('/page1'); - await routeUpdated('/page2'); + await routeInformationUpdated('/page1', null); + await routeInformationUpdated('/page2', null); // Make sure we are on page2. expect(strategy.history, hasLength(2)); @@ -237,7 +237,7 @@ void testMain() { expect(spy.messages[0].methodArguments, isNull); spy.messages.clear(); // 2. The framework sends a `routePopped` platform message. - await routeUpdated('/page1'); + await routeInformationUpdated('/page1', null); // 3. The history state should reflect that /page1 is currently active. expect(strategy.history, hasLength(2)); expect(strategy.currentEntryIndex, 1); @@ -253,7 +253,7 @@ void testMain() { expect(spy.messages[0].methodArguments, isNull); spy.messages.clear(); // 2. The framework sends a `routePopped` platform message. - await routeUpdated('/home'); + await routeInformationUpdated('/home', null); // 3. The history state should reflect that /page1 is currently active. expect(strategy.history, hasLength(2)); expect(strategy.currentEntryIndex, 1); @@ -296,7 +296,7 @@ void testMain() { expect(spy.messages[0].methodArguments, '/page3'); spy.messages.clear(); // 2. The framework sends a `routeUpdated` platform message. - await routeUpdated('/page3'); + await routeInformationUpdated('/page3', null); // 3. The history state should reflect that /page3 is currently active. expect(strategy.history, hasLength(3)); expect(strategy.currentEntryIndex, 1); @@ -312,7 +312,7 @@ void testMain() { expect(spy.messages[0].methodArguments, isNull); spy.messages.clear(); // 2. The framework sends a `routeUpdated` platform message. - await routeUpdated('/home'); + await routeInformationUpdated('/home', null); // 3. The history state should reflect that /home is currently active. expect(strategy.history, hasLength(2)); expect(strategy.currentEntryIndex, 1); @@ -351,7 +351,7 @@ void testMain() { await implicitView.debugInitializeHistory(strategy, useSingle: true); // Go to a named route. - await routeUpdated('/named-route'); + await routeInformationUpdated('/named-route', null); expect(strategy.currentEntry.url, '/named-route'); // Now, push a nameless route. The url shouldn't change. @@ -761,16 +761,6 @@ void testMain() { }); } -Future routeUpdated(String routeName) { - final Completer completer = Completer(); - EnginePlatformDispatcher.instance.sendPlatformMessage( - 'flutter/navigation', - codec.encodeMethodCall(MethodCall('routeUpdated', {'routeName': routeName})), - (_) => completer.complete(), - ); - return completer.future; -} - Future routeInformationUpdated(String location, dynamic state) { final Completer completer = Completer(); EnginePlatformDispatcher.instance.sendPlatformMessage( diff --git a/engine/src/flutter/lib/web_ui/test/engine/navigation_test.dart b/engine/src/flutter/lib/web_ui/test/engine/navigation_test.dart index 1b602da8f3501..d4b490b31904a 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/navigation_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/navigation_test.dart @@ -29,7 +29,7 @@ void testMain() { ui.PlatformDispatcher.instance.sendPlatformMessage( 'flutter/navigation', codec.encodeMethodCall( - const MethodCall('routeUpdated', {'routeName': '/foo'}), + const MethodCall('routeInformationUpdated', {'location': '/foo'}), ), (ByteData? response) => completer.complete(response), ); @@ -53,7 +53,7 @@ void testMain() { ui.PlatformDispatcher.instance.sendPlatformMessage( 'flutter/navigation', codec.encodeMethodCall( - const MethodCall('routeUpdated', {'routeName': '/foo'}), + const MethodCall('routeInformationUpdated', {'location': '/foo'}), ), (_) => completer.complete(), ); diff --git a/engine/src/flutter/lib/web_ui/test/engine/routing_test.dart b/engine/src/flutter/lib/web_ui/test/engine/routing_test.dart index eb0afd8ed48b5..a76d0e4f70e97 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/routing_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/routing_test.dart @@ -78,7 +78,7 @@ void testMain() { myWindow.sendPlatformMessage( 'flutter/navigation', const JSONMethodCodec().encodeMethodCall( - const MethodCall('routeUpdated', {'routeName': '/bar'}), + const MethodCall('routeInformationUpdated', {'location': '/bar'}), ), (_) { callback.complete(); @@ -125,17 +125,14 @@ void testMain() { 'selectMultiEntryHistory', {}, ); // -> multi - await check('routeUpdated', { - 'routeName': '/bar', - }); // -> single - await check('routeInformationUpdated', { + await check('routeInformationUpdated', { 'location': '/bar', }); // does not change mode - await check( - 'selectMultiEntryHistory', + await check( + 'selectSingleEntryHistory', {}, - ); // -> multi - await check('routeInformationUpdated', { + ); // -> single + await check('routeInformationUpdated', { 'location': '/bar', }); // does not change mode }); @@ -143,12 +140,6 @@ void testMain() { test( 'handleNavigationMessage throws for route update methods called with null arguments', () async { - expect(() async { - await myWindow.handleNavigationMessage( - const JSONMethodCodec().encodeMethodCall(const MethodCall('routeUpdated')), - ); - }, throwsAssertionError); - expect(() async { await myWindow.handleNavigationMessage( const JSONMethodCodec().encodeMethodCall(const MethodCall('routeInformationUpdated')), @@ -214,12 +205,21 @@ void testMain() { ); expect(myWindow.browserHistory, isA()); - // routeUpdated resets the history type + // change the history type Completer callback = Completer(); + myWindow.sendPlatformMessage( + 'flutter/navigation', + const JSONMethodCodec().encodeMethodCall(const MethodCall('selectSingleEntryHistory')), + (_) { + callback.complete(); + }, + ); + await callback.future; + callback = Completer(); myWindow.sendPlatformMessage( 'flutter/navigation', const JSONMethodCodec().encodeMethodCall( - const MethodCall('routeUpdated', {'routeName': '/bar'}), + const MethodCall('routeInformationUpdated', {'location': '/bar'}), ), (_) { callback.complete(); @@ -250,7 +250,7 @@ void testMain() { // they can be interleaved safely await myWindow.handleNavigationMessage( const JSONMethodCodec().encodeMethodCall( - const MethodCall('routeUpdated', {'routeName': '/foo'}), + const MethodCall('routeInformationUpdated', {'location': '/foo'}), ), ); expect(myWindow.browserHistory, isA()); @@ -390,32 +390,6 @@ void testMain() { expect(myWindow.browserHistory.urlStrategy!.getState(), _tagStateWithSerialCount('/state1', 1)); }); - test('initialize browser history with default url strategy (single)', () async { - // On purpose, we don't initialize history on the window. We want to let the - // window to self-initialize when it receives a navigation message. - - // Without initializing history, the default route name should be - // initialized to "/" in tests. - expect(myWindow.defaultRouteName, '/'); - - final Completer callback = Completer(); - myWindow.sendPlatformMessage( - 'flutter/navigation', - const JSONMethodCodec().encodeMethodCall( - const MethodCall('routeUpdated', {'routeName': '/bar'}), - ), - (_) { - callback.complete(); - }, - ); - await callback.future; - expect(myWindow.browserHistory, isA()); - // The url strategy should've been set to the default, and the path - // should've been correctly set to "/bar". - expect(myWindow.browserHistory.urlStrategy, isNot(isNull)); - expect(myWindow.browserHistory.urlStrategy!.getPath(), '/bar'); - }, skip: isSafari); // https://github.com/flutter/flutter/issues/50836 - test('initialize browser history with default url strategy (multiple)', () async { // On purpose, we don't initialize history on the window. We want to let the // window to self-initialize when it receives a navigation message. @@ -443,7 +417,7 @@ void testMain() { // should've been correctly set to "/baz". expect(myWindow.browserHistory.urlStrategy, isNot(isNull)); expect(myWindow.browserHistory.urlStrategy!.getPath(), '/baz'); - }, skip: isSafari); // https://github.com/flutter/flutter/issues/50836 + }); test('can disable location strategy', () async { // Disable URL strategy. From 9583f282a50f88a269f5716f1ecfe42f6b6387a3 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 14 Aug 2025 18:53:01 -0400 Subject: [PATCH 066/720] Roll Skia from dca5f05fee87 to ad5d04000101 (8 revisions) (#173798) https://skia.googlesource.com/skia.git/+log/dca5f05fee87..ad5d04000101 2025-08-14 alecmouri@google.com Write correct sBIT for 10-bit BGRX 2025-08-14 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2025-08-14 nicolettep@google.com [graphite] Add spvtools_val dependency 2025-08-14 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2025-08-14 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 1d9ad72b24bd to 07db58fa4a14 (2 revisions) 2025-08-14 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2025-08-14 robertphillips@google.com Rehabilitate some Win11 jobs 2025-08-14 robertphillips@google.com Rehabilitate Mac14/ANGLE job 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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 c81c66cfbf1b6..cf0ecca6185a4 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': 'dca5f05fee87961bd96a83ead6549838b725660d', + 'skia_revision': 'ad5d04000101c356e572c45b7f26e42270a601a1', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 0e57a627f555bc302d61437205798ab10c3a6523 Mon Sep 17 00:00:00 2001 From: "auto-submit[bot]" <98614782+auto-submit[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 23:38:48 +0000 Subject: [PATCH 067/720] Reverts "Predictive back route transitions by default (#165832)" (#173809) Reverts: flutter/flutter#165832 Initiated by: matanlurey Reason for reverting: Breaks `Linux_pixel_7pro embedded_android_views_integration_test`: - https://ci.chromium.org/ui/p/flutter/builders/prod/Linux_pixel_7pro%20embedded_android_views_integration_test/8918/overview - https://ci.chromium.org/ui/p/flutter/builders/prod/Linux_pixel_7pro%20embedded_android_views_integration_test/8917/overview ```txt [2025-08-14 16:01:17.600761] [STDOUT] stdout: [ +1 ms] Expecte Original PR Author: justinmc Reviewed By: {QuncCccccc} This change reverts the following previous change: This PR turns on predictive back route transitions by default on supported Android devices. With https://github.com/flutter/flutter/pull/154718, the default (PredictiveBackPageTransitionsBuilder) is the [shared element transition](https://developer.android.com/design/ui/mobile/guides/patterns/predictive-back#shared-element-transition). The [full screen transition](https://developer.android.com/design/ui/mobile/guides/patterns/predictive-back#full-screen-surfaces) is also available by using PredictiveBackFullScreenPageTransitionsBuilder. Original PR: https://github.com/flutter/flutter/pull/146788 Depends on: https://github.com/flutter/flutter/pull/154718 When this lands in stable, the docs should be updated: https://docs.flutter.dev/platform-integration/android/predictive-back Co-authored-by: auto-submit[bot] --- .../test/material/divider/divider.0_test.dart | 6 +- .../test/widgets/basic/listener.0_test.dart | 9 +- .../widgets/basic/mouse_region.0_test.dart | 19 +- .../api/test/widgets/heroes/hero.0_test.dart | 14 +- .../transitions/align_transition.0_test.dart | 8 +- .../transitions/fade_transition.0_test.dart | 9 +- .../transitions/slide_transition.0_test.dart | 8 +- .../src/material/page_transitions_theme.dart | 10 +- .../test/material/bottom_app_bar_test.dart | 4 +- .../test/material/expansion_tile_test.dart | 8 +- .../material/flexible_space_bar_test.dart | 13 +- .../test/material/icon_button_test.dart | 4 +- .../test/material/navigation_drawer_test.dart | 2 +- .../navigation_drawer_theme_test.dart | 2 +- packages/flutter/test/material/page_test.dart | 23 +- .../material/page_transitions_theme_test.dart | 249 +----------------- ...ve_back_page_transitions_builder_test.dart | 8 +- .../flutter/test/material/snack_bar_test.dart | 2 +- .../flutter/test/material/stepper_test.dart | 6 +- .../test/material/text_field_test.dart | 11 +- .../flutter/test/widgets/heroes_test.dart | 1 - .../flutter/test/widgets/navigator_test.dart | 4 +- .../test/widgets/slivers_evil_test.dart | 16 +- 23 files changed, 48 insertions(+), 388 deletions(-) diff --git a/examples/api/test/material/divider/divider.0_test.dart b/examples/api/test/material/divider/divider.0_test.dart index eb353e81a699f..5e9ffe36327fb 100644 --- a/examples/api/test/material/divider/divider.0_test.dart +++ b/examples/api/test/material/divider/divider.0_test.dart @@ -13,11 +13,7 @@ void main() { expect(find.byType(Divider), findsOneWidget); // Divider is positioned horizontally. - final Offset container = tester.getBottomLeft( - find - .descendant(of: find.byType(example.DividerExample), matching: find.byType(ColoredBox)) - .first, - ); + final Offset container = tester.getBottomLeft(find.byType(ColoredBox).first); expect(container.dy, tester.getTopLeft(find.byType(Divider)).dy); final Offset subheader = tester.getTopLeft(find.text('Subheader')); diff --git a/examples/api/test/widgets/basic/listener.0_test.dart b/examples/api/test/widgets/basic/listener.0_test.dart index f27ebc56b0536..fbdc1f808103e 100644 --- a/examples/api/test/widgets/basic/listener.0_test.dart +++ b/examples/api/test/widgets/basic/listener.0_test.dart @@ -16,14 +16,7 @@ void main() { final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); - await gesture.down( - tester.getCenter( - find.descendant( - of: find.byType(example.ListenerExample), - matching: find.byType(ColoredBox), - ), - ), - ); + await gesture.down(tester.getCenter(find.byType(ColoredBox))); await tester.pump(); expect(find.text('1 presses\n0 releases'), findsOneWidget); diff --git a/examples/api/test/widgets/basic/mouse_region.0_test.dart b/examples/api/test/widgets/basic/mouse_region.0_test.dart index 4c2db4daa3de6..2f2276a175433 100644 --- a/examples/api/test/widgets/basic/mouse_region.0_test.dart +++ b/examples/api/test/widgets/basic/mouse_region.0_test.dart @@ -18,28 +18,13 @@ void main() { final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); - await gesture.moveTo( - tester.getCenter( - find.descendant( - of: find.byType(example.MouseRegionExample), - matching: find.byType(ColoredBox), - ), - ), - ); + await gesture.moveTo(tester.getCenter(find.byType(ColoredBox))); await tester.pump(); expect(find.text('1 Entries\n0 Exits'), findsOneWidget); expect(find.text('The cursor is here: (400.00, 328.00)'), findsOneWidget); - await gesture.moveTo( - tester.getCenter( - find.descendant( - of: find.byType(example.MouseRegionExample), - matching: find.byType(ColoredBox), - ), - ) + - const Offset(50.0, 30.0), - ); + await gesture.moveTo(tester.getCenter(find.byType(ColoredBox)) + const Offset(50.0, 30.0)); await tester.pump(); expect(find.text('The cursor is here: (450.00, 358.00)'), findsOneWidget); diff --git a/examples/api/test/widgets/heroes/hero.0_test.dart b/examples/api/test/widgets/heroes/hero.0_test.dart index 702b1607c2be3..1b766748bff44 100644 --- a/examples/api/test/widgets/heroes/hero.0_test.dart +++ b/examples/api/test/widgets/heroes/hero.0_test.dart @@ -24,19 +24,19 @@ void main() { expect(heroSize.height.roundToDouble(), 60.0); // Jump to 50% into the transition. - await tester.pump(quarterTransition); + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 189.0); expect(heroSize.height.roundToDouble(), 146.0); // Jump to 75% into the transition. - await tester.pump(quarterTransition); + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 199.0); expect(heroSize.height.roundToDouble(), 190.0); // Jump to 100% into the transition. - await tester.pump(quarterTransition); + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms heroSize = tester.getSize(find.byType(Container)); expect(heroSize, const Size(200.0, 200.0)); @@ -45,25 +45,25 @@ void main() { await tester.pump(); // Jump 25% into the transition (total length = 300ms) - await tester.pump(quarterTransition); + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 199.0); expect(heroSize.height.roundToDouble(), 190.0); // Jump to 50% into the transition. - await tester.pump(quarterTransition); + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 189.0); expect(heroSize.height.roundToDouble(), 146.0); // Jump to 75% into the transition. - await tester.pump(quarterTransition); + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 103.0); expect(heroSize.height.roundToDouble(), 60.0); // Jump to 100% into the transition. - await tester.pump(quarterTransition); + await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms heroSize = tester.getSize(find.byType(Container)); expect(heroSize, const Size(50.0, 50.0)); }); diff --git a/examples/api/test/widgets/transitions/align_transition.0_test.dart b/examples/api/test/widgets/transitions/align_transition.0_test.dart index 86f1813f892f4..9cb1745424391 100644 --- a/examples/api/test/widgets/transitions/align_transition.0_test.dart +++ b/examples/api/test/widgets/transitions/align_transition.0_test.dart @@ -9,13 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Shows flutter logo in transition', (WidgetTester tester) async { await tester.pumpWidget(const example.AlignTransitionExampleApp()); - expect( - find.descendant( - of: find.byType(example.AlignTransitionExample), - matching: find.byType(ColoredBox), - ), - findsOneWidget, - ); + expect(find.byType(ColoredBox), findsOneWidget); expect( find.byWidgetPredicate( (Widget padding) => padding is Padding && padding.padding == const EdgeInsets.all(8.0), diff --git a/examples/api/test/widgets/transitions/fade_transition.0_test.dart b/examples/api/test/widgets/transitions/fade_transition.0_test.dart index 1a04ca801f827..87d13827d21e1 100644 --- a/examples/api/test/widgets/transitions/fade_transition.0_test.dart +++ b/examples/api/test/widgets/transitions/fade_transition.0_test.dart @@ -13,10 +13,7 @@ void main() { await tester.pumpWidget(const example.FadeTransitionExampleApp()); expect( - find.descendant( - of: find.byType(example.FadeTransitionExample), - matching: find.byType(FadeTransition), - ), + find.ancestor(of: find.byType(FlutterLogo), matching: find.byType(FadeTransition)), findsOneWidget, ); }); @@ -24,8 +21,8 @@ void main() { testWidgets('FadeTransition animates', (WidgetTester tester) async { await tester.pumpWidget(const example.FadeTransitionExampleApp()); - final Finder fadeTransitionFinder = find.descendant( - of: find.byType(example.FadeTransitionExample), + final Finder fadeTransitionFinder = find.ancestor( + of: find.byType(FlutterLogo), matching: find.byType(FadeTransition), ); diff --git a/examples/api/test/widgets/transitions/slide_transition.0_test.dart b/examples/api/test/widgets/transitions/slide_transition.0_test.dart index 7dd51699174cc..18b279a964120 100644 --- a/examples/api/test/widgets/transitions/slide_transition.0_test.dart +++ b/examples/api/test/widgets/transitions/slide_transition.0_test.dart @@ -13,13 +13,7 @@ void main() { expect(find.byType(Center), findsOneWidget); expect(find.byType(FlutterLogo), findsOneWidget); expect(find.byType(Padding), findsAtLeast(1)); - expect( - find.descendant( - of: find.byType(example.SlideTransitionExample), - matching: find.byType(SlideTransition), - ), - findsOneWidget, - ); + expect(find.byType(SlideTransition), findsOneWidget); }); testWidgets('Animates repeatedly every 2 seconds', (WidgetTester tester) async { diff --git a/packages/flutter/lib/src/material/page_transitions_theme.dart b/packages/flutter/lib/src/material/page_transitions_theme.dart index 14cc8000c1913..c7051b1c09582 100644 --- a/packages/flutter/lib/src/material/page_transitions_theme.dart +++ b/packages/flutter/lib/src/material/page_transitions_theme.dart @@ -17,7 +17,6 @@ import 'package:flutter/services.dart'; import 'color_scheme.dart'; import 'colors.dart'; -import 'predictive_back_page_transitions_builder.dart'; import 'theme.dart'; // Slides the page upwards and fades it in, starting from 1/4 screen @@ -760,12 +759,7 @@ class FadeForwardsPageTransitionsBuilder extends PageTransitionsBuilder { final Color? backgroundColor; /// The value of [transitionDuration] in milliseconds. - /// - /// Eyeballed on a physical Pixel 9 running Android 16. This does not match - /// the actual value used by native Android, which is 800ms, because native - /// Android is using Material 3 Expressive springs that are not currently - /// supported by Flutter. So for now at least, this is an approximation. - static const int kTransitionMilliseconds = 450; + static const int kTransitionMilliseconds = 800; @override Duration get transitionDuration => const Duration(milliseconds: kTransitionMilliseconds); @@ -1103,7 +1097,7 @@ class PageTransitionsTheme with Diagnosticable { static const Map _defaultBuilders = { - TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.android: ZoomPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), TargetPlatform.windows: ZoomPageTransitionsBuilder(), diff --git a/packages/flutter/test/material/bottom_app_bar_test.dart b/packages/flutter/test/material/bottom_app_bar_test.dart index 704411db303cc..556681a6c3d2a 100644 --- a/packages/flutter/test/material/bottom_app_bar_test.dart +++ b/packages/flutter/test/material/bottom_app_bar_test.dart @@ -191,9 +191,7 @@ void main() { final BottomAppBar bottomAppBar = tester.widget(find.byType(BottomAppBar)); expect(bottomAppBar.padding, customPadding); final Rect babRect = tester.getRect(find.byType(BottomAppBar)); - final Rect childRect = tester.getRect( - find.descendant(of: find.byType(BottomAppBar), matching: find.byType(ColoredBox)), - ); + final Rect childRect = tester.getRect(find.byType(ColoredBox)); expect(childRect, const Rect.fromLTRB(250, 530, 550, 590)); expect(babRect, const Rect.fromLTRB(240, 520, 560, 600)); }); diff --git a/packages/flutter/test/material/expansion_tile_test.dart b/packages/flutter/test/material/expansion_tile_test.dart index 98c08f1de11fa..a73a1ece581ba 100644 --- a/packages/flutter/test/material/expansion_tile_test.dart +++ b/packages/flutter/test/material/expansion_tile_test.dart @@ -1690,9 +1690,7 @@ void main() { ); final Size materialAppSize = tester.getSize(find.byType(MaterialApp)); - final Size titleSize = tester.getSize( - find.descendant(of: find.byType(ExpansionTile), matching: find.byType(ColoredBox)), - ); + final Size titleSize = tester.getSize(find.byType(ColoredBox)); expect(titleSize.width, materialAppSize.width); }, @@ -1715,9 +1713,7 @@ void main() { ); final Size materialAppSize = tester.getSize(find.byType(MaterialApp)); - final Size titleSize = tester.getSize( - find.descendant(of: find.byType(ExpansionTile), matching: find.byType(ColoredBox)), - ); + final Size titleSize = tester.getSize(find.byType(ColoredBox)); expect(titleSize.width, materialAppSize.width - 32.0); }, diff --git a/packages/flutter/test/material/flexible_space_bar_test.dart b/packages/flutter/test/material/flexible_space_bar_test.dart index 04449422eb0cf..e417b75c384b1 100644 --- a/packages/flutter/test/material/flexible_space_bar_test.dart +++ b/packages/flutter/test/material/flexible_space_bar_test.dart @@ -1462,18 +1462,7 @@ void main() { }); testWidgets('FlexibleSpaceBar rebuilds when scrolling.', (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: const SubCategoryScreenView(), - theme: ThemeData( - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - }, - ), - ), - ), - ); + await tester.pumpWidget(const MaterialApp(home: SubCategoryScreenView())); expect(RenderRebuildTracker.count, 1); expect( diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index 55e3b216c1208..b3291731626a8 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -3027,9 +3027,7 @@ void main() { ), ); - final Offset topLeft = tester.getTopLeft( - find.descendant(of: find.byType(Center), matching: find.byType(ColoredBox)), - ); + final Offset topLeft = tester.getTopLeft(find.byType(ColoredBox)); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(topLeft); diff --git a/packages/flutter/test/material/navigation_drawer_test.dart b/packages/flutter/test/material/navigation_drawer_test.dart index 96ca683a7467e..667be62ebfa45 100644 --- a/packages/flutter/test/material/navigation_drawer_test.dart +++ b/packages/flutter/test/material/navigation_drawer_test.dart @@ -550,7 +550,7 @@ InkWell? _getInkWell(WidgetTester tester) { ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { return tester .firstWidget( - find.descendant(of: find.byType(NavigationIndicator), matching: find.byType(Container)), + find.descendant(of: find.byType(FadeTransition), matching: find.byType(Container)), ) .decoration as ShapeDecoration?; diff --git a/packages/flutter/test/material/navigation_drawer_theme_test.dart b/packages/flutter/test/material/navigation_drawer_theme_test.dart index a7b164f5be404..4c7cf54194f43 100644 --- a/packages/flutter/test/material/navigation_drawer_theme_test.dart +++ b/packages/flutter/test/material/navigation_drawer_theme_test.dart @@ -284,7 +284,7 @@ Material _getMaterial(WidgetTester tester) { ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { return tester .firstWidget( - find.descendant(of: find.byType(NavigationIndicator), matching: find.byType(Container)), + find.descendant(of: find.byType(FadeTransition), matching: find.byType(Container)), ) .decoration as ShapeDecoration?; diff --git a/packages/flutter/test/material/page_test.dart b/packages/flutter/test/material/page_test.dart index f72f1d799e9f0..7b0a4d4610d03 100644 --- a/packages/flutter/test/material/page_test.dart +++ b/packages/flutter/test/material/page_test.dart @@ -190,13 +190,6 @@ void main() { await tester.pumpWidget( MaterialApp( - theme: ThemeData( - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - }, - ), - ), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( allowSnapshotting: false, @@ -275,14 +268,7 @@ void main() { RepaintBoundary( key: key, child: MaterialApp( - theme: ThemeData( - useMaterial3: false, - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - }, - ), - ), + theme: ThemeData(useMaterial3: false), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { @@ -329,13 +315,6 @@ void main() { key: key, child: MaterialApp( debugShowCheckedModeBanner: false, // https://github.com/flutter/flutter/issues/143616 - theme: ThemeData( - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: ZoomPageTransitionsBuilder(), - }, - ), - ), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { diff --git a/packages/flutter/test/material/page_transitions_theme_test.dart b/packages/flutter/test/material/page_transitions_theme_test.dart index 922a435313abd..bef2440dca09a 100644 --- a/packages/flutter/test/material/page_transitions_theme_test.dart +++ b/packages/flutter/test/material/page_transitions_theme_test.dart @@ -75,7 +75,7 @@ void main() { ); testWidgets( - 'Default PageTransitionsTheme builds a _FadeForwardsPageTransition for android', + 'Default PageTransitionsTheme builds a _ZoomPageTransition for android', (WidgetTester tester) async { final Map routes = { '/': (BuildContext context) => Material( @@ -91,11 +91,11 @@ void main() { await tester.pumpWidget(MaterialApp(routes: routes)); - Finder findFadeForwardsPageTransition() { + Finder findZoomPageTransition() { return find.descendant( of: find.byType(MaterialApp), matching: find.byWidgetPredicate( - (Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition', + (Widget w) => '${w.runtimeType}' == '_ZoomPageTransition', ), ); } @@ -104,12 +104,12 @@ void main() { Theme.of(tester.element(find.text('push'))).platform, debugDefaultTargetPlatformOverride, ); - expect(findFadeForwardsPageTransition(), findsOneWidget); + expect(findZoomPageTransition(), findsOneWidget); await tester.tap(find.text('push')); await tester.pumpAndSettle(); expect(find.text('page b'), findsOneWidget); - expect(findFadeForwardsPageTransition(), findsOneWidget); + expect(findZoomPageTransition(), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android), ); @@ -1034,18 +1034,7 @@ void main() { ); await tester.pumpAndSettle(); - switch (defaultTargetPlatform) { - case TargetPlatform.android: - // Shows both pages while doing the "peek" predicitve back transition. - expect(find.text('push'), findsOneWidget); - case TargetPlatform.iOS: - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.fuchsia: - case TargetPlatform.windows: - // Does no transition yet; still shows page b only. - expect(find.text('push'), findsNothing); - } + expect(find.text('push'), findsNothing); expect(find.text('page b'), findsOneWidget); // Commit the system back gesture. @@ -1058,237 +1047,13 @@ void main() { (ByteData? _) {}, ); await tester.pumpAndSettle(); + expect(find.text('push'), findsOneWidget); expect(find.text('page b'), findsNothing); }, variant: TargetPlatformVariant.all(), ); - testWidgets('predictive back is the default on Android', (WidgetTester tester) async { - final Map routes = { - '/': (BuildContext context) => Material( - child: TextButton( - child: const Text('push'), - onPressed: () { - Navigator.of(context).pushNamed('/b'); - }, - ), - ), - }; - await tester.pumpWidget(MaterialApp(routes: routes)); - - final ThemeData themeData = Theme.of(tester.element(find.text('push'))); - switch (defaultTargetPlatform) { - case TargetPlatform.android: - expect( - themeData.pageTransitionsTheme.builders[defaultTargetPlatform], - isA(), - ); - case TargetPlatform.iOS: - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.fuchsia: - case TargetPlatform.windows: - expect( - themeData.pageTransitionsTheme.builders[defaultTargetPlatform], - isNot(isA()), - ); - } - }, variant: TargetPlatformVariant.all()); - - testWidgets('predictive back falls back to ZoomPageTransitionBuilder', ( - WidgetTester tester, - ) async { - Finder findPredictiveBackPageTransition() { - return find.descendant( - of: find.byType(PrimaryScrollController), - matching: find.byWidgetPredicate( - (Widget w) => '${w.runtimeType}' == '_PredictiveBackSharedElementPageTransition', - ), - ); - } - - Finder findFallbackPageTransition() { - return find.descendant( - of: find.byType(PrimaryScrollController), - matching: find.byWidgetPredicate( - (Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition', - ), - ); - } - - final Map routes = { - '/': (BuildContext context) => Material( - child: TextButton( - child: const Text('push'), - onPressed: () { - Navigator.of(context).pushNamed('/b'); - }, - ), - ), - '/b': (BuildContext context) => const Text('page b'), - }; - - await tester.pumpWidget( - MaterialApp( - routes: routes, - theme: ThemeData( - pageTransitionsTheme: const PageTransitionsTheme( - builders: { - TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), - TargetPlatform.iOS: PredictiveBackPageTransitionsBuilder(), - TargetPlatform.macOS: PredictiveBackPageTransitionsBuilder(), - TargetPlatform.windows: PredictiveBackPageTransitionsBuilder(), - TargetPlatform.linux: PredictiveBackPageTransitionsBuilder(), - TargetPlatform.fuchsia: PredictiveBackPageTransitionsBuilder(), - }, - ), - ), - ), - ); - - final ThemeData themeData = Theme.of(tester.element(find.text('push'))); - expect( - themeData.pageTransitionsTheme.builders[defaultTargetPlatform], - isA(), - ); - - expect(find.text('push'), findsOneWidget); - expect(find.text('page b'), findsNothing); - - await tester.tap(find.text('push')); - await tester.pumpAndSettle(); - - expect(find.text('push'), findsNothing); - expect(find.text('page b'), findsOneWidget); - - // Only Android sends system back gestures. - if (defaultTargetPlatform == TargetPlatform.android) { - final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( - const MethodCall('startBackGesture', { - 'touchOffset': [5.0, 300.0], - 'progress': 0.0, - 'swipeEdge': 0, // left - }), - ); - await binding.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/backgesture', - startMessage, - (ByteData? _) {}, - ); - await tester.pump(); - } - - switch (defaultTargetPlatform) { - case TargetPlatform.android: - expect(findPredictiveBackPageTransition(), findsOneWidget); - expect(findFallbackPageTransition(), findsNothing); - case TargetPlatform.iOS: - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.fuchsia: - case TargetPlatform.windows: - expect(findPredictiveBackPageTransition(), findsNothing); - expect(findFallbackPageTransition(), findsOneWidget); - } - - expect(find.text('push'), findsNothing); - expect(find.text('page b'), findsOneWidget); - - // Drag the system back gesture far enough to commit. - if (defaultTargetPlatform == TargetPlatform.android) { - final ByteData updateMessage = const StandardMethodCodec().encodeMethodCall( - const MethodCall('updateBackGestureProgress', { - 'x': 100.0, - 'y': 300.0, - 'progress': 0.35, - 'swipeEdge': 0, // left - }), - ); - await binding.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/backgesture', - updateMessage, - (ByteData? _) {}, - ); - await tester.pumpAndSettle(); - expect(find.text('push'), findsOneWidget); - } else { - expect(find.text('push'), findsNothing); - } - - expect(find.text('page b'), findsOneWidget); - - switch (defaultTargetPlatform) { - case TargetPlatform.android: - expect(findPredictiveBackPageTransition(), findsNWidgets(2)); - expect(findFallbackPageTransition(), findsNothing); - case TargetPlatform.iOS: - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.fuchsia: - case TargetPlatform.windows: - expect(findPredictiveBackPageTransition(), findsNothing); - expect(findFallbackPageTransition(), findsOneWidget); - } - - if (defaultTargetPlatform == TargetPlatform.android) { - // Commit the system back gesture on Android. - final ByteData commitMessage = const StandardMethodCodec().encodeMethodCall( - const MethodCall('commitBackGesture'), - ); - await binding.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/backgesture', - commitMessage, - (ByteData? _) {}, - ); - } else { - // On other platforms, send a one-off system pop. - final ByteData popMessage = const JSONMethodCodec().encodeMethodCall( - const MethodCall('popRoute'), - ); - await binding.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/navigation', - popMessage, - (ByteData? _) {}, - ); - } - await tester.pump(); - - expect(find.text('push'), findsOneWidget); - expect(find.text('page b'), findsOneWidget); - - switch (defaultTargetPlatform) { - case TargetPlatform.android: - expect(findPredictiveBackPageTransition(), findsNWidgets(2)); - expect(findFallbackPageTransition(), findsNothing); - case TargetPlatform.iOS: - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.fuchsia: - case TargetPlatform.windows: - expect(findPredictiveBackPageTransition(), findsNothing); - expect(findFallbackPageTransition(), findsNWidgets(2)); - } - - await tester.pumpAndSettle(); - - expect(find.text('push'), findsOneWidget); - expect(find.text('page b'), findsNothing); - - switch (defaultTargetPlatform) { - case TargetPlatform.android: - expect(findPredictiveBackPageTransition(), findsNothing); - expect(findFallbackPageTransition(), findsOneWidget); - case TargetPlatform.iOS: - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.fuchsia: - case TargetPlatform.windows: - expect(findPredictiveBackPageTransition(), findsNothing); - expect(findFallbackPageTransition(), findsOneWidget); - } - }, variant: TargetPlatformVariant.all()); - testWidgets( 'ZoomPageTransitionsBuilder uses theme color during transition effects', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart b/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart index e595d3ab4b686..42200f91f4f20 100644 --- a/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart +++ b/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart @@ -63,7 +63,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackSharedElementPageTransition for the page transition. + // _PredictiveBackPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], @@ -173,7 +173,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackSharedElementPageTransition for the page transition. + // _PredictiveBackPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], @@ -326,7 +326,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackSharedElementPageTransition for the page transition. + // _PredictiveBackPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], @@ -488,7 +488,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackSharedElementPageTransition for the page transition. + // _PredictiveBackPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], diff --git a/packages/flutter/test/material/snack_bar_test.dart b/packages/flutter/test/material/snack_bar_test.dart index cf8baa0b8477f..351a6ef32811a 100644 --- a/packages/flutter/test/material/snack_bar_test.dart +++ b/packages/flutter/test/material/snack_bar_test.dart @@ -2753,7 +2753,7 @@ void main() { expect(find.text(snackBarText), findsOneWidget); expect(find.text(firstHeader), findsOneWidget); expect(find.text(secondHeader), findsOneWidget); - await tester.pump(const Duration(milliseconds: 1500)); + await tester.pump(const Duration(milliseconds: 750)); expect(find.text(snackBarText), findsOneWidget); expect(find.text(firstHeader), findsNothing); expect(find.text(secondHeader), findsOneWidget); diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart index 0f7b565eaab19..9d150d970eea5 100644 --- a/packages/flutter/test/material/stepper_test.dart +++ b/packages/flutter/test/material/stepper_test.dart @@ -1298,11 +1298,7 @@ void main() { ?.color; Color lineColor() { - return tester - .widget( - find.descendant(of: find.byType(Stepper), matching: find.byType(ColoredBox)), - ) - .color; + return tester.widget(find.byType(ColoredBox)).color; } // Step 1 diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index fec45654f019b..0061595456a23 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -2122,15 +2122,10 @@ void main() { await tester.tap(find.byType(TextField)); // Wait for context menu to be built. await tester.pumpAndSettle(); - expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); - final SizedBox sizedBox = tester.widget( - find.descendant( - of: find.byType(AdaptiveTextSelectionToolbar), - matching: find.byType(SizedBox), - ), + final RenderBox container = tester.renderObject( + find.descendant(of: find.byType(SnapshotWidget), matching: find.byType(SizedBox)).first, ); - expect(sizedBox.width, 0.0); - expect(sizedBox.height, 0.0); + expect(container.size, Size.zero); }, variant: const TargetPlatformVariant({ TargetPlatform.android, diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index fd2335afc6bb3..5192603c482f8 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -1449,7 +1449,6 @@ Future main() async { .text('Hero') .evaluate() .map((Element e) => e.renderObject!); - await tester.pump(const Duration(milliseconds: 1)); expect(renderObjects.where(isVisible).length, 1); // Hero BC's flight finishes normally. diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index b69d131040d06..8ba7ddb118021 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -2976,12 +2976,12 @@ void main() { await tester.pump(kFourTenthsOfTheTransitionDuration); expect(find.text('Route: 3'), findsOneWidget); expect(find.text('Route: 4'), findsOneWidget); - expect(route4Entry.value, moreOrLessEquals(0.4)); + expect(route4Entry.value, 0.4); await tester.pump(kFourTenthsOfTheTransitionDuration); expect(find.text('Route: 3'), findsOneWidget); expect(find.text('Route: 4'), findsOneWidget); - expect(route4Entry.value, moreOrLessEquals(0.8)); + expect(route4Entry.value, 0.8); expect(find.text('Route: 2', skipOffstage: false), findsOneWidget); expect(find.text('Route: 1', skipOffstage: false), findsOneWidget); expect(find.text('Route: root', skipOffstage: false), findsOneWidget); diff --git a/packages/flutter/test/widgets/slivers_evil_test.dart b/packages/flutter/test/widgets/slivers_evil_test.dart index 6f00fdacaabd0..ef5beda4cf266 100644 --- a/packages/flutter/test/widgets/slivers_evil_test.dart +++ b/packages/flutter/test/widgets/slivers_evil_test.dart @@ -241,17 +241,9 @@ void main() { await tester.drag(find.text('5'), const Offset(0.0, -500.0)); await tester.pump(); - Finder findItem(String text) { - return find.descendant( - of: find.byType(SliverFixedExtentList), - matching: find.widgetWithText(ColoredBox, text), - ); - } - // Screen is 600px high. Moved bottom item 500px up. It's now at the top. - expect(findItem('5'), findsOneWidget); - expect(tester.getTopLeft(findItem('5')).dy, 0.0); - expect(tester.getBottomLeft(findItem('10')).dy, 600.0); + expect(tester.getTopLeft(find.widgetWithText(ColoredBox, '5')).dy, 0.0); + expect(tester.getBottomLeft(find.widgetWithText(ColoredBox, '10')).dy, 600.0); // Stop returning the first 3 items. await tester.pumpWidget( @@ -279,10 +271,10 @@ void main() { // Move up by 4 items, meaning item 1 would have been at the top but // 0 through 3 no longer exist, so item 4, 3 items down, is the first one. // Item 4 is also shifted to the top. - expect(tester.getTopLeft(findItem('4')).dy, 0.0); + expect(tester.getTopLeft(find.widgetWithText(ColoredBox, '4')).dy, 0.0); // Because the screen is still 600px, item 9 is now visible at the bottom instead // of what's supposed to be item 6 had we not re-shifted. - expect(tester.getBottomLeft(findItem('9')).dy, 600.0); + expect(tester.getBottomLeft(find.widgetWithText(ColoredBox, '9')).dy, 600.0); }); } From e4663f2f1b54677f02c12f340607b0c8425585c9 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 14 Aug 2025 17:47:36 -0700 Subject: [PATCH 068/720] Remove `luci_flags.parallel_download_builds` and friends (#173799) These default to `true` and were removed in https://flutter-review.googlesource.com/c/recipes/+/68841. --- engine/src/flutter/ci/builders/linux_android_aot_engine.json | 2 -- .../src/flutter/ci/builders/linux_android_aot_engine_ddm.json | 2 -- .../flutter/ci/builders/linux_android_aot_engine_size_exp.json | 2 -- engine/src/flutter/ci/builders/linux_android_debug_engine.json | 2 -- .../src/flutter/ci/builders/linux_android_debug_engine_ddm.json | 2 -- engine/src/flutter/ci/builders/linux_arm_host_engine.json | 2 -- engine/src/flutter/ci/builders/linux_fuchsia.json | 2 -- engine/src/flutter/ci/builders/linux_host_desktop_engine.json | 2 -- engine/src/flutter/ci/builders/linux_host_engine.json | 2 -- engine/src/flutter/ci/builders/linux_web_engine_build.json | 2 -- engine/src/flutter/ci/builders/mac_android_aot_engine.json | 2 -- engine/src/flutter/ci/builders/mac_host_engine.json | 2 -- engine/src/flutter/ci/builders/mac_ios_engine.json | 2 -- engine/src/flutter/ci/builders/mac_ios_engine_ddm.json | 2 -- engine/src/flutter/ci/builders/windows_android_aot_engine.json | 2 -- engine/src/flutter/ci/builders/windows_arm_host_engine.json | 2 -- engine/src/flutter/ci/builders/windows_host_engine.json | 2 -- 17 files changed, 34 deletions(-) diff --git a/engine/src/flutter/ci/builders/linux_android_aot_engine.json b/engine/src/flutter/ci/builders/linux_android_aot_engine.json index 8d173794281b4..b4078a6f131df 100644 --- a/engine/src/flutter/ci/builders/linux_android_aot_engine.json +++ b/engine/src/flutter/ci/builders/linux_android_aot_engine.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/linux_android_aot_engine_ddm.json b/engine/src/flutter/ci/builders/linux_android_aot_engine_ddm.json index bbc05d459cdc2..6bb1831341704 100644 --- a/engine/src/flutter/ci/builders/linux_android_aot_engine_ddm.json +++ b/engine/src/flutter/ci/builders/linux_android_aot_engine_ddm.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/linux_android_aot_engine_size_exp.json b/engine/src/flutter/ci/builders/linux_android_aot_engine_size_exp.json index f5532cdab2bb0..78a93c5c2ea65 100644 --- a/engine/src/flutter/ci/builders/linux_android_aot_engine_size_exp.json +++ b/engine/src/flutter/ci/builders/linux_android_aot_engine_size_exp.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/linux_android_debug_engine.json b/engine/src/flutter/ci/builders/linux_android_debug_engine.json index 1cb564c9f4363..0cc52f088b2c2 100644 --- a/engine/src/flutter/ci/builders/linux_android_debug_engine.json +++ b/engine/src/flutter/ci/builders/linux_android_debug_engine.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/linux_android_debug_engine_ddm.json b/engine/src/flutter/ci/builders/linux_android_debug_engine_ddm.json index 931b710c6f918..cf4a7719ae8a8 100644 --- a/engine/src/flutter/ci/builders/linux_android_debug_engine_ddm.json +++ b/engine/src/flutter/ci/builders/linux_android_debug_engine_ddm.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ 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 59f96d696d3c1..60008fc008da7 100644 --- a/engine/src/flutter/ci/builders/linux_arm_host_engine.json +++ b/engine/src/flutter/ci/builders/linux_arm_host_engine.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/linux_fuchsia.json b/engine/src/flutter/ci/builders/linux_fuchsia.json index 1bc8ff0a63460..1a5856c63bd70 100644 --- a/engine/src/flutter/ci/builders/linux_fuchsia.json +++ b/engine/src/flutter/ci/builders/linux_fuchsia.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/linux_host_desktop_engine.json b/engine/src/flutter/ci/builders/linux_host_desktop_engine.json index a7a32f07fafbd..fd54b210c9e96 100644 --- a/engine/src/flutter/ci/builders/linux_host_desktop_engine.json +++ b/engine/src/flutter/ci/builders/linux_host_desktop_engine.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/linux_host_engine.json b/engine/src/flutter/ci/builders/linux_host_engine.json index 682107ff55b9d..b3eed2f4921a3 100644 --- a/engine/src/flutter/ci/builders/linux_host_engine.json +++ b/engine/src/flutter/ci/builders/linux_host_engine.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/linux_web_engine_build.json b/engine/src/flutter/ci/builders/linux_web_engine_build.json index 749458994fe2a..c02a5aa8b2273 100644 --- a/engine/src/flutter/ci/builders/linux_web_engine_build.json +++ b/engine/src/flutter/ci/builders/linux_web_engine_build.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/mac_android_aot_engine.json b/engine/src/flutter/ci/builders/mac_android_aot_engine.json index 9de57f7c3e2a8..57f61d4c0b156 100644 --- a/engine/src/flutter/ci/builders/mac_android_aot_engine.json +++ b/engine/src/flutter/ci/builders/mac_android_aot_engine.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/mac_host_engine.json b/engine/src/flutter/ci/builders/mac_host_engine.json index db715f15a3ceb..89ebd06bbd965 100644 --- a/engine/src/flutter/ci/builders/mac_host_engine.json +++ b/engine/src/flutter/ci/builders/mac_host_engine.json @@ -11,8 +11,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/mac_ios_engine.json b/engine/src/flutter/ci/builders/mac_ios_engine.json index 5573abca64207..3e84fdc4629b2 100644 --- a/engine/src/flutter/ci/builders/mac_ios_engine.json +++ b/engine/src/flutter/ci/builders/mac_ios_engine.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/mac_ios_engine_ddm.json b/engine/src/flutter/ci/builders/mac_ios_engine_ddm.json index a76c23f3c897a..1718ca9f8985b 100644 --- a/engine/src/flutter/ci/builders/mac_ios_engine_ddm.json +++ b/engine/src/flutter/ci/builders/mac_ios_engine_ddm.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/windows_android_aot_engine.json b/engine/src/flutter/ci/builders/windows_android_aot_engine.json index e660d7b60ab95..f51cc809a8d8e 100644 --- a/engine/src/flutter/ci/builders/windows_android_aot_engine.json +++ b/engine/src/flutter/ci/builders/windows_android_aot_engine.json @@ -1,7 +1,5 @@ { "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/windows_arm_host_engine.json b/engine/src/flutter/ci/builders/windows_arm_host_engine.json index ac771ae8cf61b..2031e1d131fc7 100644 --- a/engine/src/flutter/ci/builders/windows_arm_host_engine.json +++ b/engine/src/flutter/ci/builders/windows_arm_host_engine.json @@ -1,7 +1,5 @@ { "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ diff --git a/engine/src/flutter/ci/builders/windows_host_engine.json b/engine/src/flutter/ci/builders/windows_host_engine.json index d864eb6e8b05a..ae9d426228d76 100644 --- a/engine/src/flutter/ci/builders/windows_host_engine.json +++ b/engine/src/flutter/ci/builders/windows_host_engine.json @@ -8,8 +8,6 @@ "definition files." ], "luci_flags": { - "delay_collect_builds": true, - "parallel_download_builds": true, "upload_content_hash": true }, "builds": [ From 2d2070c47f6eb84a8b5c5873dab7a1bfca3525ce Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 14 Aug 2025 17:51:08 -0700 Subject: [PATCH 069/720] Stop writing legacy `FLUTTER_ROOT/version` file (by default?) (#172793) Towards https://github.com/flutter/flutter/issues/171900. I'll make a breaking change announcement before landing this PR. We can keep the flag for a stable release (allow folks to write `flutter config --no-enable-omit-legacy-version-file` if they still need the file), or we can decide it's not worth it and just rely on the breaking change announcement. --- packages/flutter_tools/lib/src/features.dart | 5 +---- .../test/general.shard/version_test.dart | 11 ++++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index 31d02666a59b0..bfb5d36e8b9c6 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -199,16 +199,13 @@ const swiftPackageManager = Feature( /// Whether to continue writing the `{FLUTTER_ROOT}/version` legacy file. /// /// Tracking removal: . -const omitLegacyVersionFile = Feature( +const omitLegacyVersionFile = Feature.fullyEnabled( name: 'stops writing the legacy version file', configSetting: 'omit-legacy-version-file', extraHelpText: 'If set, the file {FLUTTER_ROOT}/version is no longer written as part of ' 'the flutter tool execution; a newer file format has existed for some ' 'time in {FLUTTER_ROOT}/bin/cache/flutter.version.json.', - master: FeatureChannelSetting(available: true), - beta: FeatureChannelSetting(available: true), - stable: FeatureChannelSetting(available: true), ); /// Whether desktop windowing is enabled. diff --git a/packages/flutter_tools/test/general.shard/version_test.dart b/packages/flutter_tools/test/general.shard/version_test.dart index afa9b103e10db..2498032f8b132 100644 --- a/packages/flutter_tools/test/general.shard/version_test.dart +++ b/packages/flutter_tools/test/general.shard/version_test.dart @@ -417,7 +417,6 @@ void main() { // Verify the version files exist and have been repopulated after the fetch. expect(FlutterVersion.getVersionFile(fs, flutterRoot), exists); // flutter.version.json - expect(fs.file(fs.path.join(flutterRoot, 'version')), exists); // legacy expect(flutterVersion.channel, channel); expect(flutterVersion.repositoryUrl, flutterUpstreamUrl); @@ -1056,7 +1055,12 @@ void main() { expect(legacyVersionFile.existsSync(), isTrue); expect(legacyVersionFile.readAsStringSync(), '1.2.3'); }, - overrides: {ProcessManager: () => processManager, Cache: () => cache}, + overrides: { + ProcessManager: () => processManager, + Cache: () => cache, + // ignore: avoid_redundant_argument_values + FeatureFlags: () => TestFeatureFlags(isOmitLegacyVersionFileEnabled: false), + }, ); testUsingContext( @@ -1128,7 +1132,6 @@ void main() { final Directory flutterRoot = fs.directory(fs.path.join('path', 'to', 'flutter')); final Directory cacheDir = flutterRoot.childDirectory('bin').childDirectory('cache') ..createSync(recursive: true); - final File legacyVersionFile = flutterRoot.childFile('version'); final File versionFile = cacheDir.childFile('flutter.version.json')..writeAsStringSync('{'); processManager.addCommands([ @@ -1202,12 +1205,10 @@ void main() { // version file was deleted because it couldn't be parsed expect(versionFile.existsSync(), isFalse); - expect(legacyVersionFile.existsSync(), isFalse); // version file was written to disk flutterVersion.ensureVersionFile(); expect(processManager, hasNoRemainingExpectations); expect(versionFile.existsSync(), isTrue); - expect(legacyVersionFile.existsSync(), isTrue); }, overrides: {ProcessManager: () => processManager, Cache: () => cache}, ); From bf4cf11a754fcbe1bd6941e1cdfc1d3763978e8e Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Thu, 14 Aug 2025 17:53:12 -0700 Subject: [PATCH 070/720] Update the RBE configuration for the recent Clang update (#173803) This version of the reclient_cfgs CIPD package references Clang version 21, which matches the Clang roll at https://github.com/flutter/flutter/pull/173429 --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index cf0ecca6185a4..2e8dab4a6c392 100644 --- a/DEPS +++ b/DEPS @@ -774,7 +774,7 @@ deps = { 'packages': [ { 'package': 'flutter_internal/rbe/reclient_cfgs', - 'version': 'XIomtC8MFuQrF9qI5xYcFfcfKXZTbcY6nL6NgF-pSRIC', + 'version': 'LNMZdvF2Y86Dq05IWthtVJ_PswIFSRiywIHrkfHhelUC', } ], 'condition': 'use_rbe', From eb830615a4333d5edce9167b8352ec44a2a3f09b Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 14 Aug 2025 23:55:08 -0400 Subject: [PATCH 071/720] Roll Skia from ad5d04000101 to 162f47d6b6bd (5 revisions) (#173822) https://skia.googlesource.com/skia.git/+log/ad5d04000101..162f47d6b6bd 2025-08-15 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2025-08-14 fmalita@google.com [rustypng] Discard profiles with invalid gamma 2025-08-14 bungeman@google.com Update SkFont size documentation 2025-08-14 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2025-08-14 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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 2e8dab4a6c392..b5207596e2ac0 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': 'ad5d04000101c356e572c45b7f26e42270a601a1', + 'skia_revision': '162f47d6b6bda69d1ecb128801dbe316060b120a', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From e076dd4e02e4535b8dc2de68f925f78d3f93d354 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 15 Aug 2025 01:25:44 -0400 Subject: [PATCH 072/720] Roll Dart SDK from c7faab270f27 to cc008dc8e7aa (2 revisions) (#173826) https://dart.googlesource.com/sdk.git/+log/c7faab270f27..cc008dc8e7aa 2025-08-15 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-101.0.dev 2025-08-14 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-100.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 chinmaygarde@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 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DEPS b/DEPS index b5207596e2ac0..ebb679d528a0c 100644 --- a/DEPS +++ b/DEPS @@ -56,11 +56,11 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': 'c7faab270f275c030ea45804c222127f16ef21cf', + 'dart_revision': 'cc008dc8e7aa27f975b109436dc6bb9d0b240c75', # 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_ai_rev': '6b4b2bce4ee9204f35ba9f7109ba92a24f7049e0', + 'dart_ai_rev': 'ee5b2b25a013322307a60e445b480bc57ec34217', 'dart_binaryen_rev': '1d2e23d5e55788091a51420ba3a9889d4efe7509', 'dart_boringssl_rev': '739613d2e62bd6ab3e7378bf70a5767b50e3c4bf', 'dart_core_rev': 'b59ecf4ceebe6153e1c0166b7c9a7fdd9458a89d', @@ -71,13 +71,13 @@ vars = { 'dart_libprotobuf_rev': '24487dd1045c7f3d64a21f38a3f0c06cc4cf2edb', 'dart_perfetto_rev': '13ce0c9e13b0940d2476cd0cff2301708a9a2e2b', 'dart_protobuf_gn_rev': 'ca669f79945418f6229e4fef89b666b2a88cbb10', - 'dart_protobuf_rev': '0b73b0d64c15e34d35f6e5f9036aac52e4a64033', + 'dart_protobuf_rev': '6e9c9f4637bc0db8a855c7b26e8f87a2279307cc', 'dart_pub_rev': '619db737b4aba0a43beaf16ffa141ee70d7bbd9e', 'dart_sync_http_rev': 'c07f96f89a7eec7e3daac641fa6c587224fcfbaa', 'dart_tools_rev': '1b52e89e0b4ef70e004383c1cf781ad4182f380b', 'dart_vector_math_rev': '3939545edc38ed657381381d33acde02c49ff827', - 'dart_web_rev': '72cdd84cd34feaf777787890d183724108a8344a', - 'dart_webdev_rev': '94c172cc862d0c39c72158c6537f1e20b4432e0e', + 'dart_web_rev': '4310354266abaffae08fdb483327a8523585aed4', + 'dart_webdev_rev': 'c0492f1b40b591710cd8359d6f142ea6ff315985', 'dart_webdriver_rev': '595649d890f69b9d05a596426ca93681b1921132', 'dart_webkit_inspection_protocol_rev': 'effa75205516757795683d527c3dea9546eb0c32', @@ -338,13 +338,13 @@ 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@2a46b4ffe1f095909c3b14bdf62da40cbdbd82e9', + Var('dart_git') + '/shelf.git@400fc396b9fd07b3dced5c6a0d7567072598f2d9', 'engine/src/flutter/third_party/dart/third_party/pkg/sync_http': Var('dart_git') + '/sync_http.git' + '@' + Var('dart_sync_http_rev'), 'engine/src/flutter/third_party/dart/third_party/pkg/tar': - Var('dart_git') + '/external/github.com/simolus3/tar.git@5a1ea943e70cdf3fa5e1102cdbb9418bd9b4b81a', + Var('dart_git') + '/external/github.com/simolus3/tar.git@d511a1dc4f197a29bc06479f4614135abb168b6e', 'engine/src/flutter/third_party/dart/third_party/pkg/test': Var('dart_git') + '/test.git@5aef9719ad9b598260c062b2a90a50d2f50a78f3', From 3bf280c86fb9c083e05a7cd60f27f8a5e5c21424 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 15 Aug 2025 05:05:34 -0400 Subject: [PATCH 073/720] Roll Skia from 162f47d6b6bd to 46ec77ae3954 (2 revisions) (#173833) https://skia.googlesource.com/skia.git/+log/162f47d6b6bd..46ec77ae3954 2025-08-15 skia-autoroll@skia-public.iam.gserviceaccount.com Roll ANGLE from 899f3505748e to 63d8f74cdf9c (5 revisions) 2025-08-15 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Dawn from e07d4f333e72 to 8f680a2a9cb9 (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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 ebb679d528a0c..c758bcb5cdf51 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '162f47d6b6bda69d1ecb128801dbe316060b120a', + 'skia_revision': '46ec77ae39545acb1d6734028d9e2fbfef55f1c3', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From da5c047a13d70a3797ca3edba3cc3220801fb84f Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 15 Aug 2025 11:08:09 -0400 Subject: [PATCH 074/720] Roll Skia from 46ec77ae3954 to 5654ac32ede0 (1 revision) (#173848) https://skia.googlesource.com/skia.git/+log/46ec77ae3954..5654ac32ede0 2025-08-15 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 07db58fa4a14 to 2d05384af7d2 (10 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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 c758bcb5cdf51..b54cd5ef34786 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '46ec77ae39545acb1d6734028d9e2fbfef55f1c3', + 'skia_revision': '5654ac32ede047f3640eb3118de53857f189d557', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 52af7a5040254357f2ab98723b51bbd92d4e6337 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 15 Aug 2025 12:48:08 -0400 Subject: [PATCH 075/720] Roll Packages from 09533b7d5d66 to 5c52c5545f54 (6 revisions) (#173854) https://github.com/flutter/packages/compare/09533b7d5d66...5c52c5545f54 2025-08-15 stuartmorgan@google.com [video_player] Move Android buffer updates to Dart (flutter/packages#9771) 2025-08-15 sfprhythnn@gmail.com [webview_flutter] Add support for payment requests on Android (flutter/packages#9679) 2025-08-15 jason-simmons@users.noreply.github.com [vector_graphics_compiler] Set the m4_10 (Z scale) value to 1 when constructing an AffineMatrix from an SVG matrix (flutter/packages#9813) 2025-08-15 magder@google.com [url_launcher_ios] Fix test button text to work on iOS 26 (flutter/packages#9766) 2025-08-15 stuartmorgan@google.com [video_player] Simplify native iOS code (flutter/packages#9800) 2025-08-14 stuartmorgan@google.com [image_picker] Add the ability to pick multiple videos - platform_interface (flutter/packages#9804) 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 4b87b74f1636f..e4931a9c96083 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -09533b7d5d66b66b75b43ec89c116312838f94a7 +5c52c5545f54cbf93b3572c88a1420a5bb52c589 From e0c1c49c282974522f824e63968ab62c5987dcbb Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Fri, 15 Aug 2025 10:39:19 -0700 Subject: [PATCH 076/720] Re-add `Linux_android_emu *` tests that had KVM issues, now resolved (#173812) Underlying issue was fixed in https://github.com/flutter/flutter/issues/170529, but it was manually marked `bringup`. --- .ci.yaml | 3 --- engine/src/flutter/.ci.yaml | 1 - 2 files changed, 4 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index fdad90adc465f..d4ca31b5dcee9 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -407,7 +407,6 @@ targets: - name: Linux_android_emu android views recipe: devicelab/devicelab_drone - bringup: true # LUCI failing KVM access https://github.com/flutter/flutter/issues/170529 properties: tags: > ["framework","hostonly","linux"] @@ -1665,7 +1664,6 @@ targets: - name: Linux_android_emu_vulkan_stable android_engine_vulkan_tests recipe: flutter/flutter_drone - bringup: true timeout: 60 properties: shard: android_engine_vulkan_tests @@ -2316,7 +2314,6 @@ targets: - name: Linux_android_emu android_defines_test recipe: devicelab/devicelab_drone - bringup: true # LUCI failing KVM access https://github.com/flutter/flutter/issues/170529 timeout: 60 properties: tags: > diff --git a/engine/src/flutter/.ci.yaml b/engine/src/flutter/.ci.yaml index d0710747ff5c9..582d3312a843c 100644 --- a/engine/src/flutter/.ci.yaml +++ b/engine/src/flutter/.ci.yaml @@ -60,7 +60,6 @@ targets: enabled_branches: - master recipe: engine_v2/engine_v2 - bringup: true # LUCI failing KVM access https://github.com/flutter/flutter/issues/170529 properties: config_name: linux_android_emulator dependencies: >- From 8bd06c09beffaaeadf60d11c6e196daf8fb3246d Mon Sep 17 00:00:00 2001 From: "auto-submit[bot]" <98614782+auto-submit[bot]@users.noreply.github.com> Date: Fri, 15 Aug 2025 17:51:20 +0000 Subject: [PATCH 077/720] Reverts "Implements the Android native stretch effect as a fragment shader (Impeller-only). (#169293)" (#173865) Reverts: flutter/flutter#169293 Initiated by: matanlurey Reason for reverting: Does not work on Metal (iOS/macOS): ``` The following _Exception was thrown running a test: Exception: Asset 'shaders/stretch_effect.frag' does not contain appropriate runtime stage data for current backend (Metal). ``` Original PR Author: MTtankkeo Reviewed By: {dkwingsmt, justinmc, chunhtai} This change reverts the following previous change: > Sorry, Closing PR #169196 and reopening this in a new PR for clarity and a cleaner commit history. Fixes #82906 In the existing Flutter implementation, the Android stretch overscroll effect is achieved using Transform. However, this approach does not evenly stretch the screen as it does in the native Android environment. Therefore, in the Impeller environment, add or modify files to implement the effect using a fragment shader identical to the one used in native Android. > [!NOTE] > - The addition of a separate test file for StretchOverscrollEffect was not included because it would likely duplicate coverage already provided by the changes made in overscroll_stretch_indicator_test.dart within this PR. >> However, since determining whether stretching occurred by referencing global coordinates is currently considered impossible with the new Fragment Shader approach, the code was modified to partially exclude the relevant test logic in the Impeller. >> >> For reference, in the page_view_test.dart test, there was an issue with referencing the child Transform widget, but because the StretchOverscrollEffect widget is defined instead of the Transform widget in the Impeller environment, the code logic was adjusted accordingly. > > - Golden image tests were updated as the visual effect changes. ## Reference Source - https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/effects/StretchEffect.cpp ## `Old` Skia (Using Transform) https://github.com/user-attachments/assets/22d8ff96-d875-4722-bf6f-f0ad15b9077d ## `New` Impeller (Using fragment shader) https://github.com/user-attachments/assets/73b6ef18-06b2-42ea-9793-c391ec2ce282 ## 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 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 [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: auto-submit[bot] --- .../src/material/shaders/stretch_effect.frag | 159 -- .../lib/src/widgets/overscroll_indicator.dart | 45 +- .../lib/src/widgets/stretch_effect.dart | 254 ---- packages/flutter/lib/widgets.dart | 1 - .../overscroll_stretch_indicator_test.dart | 1317 ++++++++--------- .../flutter/test/widgets/page_view_test.dart | 20 +- .../test/widgets/stretch_effect_test.dart | 56 - packages/flutter_tools/lib/src/asset.dart | 2 +- .../test/general.shard/asset_bundle_test.dart | 44 +- .../test/general.shard/asset_test.dart | 60 +- 10 files changed, 709 insertions(+), 1249 deletions(-) delete mode 100644 packages/flutter/lib/src/material/shaders/stretch_effect.frag delete mode 100644 packages/flutter/lib/src/widgets/stretch_effect.dart delete mode 100644 packages/flutter/test/widgets/stretch_effect_test.dart diff --git a/packages/flutter/lib/src/material/shaders/stretch_effect.frag b/packages/flutter/lib/src/material/shaders/stretch_effect.frag deleted file mode 100644 index 53be5a6971e1c..0000000000000 --- a/packages/flutter/lib/src/material/shaders/stretch_effect.frag +++ /dev/null @@ -1,159 +0,0 @@ -#version 320 es -// 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. - -// This shader was created based on or with reference to the implementation found at: -// https://cs.android.com/android/platform/superproject/main/+/512046e84bcc51cc241bc6599f83ab345e93ab12:frameworks/base/libs/hwui/effects/StretchEffect.cpp - -#include - -uniform vec2 u_size; -uniform sampler2D u_texture; - -// Multiplier to apply to scale effect. -uniform float u_max_stretch_intensity; - -// Normalized overscroll amount in the horizontal direction. -uniform float u_overscroll_x; - -// Normalized overscroll amount in the vertical direction. -uniform float u_overscroll_y; - -// u_interpolation_strength is the intensity of the interpolation. -uniform float u_interpolation_strength; - -float ease_in(float t, float d) { - return t * d; -} - -float compute_overscroll_start( - float in_pos, - float overscroll, - float u_stretch_affected_dist, - float u_inverse_stretch_affected_dist, - float distance_stretched, - float interpolation_strength -) { - float offset_pos = u_stretch_affected_dist - in_pos; - float pos_based_variation = mix( - 1.0, - ease_in(offset_pos, u_inverse_stretch_affected_dist), - interpolation_strength - ); - float stretch_intensity = overscroll * pos_based_variation; - return distance_stretched - (offset_pos / (1.0 + stretch_intensity)); -} - -float compute_overscroll_end( - float in_pos, - float overscroll, - float reverse_stretch_dist, - float u_stretch_affected_dist, - float u_inverse_stretch_affected_dist, - float distance_stretched, - float interpolation_strength, - float viewport_dimension -) { - float offset_pos = in_pos - reverse_stretch_dist; - float pos_based_variation = mix( - 1.0, - ease_in(offset_pos, u_inverse_stretch_affected_dist), - interpolation_strength - ); - float stretch_intensity = (-overscroll) * pos_based_variation; - return viewport_dimension - (distance_stretched - (offset_pos / (1.0 + stretch_intensity))); -} - -float compute_streched_effect( - float in_pos, - float overscroll, - float u_stretch_affected_dist, - float u_inverse_stretch_affected_dist, - float distance_stretched, - float distance_diff, - float interpolation_strength, - float viewport_dimension -) { - if (overscroll > 0.0) { - if (in_pos <= u_stretch_affected_dist) { - return compute_overscroll_start( - in_pos, overscroll, u_stretch_affected_dist, - u_inverse_stretch_affected_dist, distance_stretched, - interpolation_strength - ); - } else { - return distance_diff + in_pos; - } - } else if (overscroll < 0.0) { - float stretch_affected_dist_calc = viewport_dimension - u_stretch_affected_dist; - if (in_pos >= stretch_affected_dist_calc) { - return compute_overscroll_end( - in_pos, - overscroll, - stretch_affected_dist_calc, - u_stretch_affected_dist, - u_inverse_stretch_affected_dist, - distance_stretched, - interpolation_strength, - viewport_dimension - ); - } else { - return -distance_diff + in_pos; - } - } else { - return in_pos; - } -} - -out vec4 frag_color; - -void main() { - vec2 uv = FlutterFragCoord().xy / u_size; - float in_u_norm = uv.x; - float in_v_norm = uv.y; - - float out_u_norm; - float out_v_norm; - - bool isVertical = u_overscroll_y != 0; - float overscroll = isVertical ? u_overscroll_y : u_overscroll_x; - - float norm_distance_stretched = 1.0 / (1.0 + abs(overscroll)); - float norm_dist_diff = norm_distance_stretched - 1.0; - - const float norm_viewport = 1.0; - const float norm_stretch_affected_dist = 1.0; - const float norm_inverse_stretch_affected_dist = 1.0; - - out_u_norm = isVertical ? in_u_norm : compute_streched_effect( - in_u_norm, - overscroll, - norm_stretch_affected_dist, - norm_inverse_stretch_affected_dist, - norm_distance_stretched, - norm_dist_diff, - u_interpolation_strength, - norm_viewport - ); - - out_v_norm = isVertical ? compute_streched_effect( - in_v_norm, - overscroll, - norm_stretch_affected_dist, - norm_inverse_stretch_affected_dist, - norm_distance_stretched, - norm_dist_diff, - u_interpolation_strength, - norm_viewport - ) : in_v_norm; - - uv.x = out_u_norm; - #ifdef IMPELLER_TARGET_OPENGLES - uv.y = 1.0 - out_v_norm; - #else - uv.y = out_v_norm; - #endif - - frag_color = texture(u_texture, uv); -} diff --git a/packages/flutter/lib/src/widgets/overscroll_indicator.dart b/packages/flutter/lib/src/widgets/overscroll_indicator.dart index 952214bfd4e6a..7e1751a5cc202 100644 --- a/packages/flutter/lib/src/widgets/overscroll_indicator.dart +++ b/packages/flutter/lib/src/widgets/overscroll_indicator.dart @@ -23,7 +23,6 @@ import 'framework.dart'; import 'media_query.dart'; import 'notification_listener.dart'; import 'scroll_notification.dart'; -import 'stretch_effect.dart'; import 'ticker_provider.dart'; import 'transitions.dart'; @@ -775,6 +774,20 @@ class _StretchingOverscrollIndicatorState extends State widget.axisDirection, + _StretchDirection.leading => flipAxisDirection(widget.axisDirection), + }; + return switch (direction) { + AxisDirection.up => AlignmentDirectional.topCenter, + AxisDirection.down => AlignmentDirectional.bottomCenter, + AxisDirection.left => Alignment.centerLeft, + AxisDirection.right => Alignment.centerRight, + }; + } + @override void dispose() { _stretchController.dispose(); @@ -789,34 +802,30 @@ class _StretchingOverscrollIndicatorState extends State= -1.0 && stretchStrength <= 1.0, - 'stretchStrength must be between -1.0 and 1.0', - ); - - /// The overscroll strength applied for the stretching effect. - /// - /// The value should be between -1.0 and 1.0 inclusive. - /// - /// For the horizontal axis: - /// - Positive values apply a pull/stretch from left to right, - /// where 1.0 represents the maximum stretch to the right. - /// - Negative values apply a pull/stretch from right to left, - /// where -1.0 represents the maximum stretch to the left. - /// - /// For the vertical axis: - /// - Positive values apply a pull/stretch from top to bottom, - /// where 1.0 represents the maximum stretch downward. - /// - Negative values apply a pull/stretch from bottom to top, - /// where -1.0 represents the maximum stretch upward. - /// - /// {@tool snippet} - /// This example shows how to set the horizontal stretch strength to pull right. - /// - /// ```dart - /// const StretchEffect( - /// stretchStrength: 0.5, - /// axis: Axis.horizontal, - /// child: Text('Hello, World!'), - /// ); - /// ``` - /// {@end-tool} - final double stretchStrength; - - /// The axis along which the stretching overscroll effect is applied. - /// - /// Determines the direction of the stretch, either horizontal or vertical. - final Axis axis; - - /// The child widget that the stretching overscroll effect applies to. - final Widget child; - - AlignmentGeometry _getAlignment(TextDirection direction) { - final bool isForward = stretchStrength > 0; - - if (axis == Axis.vertical) { - return isForward ? AlignmentDirectional.topCenter : AlignmentDirectional.bottomCenter; - } - - // RTL horizontal. - if (direction == TextDirection.rtl) { - return isForward ? AlignmentDirectional.centerEnd : AlignmentDirectional.centerStart; - } else { - return isForward ? AlignmentDirectional.centerStart : AlignmentDirectional.centerEnd; - } - } - - @override - Widget build(BuildContext context) { - if (ui.ImageFilter.isShaderFilterSupported) { - return _StretchOverscrollEffect(stretchStrength: stretchStrength, axis: axis, child: child); - } - - final TextDirection textDirection = Directionality.of(context); - double x = 1.0; - double y = 1.0; - - switch (axis) { - case Axis.horizontal: - x += stretchStrength.abs(); - case Axis.vertical: - y += stretchStrength.abs(); - } - - return Transform( - alignment: _getAlignment(textDirection), - transform: Matrix4.diagonal3Values(x, y, 1.0), - filterQuality: stretchStrength == 0 ? null : FilterQuality.medium, - child: child, - ); - } -} - -/// A widget that replicates the native Android stretch overscroll effect. -/// -/// This widget is used in the [StretchEffect] widget and creates -/// a stretch visual feedback when the user overscrolls at the edges. -/// -/// Only supported when using the Impeller rendering engine. -class _StretchOverscrollEffect extends StatefulWidget { - /// Creates a [_StretchOverscrollEffect] widget that applies a stretch - /// effect when the user overscrolls horizontally or vertically. - const _StretchOverscrollEffect({ - this.stretchStrength = 0.0, - required this.axis, - required this.child, - }) : assert( - stretchStrength >= -1.0 && stretchStrength <= 1.0, - 'stretchStrength must be between -1.0 and 1.0', - ); - - /// The overscroll strength applied for the stretching effect. - /// - /// The value should be between -1.0 and 1.0 inclusive. - /// For horizontal axis, Positive values apply a pull from - /// left to right, while negative values pull from right to left. - final double stretchStrength; - - /// The axis along which the stretching overscroll effect is applied. - /// - /// Determines the direction of the stretch, either horizontal or vertical. - final Axis axis; - - /// The child widget that the stretching overscroll effect applies to. - final Widget child; - - @override - State<_StretchOverscrollEffect> createState() => _StretchOverscrollEffectState(); -} - -class _StretchOverscrollEffectState extends State<_StretchOverscrollEffect> { - ui.FragmentShader? _fragmentShader; - - /// The maximum scale multiplier applied during a stretch effect. - static const double maxStretchIntensity = 1.0; - - /// The strength of the interpolation used for smoothing the effect. - static const double interpolationStrength = 0.7; - - /// A no-op [ui.ImageFilter] that uses the identity matrix. - static final ui.ImageFilter _emptyFilter = ui.ImageFilter.matrix(Matrix4.identity().storage); - - @override - void dispose() { - _fragmentShader?.dispose(); - super.dispose(); - } - - @override - void initState() { - super.initState(); - _StretchEffectShader.initializeShader(); - } - - @override - Widget build(BuildContext context) { - final bool isShaderNeeded = widget.stretchStrength.abs() > precisionErrorTolerance; - - final ui.ImageFilter imageFilter; - - if (_StretchEffectShader._initialized) { - _fragmentShader?.dispose(); - _fragmentShader = _StretchEffectShader._program!.fragmentShader(); - _fragmentShader!.setFloat(2, maxStretchIntensity); - if (widget.axis == Axis.vertical) { - _fragmentShader!.setFloat(3, 0.0); - _fragmentShader!.setFloat(4, widget.stretchStrength); - } else { - _fragmentShader!.setFloat(3, widget.stretchStrength); - _fragmentShader!.setFloat(4, 0.0); - } - _fragmentShader!.setFloat(5, interpolationStrength); - - imageFilter = ui.ImageFilter.shader(_fragmentShader!); - } else { - _fragmentShader?.dispose(); - _fragmentShader = null; - - imageFilter = _emptyFilter; - } - - return ImageFiltered( - imageFilter: imageFilter, - enabled: isShaderNeeded, - // A nearly-transparent pixels is used to ensure the shader gets applied, - // even when the child is visually transparent or has no paint operations. - child: CustomPaint( - painter: isShaderNeeded ? _StretchEffectPainter() : null, - child: widget.child, - ), - ); - } -} - -/// A [CustomPainter] that draws nearly transparent pixels at the four corners. -/// -/// This ensures the fragment shader covers the entire canvas by forcing -/// painting operations on all edges, preventing shader optimization skips. -class _StretchEffectPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final Paint paint = Paint() - ..color = const Color.fromARGB(1, 0, 0, 0) - ..style = PaintingStyle.fill; - - canvas.drawPoints(ui.PointMode.points, [ - Offset.zero, - Offset(size.width - 1, 0), - Offset(0, size.height - 1), - Offset(size.width - 1, size.height - 1), - ], paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} - -class _StretchEffectShader { - static bool _initCalled = false; - static bool _initialized = false; - static ui.FragmentProgram? _program; - - static void initializeShader() { - if (!_initCalled) { - ui.FragmentProgram.fromAsset('shaders/stretch_effect.frag').then(( - ui.FragmentProgram program, - ) { - _program = program; - _initialized = true; - }); - _initCalled = true; - } - } -} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 81486a12f6ca0..bcb359732a1a3 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -154,7 +154,6 @@ export 'src/widgets/spacer.dart'; export 'src/widgets/spell_check.dart'; export 'src/widgets/standard_component_type.dart'; export 'src/widgets/status_transitions.dart'; -export 'src/widgets/stretch_effect.dart'; export 'src/widgets/system_context_menu.dart'; export 'src/widgets/table.dart'; export 'src/widgets/tap_region.dart'; diff --git a/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart b/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart index 5a4e6854440e1..eec79a4e941bc 100644 --- a/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart +++ b/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart @@ -7,18 +7,11 @@ @Tags(['reduced-test-set']) library; -import 'dart:ui' as ui; - import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - // `StretchingOverscrollIndicator` uses a different algorithm when - // shader is available, therefore the tests must be different depending - // on shader support. - final bool shaderSupported = ui.ImageFilter.isShaderFilterSupported; - Widget buildTest( GlobalKey box1Key, GlobalKey box2Key, @@ -153,401 +146,373 @@ void main() { expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); }); - testWidgets( - 'Stretch overscroll vertically', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll vertically', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.start.png'), - ); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.start.png'), + ); - TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(0.0, 200.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(255.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(510.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.start.stretched.png'), - ); + TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(0.0, 200.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(255.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(510.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.start.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back to the start - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + // Stretch released back to the start + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - // Jump to end of the list - controller.jumpTo(controller.position.maxScrollExtent); - await tester.pumpAndSettle(); - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.end.png'), - ); + // Jump to end of the list + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pumpAndSettle(); + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.end.png'), + ); - gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); - // Overscroll the end - await gesture.moveBy(const Offset(0.0, -200.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(-165)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(90.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(350.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.end.stretched.png'), - ); + gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); + // Overscroll the end + await gesture.moveBy(const Offset(0.0, -200.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(-165)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(90.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(350.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.end.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // Stretch released back + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); + }); - testWidgets( - 'Stretch overscroll works in reverse - vertical', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll works in reverse - vertical', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller, reverse: true)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller, reverse: true)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), const Offset(0.0, 350.0)); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, -150.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), const Offset(0.0, 350.0)); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, -150.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll - await gesture.moveBy(const Offset(0.0, -200.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(350.0)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(100.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(-150.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.reverse.png'), - ); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll + await gesture.moveBy(const Offset(0.0, -200.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(350.0)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(100.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(-150.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.reverse.png'), + ); + }); - testWidgets( - 'Stretch overscroll works in reverse - horizontal', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll works in reverse - horizontal', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal, reverse: true), - ); + await tester.pumpWidget( + buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal, reverse: true), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); - expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); + expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.png'), - ); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.png'), + ); + }); - testWidgets( - 'Stretch overscroll works in reverse - horizontal - RTL', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll works in reverse - horizontal - RTL', ( + WidgetTester tester, + ) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - axis: Axis.horizontal, - reverse: true, - textDirection: TextDirection.rtl, - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + axis: Axis.horizontal, + reverse: true, + textDirection: TextDirection.rtl, + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.png'), - ); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.png'), + ); - TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(200.0, 0.0)); - await tester.pumpAndSettle(); + TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(200.0, 0.0)); + await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.stretched.png'), - ); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back to the start - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + // Stretch released back to the start + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - // Jump to end of the list - controller.jumpTo(controller.position.maxScrollExtent); - await tester.pumpAndSettle(); - expect(controller.offset, 100.0); - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.png'), - ); + // Jump to end of the list + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pumpAndSettle(); + expect(controller.offset, 100.0); + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.png'), + ); - gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); - // Overscroll the end - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.stretched.png'), - ); + gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); + // Overscroll the end + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // Stretch released back + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + }); - testWidgets( - 'Stretch overscroll horizontally', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll horizontally', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal), - ); + await tester.pumpWidget( + buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.start.png'), - ); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.start.png'), + ); - TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.start.stretched.png'), - ); + TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.start.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back to the start - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + // Stretch released back to the start + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - // Jump to end of the list - controller.jumpTo(controller.position.maxScrollExtent); - await tester.pumpAndSettle(); - expect(controller.offset, 100.0); - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.end.png'), - ); + // Jump to end of the list + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pumpAndSettle(); + expect(controller.offset, 100.0); + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.end.png'), + ); - gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); - // Overscroll the end - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.end.stretched.png'), - ); + gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); + // Overscroll the end + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.end.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // Stretch released back + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + }); - testWidgets( - 'Stretch overscroll horizontally RTL', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll horizontally RTL', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - axis: Axis.horizontal, - textDirection: TextDirection.rtl, - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + axis: Axis.horizontal, + textDirection: TextDirection.rtl, + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); - expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); + expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.rtl.png'), - ); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.rtl.png'), + ); + }); testWidgets('Disallow stretching overscroll', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); @@ -768,403 +733,373 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets( - 'Stretch limit', - (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/issues/99264 - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: ScrollConfiguration( - behavior: const ScrollBehavior().copyWith(overscroll: false), - child: StretchingOverscrollIndicator( - axisDirection: AxisDirection.down, - child: SizedBox( - height: 300, - child: ListView.builder( - itemCount: 20, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Text('Index $index'), - ); - }, - ), + testWidgets('Stretch limit', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/99264 + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(overscroll: false), + child: StretchingOverscrollIndicator( + axisDirection: AxisDirection.down, + child: SizedBox( + height: 300, + child: ListView.builder( + itemCount: 20, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Text('Index $index'), + ); + }, ), ), ), ), ), - ); - const double maxStretchLocation = 52.63178407049861; + ), + ); + const double maxStretchLocation = 52.63178407049861; - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, 51.0); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, 51.0); - TestGesture pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Overscroll beyond the limit (the viewport is 600.0). - await pointer.moveBy(const Offset(0.0, 610.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); + TestGesture pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Overscroll beyond the limit (the viewport is 600.0). + await pointer.moveBy(const Offset(0.0, 610.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); - pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Overscroll way way beyond the limit - await pointer.moveBy(const Offset(0.0, 1000.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); + pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Overscroll way way beyond the limit + await pointer.moveBy(const Offset(0.0, 1000.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); - await pointer.up(); - await tester.pumpAndSettle(); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + await pointer.up(); + await tester.pumpAndSettle(); + }); - testWidgets( - 'Multiple pointers will not exceed stretch limit', - (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/issues/99264 - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: ScrollConfiguration( - behavior: const ScrollBehavior().copyWith(overscroll: false), - child: StretchingOverscrollIndicator( - axisDirection: AxisDirection.down, - child: SizedBox( - height: 300, - child: ListView.builder( - itemCount: 20, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Text('Index $index'), - ); - }, - ), + testWidgets('Multiple pointers will not exceed stretch limit', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/99264 + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(overscroll: false), + child: StretchingOverscrollIndicator( + axisDirection: AxisDirection.down, + child: SizedBox( + height: 300, + child: ListView.builder( + itemCount: 20, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Text('Index $index'), + ); + }, ), ), ), ), ), - ); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, 51.0); + ), + ); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, 51.0); - final TestGesture pointer1 = await tester.startGesture( - tester.getCenter(find.text('Index 1')), - ); - // Overscroll the start. - await pointer1.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - double lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; - expect(lastStretchedLocation, greaterThan(51.0)); + final TestGesture pointer1 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Overscroll the start. + await pointer1.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + double lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; + expect(lastStretchedLocation, greaterThan(51.0)); - final TestGesture pointer2 = await tester.startGesture( - tester.getCenter(find.text('Index 1')), - ); - // Add overscroll from an additional pointer - await pointer2.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); - lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; + final TestGesture pointer2 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Add overscroll from an additional pointer + await pointer2.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); + lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; - final TestGesture pointer3 = await tester.startGesture( - tester.getCenter(find.text('Index 1')), - ); - // Add overscroll from an additional pointer, exceeding the max stretch (600) - await pointer3.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); - lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; + final TestGesture pointer3 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Add overscroll from an additional pointer, exceeding the max stretch (600) + await pointer3.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); + lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; - final TestGesture pointer4 = await tester.startGesture( - tester.getCenter(find.text('Index 1')), - ); - // Since we have maxed out the overscroll, it should not have stretched - // further, regardless of the number of pointers. - await pointer4.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, lastStretchedLocation); + final TestGesture pointer4 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Since we have maxed out the overscroll, it should not have stretched + // further, regardless of the number of pointers. + await pointer4.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, lastStretchedLocation); - await pointer1.up(); - await pointer2.up(); - await pointer3.up(); - await pointer4.up(); - await tester.pumpAndSettle(); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + await pointer1.up(); + await pointer2.up(); + await pointer3.up(); + await pointer4.up(); + await tester.pumpAndSettle(); + }); - testWidgets( - 'Stretch overscroll vertically, change direction mid scroll', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll vertically, change direction mid scroll', ( + WidgetTester tester, + ) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - // Setting the `boxHeight` to 100.0 will make the boxes fit in the - // scrollable viewport. - boxHeight: 100, - // To make the scroll view in the test still scrollable, we need to add - // the `AlwaysScrollableScrollPhysics`. - physics: const AlwaysScrollableScrollPhysics(), - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + // Setting the `boxHeight` to 100.0 will make the boxes fit in the + // scrollable viewport. + boxHeight: 100, + // To make the scroll view in the test still scrollable, we need to add + // the `AlwaysScrollableScrollPhysics`. + physics: const AlwaysScrollableScrollPhysics(), + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(0.0, 600.0)); - await tester.pumpAndSettle(); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(0.0, 600.0)); + await tester.pumpAndSettle(); - // The boxes should now be at different locations because of the scaling. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); + // The boxes should now be at different locations because of the scaling. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); - // Move the pointer up a miniscule amount to trigger a directional change. - await gesture.moveBy(const Offset(0.0, -20.0)); - await tester.pumpAndSettle(); + // Move the pointer up a miniscule amount to trigger a directional change. + await gesture.moveBy(const Offset(0.0, -20.0)); + await tester.pumpAndSettle(); - // The boxes should remain roughly at the same locations, since the pointer - // didn't move far. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); + // The boxes should remain roughly at the same locations, since the pointer + // didn't move far. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); - // Now make the pointer overscroll to the end - await gesture.moveBy(const Offset(0.0, -1200.0)); - await tester.pumpAndSettle(); + // Now make the pointer overscroll to the end + await gesture.moveBy(const Offset(0.0, -1200.0)); + await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(-19.0)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(85.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(188.0)); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(-19.0)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(85.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(188.0)); - // Release the pointer - await gesture.up(); - await tester.pumpAndSettle(); + // Release the pointer + await gesture.up(); + await tester.pumpAndSettle(); - // Now the boxes should be back to their original locations. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // Now the boxes should be back to their original locations. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); + }); - testWidgets( - 'Stretch overscroll horizontally, change direction mid scroll', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll horizontally, change direction mid scroll', ( + WidgetTester tester, + ) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - // Setting the `boxWidth` to 100.0 will make the boxes fit in the - // scrollable viewport. - boxWidth: 100, - // To make the scroll view in the test still scrollable, we need to add - // the `AlwaysScrollableScrollPhysics`. - physics: const AlwaysScrollableScrollPhysics(), - axis: Axis.horizontal, - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + // Setting the `boxWidth` to 100.0 will make the boxes fit in the + // scrollable viewport. + boxWidth: 100, + // To make the scroll view in the test still scrollable, we need to add + // the `AlwaysScrollableScrollPhysics`. + physics: const AlwaysScrollableScrollPhysics(), + axis: Axis.horizontal, + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(600.0, 0.0)); - await tester.pumpAndSettle(); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(600.0, 0.0)); + await tester.pumpAndSettle(); - // The boxes should now be at different locations because of the scaling. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); + // The boxes should now be at different locations because of the scaling. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); - // Move the pointer up a miniscule amount to trigger a directional change. - await gesture.moveBy(const Offset(-20.0, 0.0)); - await tester.pumpAndSettle(); + // Move the pointer up a miniscule amount to trigger a directional change. + await gesture.moveBy(const Offset(-20.0, 0.0)); + await tester.pumpAndSettle(); - // The boxes should remain roughly at the same locations, since the pointer - // didn't move far. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); + // The boxes should remain roughly at the same locations, since the pointer + // didn't move far. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); - // Now make the pointer overscroll to the end - await gesture.moveBy(const Offset(-1200.0, 0.0)); - await tester.pumpAndSettle(); + // Now make the pointer overscroll to the end + await gesture.moveBy(const Offset(-1200.0, 0.0)); + await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(-19.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(85.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(188.0)); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(-19.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(85.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(188.0)); - // Release the pointer - await gesture.up(); - await tester.pumpAndSettle(); + // Release the pointer + await gesture.up(); + await tester.pumpAndSettle(); - // Now the boxes should be back to their original locations. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // Now the boxes should be back to their original locations. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + }); - testWidgets( - 'Fling toward the trailing edge causes stretch toward the leading edge', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Fling toward the trailing edge causes stretch toward the leading edge', ( + WidgetTester tester, + ) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); + await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); - // The boxes should now be at different locations because of the scaling. - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(-160.0)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(93.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(347.0)); + // The boxes should now be at different locations because of the scaling. + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(-160.0)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(93.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(347.0)); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - // The boxes should now be at their final position. - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // The boxes should now be at their final position. + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); + }); - testWidgets( - 'Fling toward the leading edge causes stretch toward the trailing edge', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Fling toward the leading edge causes stretch toward the trailing edge', ( + WidgetTester tester, + ) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - // We fling to the trailing edge and let it settle. - await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); - await tester.pumpAndSettle(); + // We fling to the trailing edge and let it settle. + await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); + await tester.pumpAndSettle(); - // We are now at the trailing edge - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); + // We are now at the trailing edge + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); - // Now fling to the leading edge - await tester.fling(find.byType(CustomScrollView), const Offset(0.0, 50.0), 10000.0); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); + // Now fling to the leading edge + await tester.fling(find.byType(CustomScrollView), const Offset(0.0, 50.0), 10000.0); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); - // The boxes should now be at different locations because of the scaling. - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero).dy, 0.0); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(254.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(508.0)); + // The boxes should now be at different locations because of the scaling. + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero).dy, 0.0); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(254.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(508.0)); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - // The boxes should now be at their final position. - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero).dy, 0.0); - expect(box2.localToGlobal(Offset.zero).dy, 250.0); - expect(box3.localToGlobal(Offset.zero).dy, 500.0); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // The boxes should now be at their final position. + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero).dy, 0.0); + expect(box2.localToGlobal(Offset.zero).dy, 250.0); + expect(box3.localToGlobal(Offset.zero).dy, 500.0); + }); testWidgets( 'changing scroll direction during recede animation will not change the stretch direction', @@ -1231,8 +1166,6 @@ void main() { await gesture.up(); }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, ); testWidgets('Stretch overscroll only uses image filter during stretch effect', ( @@ -1300,7 +1233,7 @@ void main() { // We fling to the trailing edge and let it settle. await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); - await tester.pumpAndSettle(const Duration(milliseconds: 300)); + await tester.pumpAndSettle(); // We are now at the trailing edge expect(overscrollNotification.velocity, lessThan(25)); diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart index 3d127de0b4243..92aa4509efb6b 100644 --- a/packages/flutter/test/widgets/page_view_test.dart +++ b/packages/flutter/test/widgets/page_view_test.dart @@ -2,11 +2,6 @@ // 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/foundation.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; @@ -1269,10 +1264,19 @@ void main() { controller.animateToPage(2, duration: const Duration(milliseconds: 300), curve: Curves.ease); await tester.pumpAndSettle(); - await expectLater( - find.byType(PageView), - matchesGoldenFile('page_view_no_stretch_precision_error.png'), + final Finder transformFinder = find.descendant( + of: find.byType(PageView), + matching: find.byType(Transform), + ); + expect(transformFinder, findsOneWidget); + + // Get the Transform widget that stretches the PageView. + final Transform transform = tester.firstWidget( + find.descendant(of: find.byType(PageView), matching: find.byType(Transform)), ); + + // Check the stretch factor in the first element of the transform matrix. + expect(transform.transform.storage.first, 1.0); }); testWidgets('PageController onAttach, onDetach', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/stretch_effect_test.dart b/packages/flutter/test/widgets/stretch_effect_test.dart deleted file mode 100644 index 9f519c1e42f47..0000000000000 --- a/packages/flutter/test/widgets/stretch_effect_test.dart +++ /dev/null @@ -1,56 +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. - -// This file is run as part of a reduced test set in CI on Mac and Windows -// machines. -@Tags(['reduced-test-set']) -library; - -import 'dart:ui' as ui; - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - // `StretchingOverscrollIndicator` uses a different algorithm when - // shader is available, therefore the tests must be different depending - // on shader support. - final bool shaderSupported = ui.ImageFilter.isShaderFilterSupported; - - testWidgets( - 'Stretch effect covers full viewport', - (WidgetTester tester) async { - // This test verifies that when the stretch effect is applied to a scrollable widget, - // it should cover the entire scrollable area (e.g., full height of the scroll view), - // even if the actual content inside has a smaller height. - // - // Without this behavior, the shader is clipped only to the content area, - // causing the stretch effect to render incorrectly or be invisible - // when the content doesn't fill the scroll view. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: StretchEffect( - stretchStrength: 1, - axis: Axis.vertical, - child: Stack( - alignment: Alignment.topCenter, - children: [ - Container(height: 100), - Container(height: 50, color: const Color.fromRGBO(255, 0, 0, 1)), - ], - ), - ), - ), - ); - - await expectLater( - find.byType(StretchEffect), - matchesGoldenFile('stretch_effect_covers_full_viewport.png'), - ); - }, - // Skips this test when fragment shaders are not used. - skip: !shaderSupported, - ); -} diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 406ae53d82529..393f882f38183 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -50,7 +50,7 @@ const kMaterialFonts = >[ }, ]; -const kMaterialShaders = ['shaders/ink_sparkle.frag', 'shaders/stretch_effect.frag']; +const kMaterialShaders = ['shaders/ink_sparkle.frag']; /// Injected factory class for spawning [AssetBundle] instances. abstract class AssetBundleFactory { diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart index 39c11fded0dc1..4edbc1b89cb97 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart @@ -1017,30 +1017,26 @@ flutter: materialDir.childFile(shader).createSync(recursive: true); } - final testShaders = ['ink_sparkle.frag', 'stretch_effect.frag']; - - for (final shader in testShaders) { - (globals.processManager as FakeProcessManager).addCommand( - FakeCommand( - command: [ - impellerc, - '--sksl', - '--iplr', - '--json', - '--sl=${fileSystem.path.join(output.path, 'shaders', shader)}', - '--spirv=${fileSystem.path.join(output.path, 'shaders', '$shader.spirv')}', - '--input=${fileSystem.path.join(materialDir.path, 'shaders', shader)}', - '--input-type=frag', - '--include=${fileSystem.path.join(materialDir.path, 'shaders')}', - '--include=$shaderLibDir', - ], - onRun: (_) { - fileSystem.file(outputPath).createSync(recursive: true); - fileSystem.file('$outputPath.spirv').createSync(recursive: true); - }, - ), - ); - } + (globals.processManager as FakeProcessManager).addCommand( + FakeCommand( + command: [ + impellerc, + '--sksl', + '--iplr', + '--json', + '--sl=${fileSystem.path.join(output.path, 'shaders', 'ink_sparkle.frag')}', + '--spirv=${fileSystem.path.join(output.path, 'shaders', 'ink_sparkle.frag.spirv')}', + '--input=${fileSystem.path.join(materialDir.path, 'shaders', 'ink_sparkle.frag')}', + '--input-type=frag', + '--include=${fileSystem.path.join(materialDir.path, 'shaders')}', + '--include=$shaderLibDir', + ], + onRun: (_) { + fileSystem.file(outputPath).createSync(recursive: true); + fileSystem.file('$outputPath.spirv').createSync(recursive: true); + }, + ), + ); fileSystem.file('pubspec.yaml') ..createSync() diff --git a/packages/flutter_tools/test/general.shard/asset_test.dart b/packages/flutter_tools/test/general.shard/asset_test.dart index 3e1f99ccedb84..f64c64f623445 100644 --- a/packages/flutter_tools/test/general.shard/asset_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_test.dart @@ -304,23 +304,18 @@ flutter: expect(assetBundle.inputFiles.map((File f) => f.path), []); }); - final testShaders = ['ink_sparkle.frag', 'stretch_effect.frag']; - testWithoutContext('bundles material shaders on non-web platforms', () async { - for (final shader in testShaders) { - final String shaderPath = fileSystem.path.join( - flutterRoot, - 'packages', - 'flutter', - 'lib', - 'src', - 'material', - 'shaders', - shader, - ); - fileSystem.file(shaderPath).createSync(recursive: true); - } - + final String shaderPath = fileSystem.path.join( + flutterRoot, + 'packages', + 'flutter', + 'lib', + 'src', + 'material', + 'shaders', + 'ink_sparkle.frag', + ); + fileSystem.file(shaderPath).createSync(recursive: true); writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_package'); fileSystem.file('pubspec.yaml').writeAsStringSync('name: my_package'); final assetBundle = ManifestAssetBundle( @@ -336,26 +331,21 @@ flutter: flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), ); - for (final shader in testShaders) { - expect(assetBundle.entries.keys, contains('shaders/$shader')); - } + expect(assetBundle.entries.keys, contains('shaders/ink_sparkle.frag')); }); testWithoutContext('bundles material shaders on web platforms', () async { - for (final shader in testShaders) { - final String shaderPath = fileSystem.path.join( - flutterRoot, - 'packages', - 'flutter', - 'lib', - 'src', - 'material', - 'shaders', - shader, - ); - fileSystem.file(shaderPath).createSync(recursive: true); - } - + final String shaderPath = fileSystem.path.join( + flutterRoot, + 'packages', + 'flutter', + 'lib', + 'src', + 'material', + 'shaders', + 'ink_sparkle.frag', + ); + fileSystem.file(shaderPath).createSync(recursive: true); writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_package'); fileSystem.file('pubspec.yaml').writeAsStringSync('name: my_package'); final assetBundle = ManifestAssetBundle( @@ -371,9 +361,7 @@ flutter: flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), ); - for (final shader in testShaders) { - expect(assetBundle.entries.keys, contains('shaders/$shader')); - } + expect(assetBundle.entries.keys, contains('shaders/ink_sparkle.frag')); }); }); } From 67f3758d07c76c3297009a3bc476d3f38358bd2a Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 15 Aug 2025 15:37:34 -0400 Subject: [PATCH 078/720] Roll Dart SDK from cc008dc8e7aa to 9277d6303da5 (2 revisions) (#173864) https://dart.googlesource.com/sdk.git/+log/cc008dc8e7aa..9277d6303da5 2025-08-15 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-103.0.dev 2025-08-15 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-102.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 chinmaygarde@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 b54cd5ef34786..5f2b49f753d44 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': 'cc008dc8e7aa27f975b109436dc6bb9d0b240c75', + 'dart_revision': '9277d6303da514dcbae6f7f6d8a51611f3b944aa', # 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 655633709a0a1db073ee27f0c6c15c640c2f3a8e Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 15 Aug 2025 15:41:29 -0400 Subject: [PATCH 079/720] Roll Skia from 5654ac32ede0 to 2f66be8a593a (6 revisions) (#173866) https://skia.googlesource.com/skia.git/+log/5654ac32ede0..2f66be8a593a 2025-08-15 syoussefi@google.com [vulkan] Re-enable use of VK_EXT_graphics_pipeline_library 2025-08-15 michaelludwig@google.com Add output colorspace parameter to SkShader::makeWithWorkingColorSpace 2025-08-15 michaelludwig@google.com [graphite] PatchWriter handles caps directly 2025-08-15 nicolettep@google.com Reland^2 "[graphite] Add AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM support" 2025-08-15 danieldilan@google.com Add invalid ICC profile test for rust png 2025-08-15 danieldilan@google.com Add default case for color encode 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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 5f2b49f753d44..301631e953975 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '5654ac32ede047f3640eb3118de53857f189d557', + 'skia_revision': '2f66be8a593af8741f5010ef2eccbf1b9cdb179d', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From e4f27cd09734db6c6ed94e104ab5333c48dfc544 Mon Sep 17 00:00:00 2001 From: lauraywu Date: Fri, 15 Aug 2025 16:41:21 -0400 Subject: [PATCH 080/720] Add onHover callback support for TableRowInkWell (#173373) - Add `onHover` parameter to `TableRowInkWell` - Add corresponding tests Issue: https://github.com/flutter/flutter/issues/173370 ## 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 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]. **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 [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/material/data_table.dart | 14 +++++++++++++- .../flutter/test/material/data_table_test.dart | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/data_table.dart b/packages/flutter/lib/src/material/data_table.dart index 983d48a30e7ae..5848b3c0d0a9f 100644 --- a/packages/flutter/lib/src/material/data_table.dart +++ b/packages/flutter/lib/src/material/data_table.dart @@ -148,6 +148,7 @@ class DataRow { this.selected = false, this.onSelectChanged, this.onLongPress, + this.onHover, this.color, this.mouseCursor, required this.cells, @@ -160,6 +161,7 @@ class DataRow { this.selected = false, this.onSelectChanged, this.onLongPress, + this.onHover, this.color, this.mouseCursor, required this.cells, @@ -200,6 +202,12 @@ class DataRow { /// that particular cell. final GestureLongPressCallback? onLongPress; + /// Called when a pointer enters or exits the row. + /// + /// The boolean value passed to the callback is true if a pointer has entered the row and false + /// when a pointer has exited the row. + final ValueChanged? onHover; + /// Whether the row is selected. /// /// If [onSelectChanged] is non-null for any row in the table, then @@ -951,6 +959,7 @@ class DataTable extends StatelessWidget { required GestureTapCancelCallback? onTapCancel, required MaterialStateProperty? overlayColor, required GestureLongPressCallback? onRowLongPress, + required ValueChanged? onRowHover, required MouseCursor? mouseCursor, }) { final ThemeData themeData = Theme.of(context); @@ -1007,10 +1016,11 @@ class DataTable extends StatelessWidget { overlayColor: overlayColor, child: label, ); - } else if (onSelectChanged != null || onRowLongPress != null) { + } else if (onSelectChanged != null || onRowLongPress != null || onRowHover != null) { label = TableRowInkWell( onTap: onSelectChanged, onLongPress: onRowLongPress, + onHover: onRowHover, overlayColor: overlayColor, mouseCursor: mouseCursor, child: label, @@ -1223,6 +1233,7 @@ class DataTable extends StatelessWidget { : () => row.onSelectChanged?.call(!row.selected), overlayColor: row.color ?? effectiveDataRowColor, onRowLongPress: row.onLongPress, + onRowHover: row.onHover, mouseCursor: row.mouseCursor?.resolve(states) ?? dataTableTheme.dataRowCursor?.resolve(states), ); @@ -1276,6 +1287,7 @@ class TableRowInkWell extends InkResponse { super.onDoubleTap, super.onLongPress, super.onHighlightChanged, + super.onHover, super.onSecondaryTap, super.onSecondaryTapDown, super.overlayColor, diff --git a/packages/flutter/test/material/data_table_test.dart b/packages/flutter/test/material/data_table_test.dart index bf4dd62a03a33..40b4c682fd409 100644 --- a/packages/flutter/test/material/data_table_test.dart +++ b/packages/flutter/test/material/data_table_test.dart @@ -47,6 +47,11 @@ void main() { onLongPress: () { log.add('onLongPress: ${dessert.name}'); }, + onHover: (bool hovering) { + if (hovering) { + log.add('onHover: ${dessert.name}'); + } + }, cells: [ DataCell(Text(dessert.name)), DataCell( @@ -91,6 +96,15 @@ void main() { expect(log, ['onLongPress: Cupcake']); log.clear(); + TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.moveTo(tester.getCenter(find.text('Cupcake'))); + + expect(log, ['onHover: Cupcake']); + log.clear(); + await tester.tap(find.text('Calories')); expect(log, ['column-sort: 1 true']); @@ -123,7 +137,7 @@ void main() { expect(log, ['cell-tapDown: 375', 'cell-tapCancel: 375', 'cell-longPress: 375']); log.clear(); - TestGesture gesture = await tester.startGesture(tester.getRect(find.text('375')).center); + gesture = await tester.startGesture(tester.getRect(find.text('375')).center); await tester.pump(const Duration(milliseconds: 100)); // onTapDown callback is registered. expect(log, equals(['cell-tapDown: 375'])); From 1ae899c5464781984c4616ab3e88867816d622e5 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 15 Aug 2025 17:38:22 -0400 Subject: [PATCH 081/720] Roll Fuchsia Linux SDK from zWRpLglb48zC1vZLv... to H1kVA85LyQsK8EDp2... (#173874) 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 chinmaygarde@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 301631e953975..ac3a905676841 100644 --- a/DEPS +++ b/DEPS @@ -810,7 +810,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'zWRpLglb48zC1vZLvXqnKtqDdcLIVvlZnG_SsGz8L54C' + 'version': 'H1kVA85LyQsK8EDp2cRaAtMd5_KgvnJZ43obPfdZuLAC' } ], 'condition': 'download_fuchsia_deps and not download_fuchsia_sdk', From 2c031ed023e379ccbf0d7d2101dba2949adef566 Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Fri, 15 Aug 2025 15:40:14 -0700 Subject: [PATCH 082/720] Reland predictive back route transitions by default (#173860) Relands https://github.com/flutter/flutter/pull/165832 by reverting flutter/flutter#173809. --- .../android_views/lib/main.dart | 13 +- .../test/material/divider/divider.0_test.dart | 6 +- .../test/widgets/basic/listener.0_test.dart | 9 +- .../widgets/basic/mouse_region.0_test.dart | 19 +- .../api/test/widgets/heroes/hero.0_test.dart | 14 +- .../transitions/align_transition.0_test.dart | 8 +- .../transitions/fade_transition.0_test.dart | 9 +- .../transitions/slide_transition.0_test.dart | 8 +- .../src/material/page_transitions_theme.dart | 10 +- .../test/material/bottom_app_bar_test.dart | 4 +- .../test/material/expansion_tile_test.dart | 8 +- .../material/flexible_space_bar_test.dart | 13 +- .../test/material/icon_button_test.dart | 4 +- .../test/material/navigation_drawer_test.dart | 2 +- .../navigation_drawer_theme_test.dart | 2 +- packages/flutter/test/material/page_test.dart | 23 +- .../material/page_transitions_theme_test.dart | 249 +++++++++++++++++- ...ve_back_page_transitions_builder_test.dart | 8 +- .../flutter/test/material/snack_bar_test.dart | 2 +- .../flutter/test/material/stepper_test.dart | 6 +- .../test/material/text_field_test.dart | 11 +- .../flutter/test/widgets/heroes_test.dart | 1 + .../flutter/test/widgets/navigator_test.dart | 4 +- .../test/widgets/slivers_evil_test.dart | 16 +- 24 files changed, 400 insertions(+), 49 deletions(-) diff --git a/dev/integration_tests/android_views/lib/main.dart b/dev/integration_tests/android_views/lib/main.dart index a6cea9a951496..0a853d7d286bf 100644 --- a/dev/integration_tests/android_views/lib/main.dart +++ b/dev/integration_tests/android_views/lib/main.dart @@ -16,7 +16,18 @@ final List _allPages = [ void main() { enableFlutterDriverExtension(handler: driverDataHandler.handleMessage); - runApp(const MaterialApp(home: Home())); + runApp( + MaterialApp( + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }, + ), + ), + home: const Home(), + ), + ); } class Home extends StatelessWidget { diff --git a/examples/api/test/material/divider/divider.0_test.dart b/examples/api/test/material/divider/divider.0_test.dart index 5e9ffe36327fb..eb353e81a699f 100644 --- a/examples/api/test/material/divider/divider.0_test.dart +++ b/examples/api/test/material/divider/divider.0_test.dart @@ -13,7 +13,11 @@ void main() { expect(find.byType(Divider), findsOneWidget); // Divider is positioned horizontally. - final Offset container = tester.getBottomLeft(find.byType(ColoredBox).first); + final Offset container = tester.getBottomLeft( + find + .descendant(of: find.byType(example.DividerExample), matching: find.byType(ColoredBox)) + .first, + ); expect(container.dy, tester.getTopLeft(find.byType(Divider)).dy); final Offset subheader = tester.getTopLeft(find.text('Subheader')); diff --git a/examples/api/test/widgets/basic/listener.0_test.dart b/examples/api/test/widgets/basic/listener.0_test.dart index fbdc1f808103e..f27ebc56b0536 100644 --- a/examples/api/test/widgets/basic/listener.0_test.dart +++ b/examples/api/test/widgets/basic/listener.0_test.dart @@ -16,7 +16,14 @@ void main() { final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); - await gesture.down(tester.getCenter(find.byType(ColoredBox))); + await gesture.down( + tester.getCenter( + find.descendant( + of: find.byType(example.ListenerExample), + matching: find.byType(ColoredBox), + ), + ), + ); await tester.pump(); expect(find.text('1 presses\n0 releases'), findsOneWidget); diff --git a/examples/api/test/widgets/basic/mouse_region.0_test.dart b/examples/api/test/widgets/basic/mouse_region.0_test.dart index 2f2276a175433..4c2db4daa3de6 100644 --- a/examples/api/test/widgets/basic/mouse_region.0_test.dart +++ b/examples/api/test/widgets/basic/mouse_region.0_test.dart @@ -18,13 +18,28 @@ void main() { final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); - await gesture.moveTo(tester.getCenter(find.byType(ColoredBox))); + await gesture.moveTo( + tester.getCenter( + find.descendant( + of: find.byType(example.MouseRegionExample), + matching: find.byType(ColoredBox), + ), + ), + ); await tester.pump(); expect(find.text('1 Entries\n0 Exits'), findsOneWidget); expect(find.text('The cursor is here: (400.00, 328.00)'), findsOneWidget); - await gesture.moveTo(tester.getCenter(find.byType(ColoredBox)) + const Offset(50.0, 30.0)); + await gesture.moveTo( + tester.getCenter( + find.descendant( + of: find.byType(example.MouseRegionExample), + matching: find.byType(ColoredBox), + ), + ) + + const Offset(50.0, 30.0), + ); await tester.pump(); expect(find.text('The cursor is here: (450.00, 358.00)'), findsOneWidget); diff --git a/examples/api/test/widgets/heroes/hero.0_test.dart b/examples/api/test/widgets/heroes/hero.0_test.dart index 1b766748bff44..702b1607c2be3 100644 --- a/examples/api/test/widgets/heroes/hero.0_test.dart +++ b/examples/api/test/widgets/heroes/hero.0_test.dart @@ -24,19 +24,19 @@ void main() { expect(heroSize.height.roundToDouble(), 60.0); // Jump to 50% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 189.0); expect(heroSize.height.roundToDouble(), 146.0); // Jump to 75% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 199.0); expect(heroSize.height.roundToDouble(), 190.0); // Jump to 100% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize, const Size(200.0, 200.0)); @@ -45,25 +45,25 @@ void main() { await tester.pump(); // Jump 25% into the transition (total length = 300ms) - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 199.0); expect(heroSize.height.roundToDouble(), 190.0); // Jump to 50% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 189.0); expect(heroSize.height.roundToDouble(), 146.0); // Jump to 75% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize.width.roundToDouble(), 103.0); expect(heroSize.height.roundToDouble(), 60.0); // Jump to 100% into the transition. - await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms + await tester.pump(quarterTransition); heroSize = tester.getSize(find.byType(Container)); expect(heroSize, const Size(50.0, 50.0)); }); diff --git a/examples/api/test/widgets/transitions/align_transition.0_test.dart b/examples/api/test/widgets/transitions/align_transition.0_test.dart index 9cb1745424391..86f1813f892f4 100644 --- a/examples/api/test/widgets/transitions/align_transition.0_test.dart +++ b/examples/api/test/widgets/transitions/align_transition.0_test.dart @@ -9,7 +9,13 @@ import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Shows flutter logo in transition', (WidgetTester tester) async { await tester.pumpWidget(const example.AlignTransitionExampleApp()); - expect(find.byType(ColoredBox), findsOneWidget); + expect( + find.descendant( + of: find.byType(example.AlignTransitionExample), + matching: find.byType(ColoredBox), + ), + findsOneWidget, + ); expect( find.byWidgetPredicate( (Widget padding) => padding is Padding && padding.padding == const EdgeInsets.all(8.0), diff --git a/examples/api/test/widgets/transitions/fade_transition.0_test.dart b/examples/api/test/widgets/transitions/fade_transition.0_test.dart index 87d13827d21e1..1a04ca801f827 100644 --- a/examples/api/test/widgets/transitions/fade_transition.0_test.dart +++ b/examples/api/test/widgets/transitions/fade_transition.0_test.dart @@ -13,7 +13,10 @@ void main() { await tester.pumpWidget(const example.FadeTransitionExampleApp()); expect( - find.ancestor(of: find.byType(FlutterLogo), matching: find.byType(FadeTransition)), + find.descendant( + of: find.byType(example.FadeTransitionExample), + matching: find.byType(FadeTransition), + ), findsOneWidget, ); }); @@ -21,8 +24,8 @@ void main() { testWidgets('FadeTransition animates', (WidgetTester tester) async { await tester.pumpWidget(const example.FadeTransitionExampleApp()); - final Finder fadeTransitionFinder = find.ancestor( - of: find.byType(FlutterLogo), + final Finder fadeTransitionFinder = find.descendant( + of: find.byType(example.FadeTransitionExample), matching: find.byType(FadeTransition), ); diff --git a/examples/api/test/widgets/transitions/slide_transition.0_test.dart b/examples/api/test/widgets/transitions/slide_transition.0_test.dart index 18b279a964120..7dd51699174cc 100644 --- a/examples/api/test/widgets/transitions/slide_transition.0_test.dart +++ b/examples/api/test/widgets/transitions/slide_transition.0_test.dart @@ -13,7 +13,13 @@ void main() { expect(find.byType(Center), findsOneWidget); expect(find.byType(FlutterLogo), findsOneWidget); expect(find.byType(Padding), findsAtLeast(1)); - expect(find.byType(SlideTransition), findsOneWidget); + expect( + find.descendant( + of: find.byType(example.SlideTransitionExample), + matching: find.byType(SlideTransition), + ), + findsOneWidget, + ); }); testWidgets('Animates repeatedly every 2 seconds', (WidgetTester tester) async { diff --git a/packages/flutter/lib/src/material/page_transitions_theme.dart b/packages/flutter/lib/src/material/page_transitions_theme.dart index c7051b1c09582..14cc8000c1913 100644 --- a/packages/flutter/lib/src/material/page_transitions_theme.dart +++ b/packages/flutter/lib/src/material/page_transitions_theme.dart @@ -17,6 +17,7 @@ import 'package:flutter/services.dart'; import 'color_scheme.dart'; import 'colors.dart'; +import 'predictive_back_page_transitions_builder.dart'; import 'theme.dart'; // Slides the page upwards and fades it in, starting from 1/4 screen @@ -759,7 +760,12 @@ class FadeForwardsPageTransitionsBuilder extends PageTransitionsBuilder { final Color? backgroundColor; /// The value of [transitionDuration] in milliseconds. - static const int kTransitionMilliseconds = 800; + /// + /// Eyeballed on a physical Pixel 9 running Android 16. This does not match + /// the actual value used by native Android, which is 800ms, because native + /// Android is using Material 3 Expressive springs that are not currently + /// supported by Flutter. So for now at least, this is an approximation. + static const int kTransitionMilliseconds = 450; @override Duration get transitionDuration => const Duration(milliseconds: kTransitionMilliseconds); @@ -1097,7 +1103,7 @@ class PageTransitionsTheme with Diagnosticable { static const Map _defaultBuilders = { - TargetPlatform.android: ZoomPageTransitionsBuilder(), + TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), TargetPlatform.windows: ZoomPageTransitionsBuilder(), diff --git a/packages/flutter/test/material/bottom_app_bar_test.dart b/packages/flutter/test/material/bottom_app_bar_test.dart index 556681a6c3d2a..704411db303cc 100644 --- a/packages/flutter/test/material/bottom_app_bar_test.dart +++ b/packages/flutter/test/material/bottom_app_bar_test.dart @@ -191,7 +191,9 @@ void main() { final BottomAppBar bottomAppBar = tester.widget(find.byType(BottomAppBar)); expect(bottomAppBar.padding, customPadding); final Rect babRect = tester.getRect(find.byType(BottomAppBar)); - final Rect childRect = tester.getRect(find.byType(ColoredBox)); + final Rect childRect = tester.getRect( + find.descendant(of: find.byType(BottomAppBar), matching: find.byType(ColoredBox)), + ); expect(childRect, const Rect.fromLTRB(250, 530, 550, 590)); expect(babRect, const Rect.fromLTRB(240, 520, 560, 600)); }); diff --git a/packages/flutter/test/material/expansion_tile_test.dart b/packages/flutter/test/material/expansion_tile_test.dart index a73a1ece581ba..98c08f1de11fa 100644 --- a/packages/flutter/test/material/expansion_tile_test.dart +++ b/packages/flutter/test/material/expansion_tile_test.dart @@ -1690,7 +1690,9 @@ void main() { ); final Size materialAppSize = tester.getSize(find.byType(MaterialApp)); - final Size titleSize = tester.getSize(find.byType(ColoredBox)); + final Size titleSize = tester.getSize( + find.descendant(of: find.byType(ExpansionTile), matching: find.byType(ColoredBox)), + ); expect(titleSize.width, materialAppSize.width); }, @@ -1713,7 +1715,9 @@ void main() { ); final Size materialAppSize = tester.getSize(find.byType(MaterialApp)); - final Size titleSize = tester.getSize(find.byType(ColoredBox)); + final Size titleSize = tester.getSize( + find.descendant(of: find.byType(ExpansionTile), matching: find.byType(ColoredBox)), + ); expect(titleSize.width, materialAppSize.width - 32.0); }, diff --git a/packages/flutter/test/material/flexible_space_bar_test.dart b/packages/flutter/test/material/flexible_space_bar_test.dart index e417b75c384b1..04449422eb0cf 100644 --- a/packages/flutter/test/material/flexible_space_bar_test.dart +++ b/packages/flutter/test/material/flexible_space_bar_test.dart @@ -1462,7 +1462,18 @@ void main() { }); testWidgets('FlexibleSpaceBar rebuilds when scrolling.', (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp(home: SubCategoryScreenView())); + await tester.pumpWidget( + MaterialApp( + home: const SubCategoryScreenView(), + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }, + ), + ), + ), + ); expect(RenderRebuildTracker.count, 1); expect( diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index b3291731626a8..55e3b216c1208 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -3027,7 +3027,9 @@ void main() { ), ); - final Offset topLeft = tester.getTopLeft(find.byType(ColoredBox)); + final Offset topLeft = tester.getTopLeft( + find.descendant(of: find.byType(Center), matching: find.byType(ColoredBox)), + ); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(); await gesture.moveTo(topLeft); diff --git a/packages/flutter/test/material/navigation_drawer_test.dart b/packages/flutter/test/material/navigation_drawer_test.dart index 667be62ebfa45..96ca683a7467e 100644 --- a/packages/flutter/test/material/navigation_drawer_test.dart +++ b/packages/flutter/test/material/navigation_drawer_test.dart @@ -550,7 +550,7 @@ InkWell? _getInkWell(WidgetTester tester) { ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { return tester .firstWidget( - find.descendant(of: find.byType(FadeTransition), matching: find.byType(Container)), + find.descendant(of: find.byType(NavigationIndicator), matching: find.byType(Container)), ) .decoration as ShapeDecoration?; diff --git a/packages/flutter/test/material/navigation_drawer_theme_test.dart b/packages/flutter/test/material/navigation_drawer_theme_test.dart index 4c7cf54194f43..a7b164f5be404 100644 --- a/packages/flutter/test/material/navigation_drawer_theme_test.dart +++ b/packages/flutter/test/material/navigation_drawer_theme_test.dart @@ -284,7 +284,7 @@ Material _getMaterial(WidgetTester tester) { ShapeDecoration? _getIndicatorDecoration(WidgetTester tester) { return tester .firstWidget( - find.descendant(of: find.byType(FadeTransition), matching: find.byType(Container)), + find.descendant(of: find.byType(NavigationIndicator), matching: find.byType(Container)), ) .decoration as ShapeDecoration?; diff --git a/packages/flutter/test/material/page_test.dart b/packages/flutter/test/material/page_test.dart index 7b0a4d4610d03..f72f1d799e9f0 100644 --- a/packages/flutter/test/material/page_test.dart +++ b/packages/flutter/test/material/page_test.dart @@ -190,6 +190,13 @@ void main() { await tester.pumpWidget( MaterialApp( + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }, + ), + ), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( allowSnapshotting: false, @@ -268,7 +275,14 @@ void main() { RepaintBoundary( key: key, child: MaterialApp( - theme: ThemeData(useMaterial3: false), + theme: ThemeData( + useMaterial3: false, + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }, + ), + ), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { @@ -315,6 +329,13 @@ void main() { key: key, child: MaterialApp( debugShowCheckedModeBanner: false, // https://github.com/flutter/flutter/issues/143616 + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: ZoomPageTransitionsBuilder(), + }, + ), + ), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { diff --git a/packages/flutter/test/material/page_transitions_theme_test.dart b/packages/flutter/test/material/page_transitions_theme_test.dart index bef2440dca09a..bf04f9e59074d 100644 --- a/packages/flutter/test/material/page_transitions_theme_test.dart +++ b/packages/flutter/test/material/page_transitions_theme_test.dart @@ -75,7 +75,7 @@ void main() { ); testWidgets( - 'Default PageTransitionsTheme builds a _ZoomPageTransition for android', + 'Default PageTransitionsTheme builds a _FadeForwardsPageTransition for android', (WidgetTester tester) async { final Map routes = { '/': (BuildContext context) => Material( @@ -91,11 +91,11 @@ void main() { await tester.pumpWidget(MaterialApp(routes: routes)); - Finder findZoomPageTransition() { + Finder findFadeForwardsPageTransition() { return find.descendant( of: find.byType(MaterialApp), matching: find.byWidgetPredicate( - (Widget w) => '${w.runtimeType}' == '_ZoomPageTransition', + (Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition', ), ); } @@ -104,12 +104,12 @@ void main() { Theme.of(tester.element(find.text('push'))).platform, debugDefaultTargetPlatformOverride, ); - expect(findZoomPageTransition(), findsOneWidget); + expect(findFadeForwardsPageTransition(), findsOneWidget); await tester.tap(find.text('push')); await tester.pumpAndSettle(); expect(find.text('page b'), findsOneWidget); - expect(findZoomPageTransition(), findsOneWidget); + expect(findFadeForwardsPageTransition(), findsOneWidget); }, variant: TargetPlatformVariant.only(TargetPlatform.android), ); @@ -1034,7 +1034,18 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('push'), findsNothing); + switch (defaultTargetPlatform) { + case TargetPlatform.android: + // Shows both pages while doing the "peek" predicitve back transition. + expect(find.text('push'), findsOneWidget); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + // Does no transition yet; still shows page b only. + expect(find.text('push'), findsNothing); + } expect(find.text('page b'), findsOneWidget); // Commit the system back gesture. @@ -1047,13 +1058,237 @@ void main() { (ByteData? _) {}, ); await tester.pumpAndSettle(); - expect(find.text('push'), findsOneWidget); expect(find.text('page b'), findsNothing); }, variant: TargetPlatformVariant.all(), ); + testWidgets('predictive back is the default on Android', (WidgetTester tester) async { + final Map routes = { + '/': (BuildContext context) => Material( + child: TextButton( + child: const Text('push'), + onPressed: () { + Navigator.of(context).pushNamed('/b'); + }, + ), + ), + }; + await tester.pumpWidget(MaterialApp(routes: routes)); + + final ThemeData themeData = Theme.of(tester.element(find.text('push'))); + switch (defaultTargetPlatform) { + case TargetPlatform.android: + expect( + themeData.pageTransitionsTheme.builders[defaultTargetPlatform], + isA(), + ); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + expect( + themeData.pageTransitionsTheme.builders[defaultTargetPlatform], + isNot(isA()), + ); + } + }, variant: TargetPlatformVariant.all()); + + testWidgets('predictive back falls back to FadeForwardsPageTransition', ( + WidgetTester tester, + ) async { + Finder findPredictiveBackPageTransition() { + return find.descendant( + of: find.byType(PrimaryScrollController), + matching: find.byWidgetPredicate( + (Widget w) => '${w.runtimeType}' == '_PredictiveBackSharedElementPageTransition', + ), + ); + } + + Finder findFallbackPageTransition() { + return find.descendant( + of: find.byType(PrimaryScrollController), + matching: find.byWidgetPredicate( + (Widget w) => '${w.runtimeType}' == '_FadeForwardsPageTransition', + ), + ); + } + + final Map routes = { + '/': (BuildContext context) => Material( + child: TextButton( + child: const Text('push'), + onPressed: () { + Navigator.of(context).pushNamed('/b'); + }, + ), + ), + '/b': (BuildContext context) => const Text('page b'), + }; + + await tester.pumpWidget( + MaterialApp( + routes: routes, + theme: ThemeData( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.iOS: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.macOS: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.windows: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.linux: PredictiveBackPageTransitionsBuilder(), + TargetPlatform.fuchsia: PredictiveBackPageTransitionsBuilder(), + }, + ), + ), + ), + ); + + final ThemeData themeData = Theme.of(tester.element(find.text('push'))); + expect( + themeData.pageTransitionsTheme.builders[defaultTargetPlatform], + isA(), + ); + + expect(find.text('push'), findsOneWidget); + expect(find.text('page b'), findsNothing); + + await tester.tap(find.text('push')); + await tester.pumpAndSettle(); + + expect(find.text('push'), findsNothing); + expect(find.text('page b'), findsOneWidget); + + // Only Android sends system back gestures. + if (defaultTargetPlatform == TargetPlatform.android) { + final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( + const MethodCall('startBackGesture', { + 'touchOffset': [5.0, 300.0], + 'progress': 0.0, + 'swipeEdge': 0, // left + }), + ); + await binding.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/backgesture', + startMessage, + (ByteData? _) {}, + ); + await tester.pump(); + } + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + expect(findPredictiveBackPageTransition(), findsOneWidget); + expect(findFallbackPageTransition(), findsNothing); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + expect(findPredictiveBackPageTransition(), findsNothing); + expect(findFallbackPageTransition(), findsOneWidget); + } + + expect(find.text('push'), findsNothing); + expect(find.text('page b'), findsOneWidget); + + // Drag the system back gesture far enough to commit. + if (defaultTargetPlatform == TargetPlatform.android) { + final ByteData updateMessage = const StandardMethodCodec().encodeMethodCall( + const MethodCall('updateBackGestureProgress', { + 'x': 100.0, + 'y': 300.0, + 'progress': 0.35, + 'swipeEdge': 0, // left + }), + ); + await binding.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/backgesture', + updateMessage, + (ByteData? _) {}, + ); + await tester.pumpAndSettle(); + expect(find.text('push'), findsOneWidget); + } else { + expect(find.text('push'), findsNothing); + } + + expect(find.text('page b'), findsOneWidget); + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + expect(findPredictiveBackPageTransition(), findsNWidgets(2)); + expect(findFallbackPageTransition(), findsNothing); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + expect(findPredictiveBackPageTransition(), findsNothing); + expect(findFallbackPageTransition(), findsOneWidget); + } + + if (defaultTargetPlatform == TargetPlatform.android) { + // Commit the system back gesture on Android. + final ByteData commitMessage = const StandardMethodCodec().encodeMethodCall( + const MethodCall('commitBackGesture'), + ); + await binding.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/backgesture', + commitMessage, + (ByteData? _) {}, + ); + } else { + // On other platforms, send a one-off system pop. + final ByteData popMessage = const JSONMethodCodec().encodeMethodCall( + const MethodCall('popRoute'), + ); + await binding.defaultBinaryMessenger.handlePlatformMessage( + 'flutter/navigation', + popMessage, + (ByteData? _) {}, + ); + } + await tester.pump(); + + expect(find.text('push'), findsOneWidget); + expect(find.text('page b'), findsOneWidget); + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + expect(findPredictiveBackPageTransition(), findsNWidgets(2)); + expect(findFallbackPageTransition(), findsNothing); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + expect(findPredictiveBackPageTransition(), findsNothing); + expect(findFallbackPageTransition(), findsNWidgets(2)); + } + + await tester.pumpAndSettle(); + + expect(find.text('push'), findsOneWidget); + expect(find.text('page b'), findsNothing); + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + expect(findPredictiveBackPageTransition(), findsNothing); + expect(findFallbackPageTransition(), findsOneWidget); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.fuchsia: + case TargetPlatform.windows: + expect(findPredictiveBackPageTransition(), findsNothing); + expect(findFallbackPageTransition(), findsOneWidget); + } + }, variant: TargetPlatformVariant.all()); + testWidgets( 'ZoomPageTransitionsBuilder uses theme color during transition effects', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart b/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart index 42200f91f4f20..e595d3ab4b686 100644 --- a/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart +++ b/packages/flutter/test/material/predictive_back_page_transitions_builder_test.dart @@ -63,7 +63,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackPageTransition for the page transition. + // _PredictiveBackSharedElementPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], @@ -173,7 +173,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackPageTransition for the page transition. + // _PredictiveBackSharedElementPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], @@ -326,7 +326,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackPageTransition for the page transition. + // _PredictiveBackSharedElementPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], @@ -488,7 +488,7 @@ void main() { } // Start a system pop gesture, which will switch to using - // _PredictiveBackPageTransition for the page transition. + // _PredictiveBackSharedElementPageTransition for the page transition. final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( const MethodCall('startBackGesture', { 'touchOffset': [5.0, 300.0], diff --git a/packages/flutter/test/material/snack_bar_test.dart b/packages/flutter/test/material/snack_bar_test.dart index 351a6ef32811a..cf8baa0b8477f 100644 --- a/packages/flutter/test/material/snack_bar_test.dart +++ b/packages/flutter/test/material/snack_bar_test.dart @@ -2753,7 +2753,7 @@ void main() { expect(find.text(snackBarText), findsOneWidget); expect(find.text(firstHeader), findsOneWidget); expect(find.text(secondHeader), findsOneWidget); - await tester.pump(const Duration(milliseconds: 750)); + await tester.pump(const Duration(milliseconds: 1500)); expect(find.text(snackBarText), findsOneWidget); expect(find.text(firstHeader), findsNothing); expect(find.text(secondHeader), findsOneWidget); diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart index 9d150d970eea5..0f7b565eaab19 100644 --- a/packages/flutter/test/material/stepper_test.dart +++ b/packages/flutter/test/material/stepper_test.dart @@ -1298,7 +1298,11 @@ void main() { ?.color; Color lineColor() { - return tester.widget(find.byType(ColoredBox)).color; + return tester + .widget( + find.descendant(of: find.byType(Stepper), matching: find.byType(ColoredBox)), + ) + .color; } // Step 1 diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 0061595456a23..fec45654f019b 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -2122,10 +2122,15 @@ void main() { await tester.tap(find.byType(TextField)); // Wait for context menu to be built. await tester.pumpAndSettle(); - final RenderBox container = tester.renderObject( - find.descendant(of: find.byType(SnapshotWidget), matching: find.byType(SizedBox)).first, + expect(find.byType(AdaptiveTextSelectionToolbar), findsOneWidget); + final SizedBox sizedBox = tester.widget( + find.descendant( + of: find.byType(AdaptiveTextSelectionToolbar), + matching: find.byType(SizedBox), + ), ); - expect(container.size, Size.zero); + expect(sizedBox.width, 0.0); + expect(sizedBox.height, 0.0); }, variant: const TargetPlatformVariant({ TargetPlatform.android, diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index 5192603c482f8..fd2335afc6bb3 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -1449,6 +1449,7 @@ Future main() async { .text('Hero') .evaluate() .map((Element e) => e.renderObject!); + await tester.pump(const Duration(milliseconds: 1)); expect(renderObjects.where(isVisible).length, 1); // Hero BC's flight finishes normally. diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index 8ba7ddb118021..b69d131040d06 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -2976,12 +2976,12 @@ void main() { await tester.pump(kFourTenthsOfTheTransitionDuration); expect(find.text('Route: 3'), findsOneWidget); expect(find.text('Route: 4'), findsOneWidget); - expect(route4Entry.value, 0.4); + expect(route4Entry.value, moreOrLessEquals(0.4)); await tester.pump(kFourTenthsOfTheTransitionDuration); expect(find.text('Route: 3'), findsOneWidget); expect(find.text('Route: 4'), findsOneWidget); - expect(route4Entry.value, 0.8); + expect(route4Entry.value, moreOrLessEquals(0.8)); expect(find.text('Route: 2', skipOffstage: false), findsOneWidget); expect(find.text('Route: 1', skipOffstage: false), findsOneWidget); expect(find.text('Route: root', skipOffstage: false), findsOneWidget); diff --git a/packages/flutter/test/widgets/slivers_evil_test.dart b/packages/flutter/test/widgets/slivers_evil_test.dart index ef5beda4cf266..6f00fdacaabd0 100644 --- a/packages/flutter/test/widgets/slivers_evil_test.dart +++ b/packages/flutter/test/widgets/slivers_evil_test.dart @@ -241,9 +241,17 @@ void main() { await tester.drag(find.text('5'), const Offset(0.0, -500.0)); await tester.pump(); + Finder findItem(String text) { + return find.descendant( + of: find.byType(SliverFixedExtentList), + matching: find.widgetWithText(ColoredBox, text), + ); + } + // Screen is 600px high. Moved bottom item 500px up. It's now at the top. - expect(tester.getTopLeft(find.widgetWithText(ColoredBox, '5')).dy, 0.0); - expect(tester.getBottomLeft(find.widgetWithText(ColoredBox, '10')).dy, 600.0); + expect(findItem('5'), findsOneWidget); + expect(tester.getTopLeft(findItem('5')).dy, 0.0); + expect(tester.getBottomLeft(findItem('10')).dy, 600.0); // Stop returning the first 3 items. await tester.pumpWidget( @@ -271,10 +279,10 @@ void main() { // Move up by 4 items, meaning item 1 would have been at the top but // 0 through 3 no longer exist, so item 4, 3 items down, is the first one. // Item 4 is also shifted to the top. - expect(tester.getTopLeft(find.widgetWithText(ColoredBox, '4')).dy, 0.0); + expect(tester.getTopLeft(findItem('4')).dy, 0.0); // Because the screen is still 600px, item 9 is now visible at the bottom instead // of what's supposed to be item 6 had we not re-shifted. - expect(tester.getBottomLeft(find.widgetWithText(ColoredBox, '9')).dy, 600.0); + expect(tester.getBottomLeft(findItem('9')).dy, 600.0); }); } From 35375e43fbae661f41dd8a6bda7454f8d55a4d54 Mon Sep 17 00:00:00 2001 From: Hannah Jin Date: Fri, 15 Aug 2025 15:41:49 -0700 Subject: [PATCH 083/720] [a11y] : set isFocused will update isFocusable to true (#170935) the isFocused and isFocusable flags should be more consistent, this is also to prepare for https://github.com/flutter/flutter/pull/170696 , So all EditableText and TextField widgets will have isFocusable flag set to true now. (because they set isFocused). ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] 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]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [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: chunhtai <47866232+chunhtai@users.noreply.github.com> --- .../lib/src/rendering/custom_paint.dart | 2 +- .../flutter/lib/src/rendering/object.dart | 2 +- .../flutter/lib/src/semantics/semantics.dart | 32 +++++++++++++++++-- .../flutter/lib/src/widgets/focus_scope.dart | 2 +- .../test/semantics/semantics_test.dart | 2 +- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/flutter/lib/src/rendering/custom_paint.dart b/packages/flutter/lib/src/rendering/custom_paint.dart index bdf755d7e7416..969c45f6bc6d9 100644 --- a/packages/flutter/lib/src/rendering/custom_paint.dart +++ b/packages/flutter/lib/src/rendering/custom_paint.dart @@ -953,7 +953,7 @@ class RenderCustomPaint extends RenderProxyBox { config.isFocusable = properties.focusable!; } if (properties.focused != null) { - config.isFocused = properties.focused!; + config.isFocused = properties.focused; } if (properties.enabled != null) { config.isEnabled = properties.enabled; diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index ac7ef2103de47..2ed1fc424a42d 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -4857,7 +4857,7 @@ mixin SemanticsAnnotationsMixin on RenderObject { config.isFocusable = _properties.focusable!; } if (_properties.focused != null) { - config.isFocused = _properties.focused!; + config.isFocused = _properties.focused; } if (_properties.inMutuallyExclusiveGroup != null) { config.isInMutuallyExclusiveGroup = _properties.inMutuallyExclusiveGroup!; diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index bd5860ccaea65..034dab4fdece8 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -1472,6 +1472,11 @@ class SemanticsProperties extends DiagnosticableTree { this.slider, this.keyboardKey, this.readOnly, + @Deprecated( + 'Use focused instead. ' + 'Setting focused automatically set focusable. ' + 'This feature was deprecated after v3.36.0-0.0.pre.', + ) this.focusable, this.focused, this.inMutuallyExclusiveGroup, @@ -1656,10 +1661,17 @@ class SemanticsProperties extends DiagnosticableTree { /// to be confused with accessibility focus. Accessibility focus is the /// green/black rectangular highlight that TalkBack/VoiceOver draws around the /// element it is reading, and is separate from input focus. + @Deprecated( + 'Use focused instead. ' + 'Setting focused automatically set focusable. ' + 'This feature was deprecated after v3.36.0-0.0.pre.', + ) final bool? focusable; /// If non-null, whether the node currently holds input focus. /// + /// If null, the node is not fosusable. + /// /// At most one node in the tree should hold input focus at any point in time, /// and it should not be set to true if [focusable] is false. /// @@ -5589,16 +5601,30 @@ class SemanticsConfiguration { } /// Whether the owning [RenderObject] can hold the input focus. + @Deprecated( + 'Check if isFocused is null instead. ' + 'This feature was deprecated after v3.36.0-0.0.pre.', + ) bool get isFocusable => _flags.isFocusable; + + @Deprecated( + 'Setting isFocused automatically set this to true. ' + 'This feature was deprecated after v3.36.0-0.0.pre.', + ) set isFocusable(bool value) { _flags = _flags.copyWith(isFocusable: value); _hasBeenAnnotated = true; } /// Whether the owning [RenderObject] currently holds the input focus. - bool get isFocused => _flags.isFocused; - set isFocused(bool value) { - _flags = _flags.copyWith(isFocused: value); + /// If the value is `null`, it's not focusable. + bool? get isFocused => _flags.isFocusable ? _flags.isFocused : null; + set isFocused(bool? value) { + if (value != null) { + _flags = _flags.copyWith(isFocusable: true, isFocused: value); + } else { + _flags = _flags.copyWith(isFocusable: false, isFocused: false); + } _hasBeenAnnotated = true; } diff --git a/packages/flutter/lib/src/widgets/focus_scope.dart b/packages/flutter/lib/src/widgets/focus_scope.dart index 0b0ea2ab2a972..661b6510523a5 100644 --- a/packages/flutter/lib/src/widgets/focus_scope.dart +++ b/packages/flutter/lib/src/widgets/focus_scope.dart @@ -724,7 +724,7 @@ class _FocusState extends State { ? focusNode.requestFocus : null, focusable: _couldRequestFocus, - focused: _hadPrimaryFocus, + focused: _couldRequestFocus ? _hadPrimaryFocus : null, child: widget.child, ); } diff --git a/packages/flutter/test/semantics/semantics_test.dart b/packages/flutter/test/semantics/semantics_test.dart index ecfba5fac5c5c..6864f2cd0810d 100644 --- a/packages/flutter/test/semantics/semantics_test.dart +++ b/packages/flutter/test/semantics/semantics_test.dart @@ -937,7 +937,7 @@ void main() { expect(config.isChecked, null); expect(config.isSelected, isFalse); expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isFalse); - expect(config.isFocused, isFalse); + expect(config.isFocused, null); expect(config.isTextField, isFalse); expect(config.onShowOnScreen, isNull); From f04e092b327efb423c709e561a72abe7164f723e Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 15 Aug 2025 18:41:51 -0400 Subject: [PATCH 084/720] Roll Skia from 2f66be8a593a to 91ad1f21ca61 (3 revisions) (#173877) https://skia.googlesource.com/skia.git/+log/2f66be8a593a..91ad1f21ca61 2025-08-15 mike@reedtribe.org Factor out common pathiter 2025-08-15 thomsmit@google.com [graphite] Lift floatStorageManager to recorder 2025-08-15 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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 ac3a905676841..03641f94537a8 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '2f66be8a593af8741f5010ef2eccbf1b9cdb179d', + 'skia_revision': '91ad1f21ca61960b74a6a13ffa4d5ddce463a6e4', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 37848d12a8717309e45ebb1611fa5180ab4ceb86 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Fri, 15 Aug 2025 18:56:22 -0400 Subject: [PATCH 085/720] Roll `package:analyzer` forward to `8.1.1` (#173867) --- dev/bots/custom_rules/no_double_clamp.dart | 2 +- dev/bots/custom_rules/no_stop_watches.dart | 6 ++-- .../protect_public_state_subtypes.dart | 4 +-- .../custom_rules/render_box_intrinsics.dart | 4 +-- .../src/widget_preview/dependency_graph.dart | 14 ++++---- .../preview_code_generator.dart | 31 ++++++++-------- .../src/widget_preview/preview_detector.dart | 6 ++-- .../lib/src/widget_preview/utils.dart | 16 ++++----- packages/flutter_tools/pubspec.yaml | 16 ++++----- .../widget_preview_scaffold/pubspec.yaml | 8 ++--- pubspec.lock | 36 +++++++++---------- pubspec.yaml | 20 +++++------ 12 files changed, 80 insertions(+), 83 deletions(-) diff --git a/dev/bots/custom_rules/no_double_clamp.dart b/dev/bots/custom_rules/no_double_clamp.dart index e2fd4539044c8..3483c3d31d9da 100644 --- a/dev/bots/custom_rules/no_double_clamp.dart +++ b/dev/bots/custom_rules/no_double_clamp.dart @@ -65,7 +65,7 @@ class _DoubleClampVisitor extends RecursiveAstVisitor { @override void visitSimpleIdentifier(SimpleIdentifier node) { - if (node.name != 'clamp' || node.staticElement is! MethodElement) { + if (node.name != 'clamp' || node.element is! MethodElement) { return; } final bool isAllowed = switch (node.parent) { diff --git a/dev/bots/custom_rules/no_stop_watches.dart b/dev/bots/custom_rules/no_stop_watches.dart index 2d08ff69920a3..4a1ce21ff9924 100644 --- a/dev/bots/custom_rules/no_stop_watches.dart +++ b/dev/bots/custom_rules/no_stop_watches.dart @@ -93,7 +93,7 @@ class _StopwatchVisitor extends RecursiveAstVisitor { bool _isInternal(LibraryElement libraryElement) { return path.isWithin( compilationUnit.session.analysisContext.contextRoot.root.path, - libraryElement.source.fullName, + libraryElement.firstFragment.source.fullName, ); } @@ -118,7 +118,7 @@ class _StopwatchVisitor extends RecursiveAstVisitor { @override void visitConstructorName(ConstructorName node) { - final Element? element = node.staticElement; + final Element? element = node.element; if (element is! ConstructorElement) { assert(false, '$element of $node is not a ConstructorElement.'); return; @@ -137,7 +137,7 @@ class _StopwatchVisitor extends RecursiveAstVisitor { @override void visitSimpleIdentifier(SimpleIdentifier node) { - final bool isAllowed = switch (node.staticElement) { + final bool isAllowed = switch (node.element) { ExecutableElement( returnType: DartType(element: final ClassElement classElement), library: final LibraryElement libraryElement, diff --git a/dev/bots/custom_rules/protect_public_state_subtypes.dart b/dev/bots/custom_rules/protect_public_state_subtypes.dart index 9d0726e6c7225..a02d25a1b740a 100644 --- a/dev/bots/custom_rules/protect_public_state_subtypes.dart +++ b/dev/bots/custom_rules/protect_public_state_subtypes.dart @@ -73,7 +73,7 @@ class _StateSubclassVisitor extends SimpleAstVisitor { @override void visitClassDeclaration(ClassDeclaration node) { - if (isPublicStateSubtype(node.declaredElement!)) { + if (isPublicStateSubtype(node.declaredFragment!.element)) { node.visitChildren(this); } } @@ -92,7 +92,7 @@ class _StateSubclassVisitor extends SimpleAstVisitor { case 'dispose': case 'build': case 'debugFillProperties': - if (!node.declaredElement!.hasProtected) { + if (!node.declaredFragment!.element.metadata.hasProtected) { unprotectedMethods.add(node); } } diff --git a/dev/bots/custom_rules/render_box_intrinsics.dart b/dev/bots/custom_rules/render_box_intrinsics.dart index 9b0d869239630..5c3c3c0b58b6e 100644 --- a/dev/bots/custom_rules/render_box_intrinsics.dart +++ b/dev/bots/custom_rules/render_box_intrinsics.dart @@ -66,7 +66,7 @@ class _RenderBoxSubclassVisitor extends RecursiveAstVisitor { // The cached version, call this method instead of _checkIfImplementsRenderBox. static bool _implementsRenderBox(InterfaceElement interfaceElement) { // Framework naming convention: a RenderObject subclass names have "Render" in its name. - if (!interfaceElement.name.contains('Render')) { + if (!interfaceElement.name!.contains('Render')) { return false; } return interfaceElement.name == 'RenderBox' || @@ -115,7 +115,7 @@ class _RenderBoxSubclassVisitor extends RecursiveAstVisitor { if (isCallingSuperImplementation) { return; } - final Element? declaredInClassElement = node.staticElement?.declaration?.enclosingElement3; + final Element? declaredInClassElement = node.element?.enclosingElement; if (declaredInClassElement is InterfaceElement && _implementsRenderBox(declaredInClassElement)) { violationNodes.add((node, correctMethodName)); diff --git a/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart b/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart index c8251d184d6bd..4582f9b8ba8a3 100644 --- a/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart +++ b/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart @@ -95,7 +95,7 @@ class _PreviewVisitor extends RecursiveAstVisitor { PreviewDetails( packageName: packageName, functionName: _currentFunction!.name.toString(), - isBuilder: returnType.name2.isWidgetBuilder, + isBuilder: returnType.name.isWidgetBuilder, previewAnnotation: preview, ), ); @@ -117,7 +117,7 @@ class _PreviewVisitor extends RecursiveAstVisitor { PreviewDetails( packageName: packageName, functionName: '${parentClass.name}.${_currentMethod!.name}', - isBuilder: returnType.name2.isWidgetBuilder, + isBuilder: returnType.name.isWidgetBuilder, previewAnnotation: preview, ), ); @@ -134,7 +134,7 @@ class _PreviewVisitor extends RecursiveAstVisitor { /// Contains all the information related to a library being watched by [PreviewDetector]. final class LibraryPreviewNode { - LibraryPreviewNode({required LibraryElement2 library, required this.logger}) + LibraryPreviewNode({required LibraryElement library, required this.logger}) : path = library.toPreviewPath() { final libraryFilePaths = [ for (final LibraryFragment fragment in library.fragments) fragment.source.fullName, @@ -190,7 +190,7 @@ final class LibraryPreviewNode { /// Finds all previews defined in the [lib] and adds them to [previews]. void findPreviews({required ResolvedLibraryResult lib}) { // Iterate over the compilation unit's AST to find previews. - final visitor = _PreviewVisitor(lib: lib.element2); + final visitor = _PreviewVisitor(lib: lib.element); for (final ResolvedUnitResult libUnit in lib.units) { libUnit.unit.visitChildren(visitor); } @@ -212,13 +212,13 @@ final class LibraryPreviewNode { for (final unit in units) { final LibraryFragment fragment = unit.libraryFragment; - for (final LibraryImport importedLib in fragment.libraryImports2) { - if (importedLib.importedLibrary2 == null) { + for (final LibraryImport importedLib in fragment.libraryImports) { + if (importedLib.importedLibrary == null) { // This is an import for a file that's not analyzed (likely an import of a package from // the pub-cache) and isn't necessary to track as part of the dependency graph. continue; } - final LibraryElement2 importedLibrary = importedLib.importedLibrary2!; + final LibraryElement importedLibrary = importedLib.importedLibrary!; final LibraryPreviewNode result = graph.putIfAbsent( importedLibrary.toPreviewPath(), () => LibraryPreviewNode(library: importedLibrary, logger: logger), diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart index 0e88caa3e3e8a..fb86117d10940 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart @@ -6,8 +6,6 @@ import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element2.dart' as analyzer; import 'package:analyzer/dart/element/element2.dart'; import 'package:analyzer/dart/element/type.dart'; -// ignore: implementation_imports, can be removed when package:analyzer 8.1.0 is released. -import 'package:analyzer/src/dart/constant/value.dart'; import 'package:built_collection/built_collection.dart'; import 'package:code_builder/code_builder.dart' as cb; import 'package:dart_style/dart_style.dart'; @@ -254,7 +252,6 @@ class PreviewCodeGenerator { extension on DartObject { cb.Expression toExpression() { - final objectImpl = this as DartObjectImpl; final DartType type = this.type!; return switch (type) { DartType(isDartCoreBool: true) => cb.literalBool(toBoolValue()!), @@ -262,27 +259,27 @@ extension on DartObject { DartType(isDartCoreInt: true) => cb.literalNum(toIntValue()!), DartType(isDartCoreString: true) => cb.literalString(toStringValue()!), DartType(isDartCoreNull: true) => cb.literalNull, - InterfaceType(element3: EnumElement2()) => _createEnumInstance(objectImpl), - InterfaceType() => _createInstance(type, objectImpl), - FunctionType() => _createTearoff(toFunctionValue2()!), + InterfaceType(element3: EnumElement()) => _createEnumInstance(this), + InterfaceType() => _createInstance(type, this), + FunctionType() => _createTearoff(toFunctionValue()!), _ => throw UnsupportedError('Unexpected DartObject type: $runtimeType'), }; } - cb.Expression _createTearoff(ExecutableElement2 element) { + cb.Expression _createTearoff(ExecutableElement element) { return cb.refer(element.displayName, _elementToLibraryIdentifier(element)); } - cb.Expression _createEnumInstance(DartObjectImpl object) { - final VariableElement2 variable = object.variable2!; + cb.Expression _createEnumInstance(DartObject object) { + final VariableElement variable = object.variable!; return switch (variable) { - FieldElement2( + FieldElement( isEnumConstant: true, displayName: final enumValue, - enclosingElement2: EnumElement2(displayName: final enumName), + enclosingElement: EnumElement(displayName: final enumName), ) => cb.refer('$enumName.$enumValue', _elementToLibraryIdentifier(variable)), - PropertyInducingElement2(:final displayName) => cb.refer( + PropertyInducingElement(:final displayName) => cb.refer( displayName, _elementToLibraryIdentifier(variable), ), @@ -290,9 +287,9 @@ extension on DartObject { }; } - cb.Expression _createInstance(InterfaceType dartType, DartObjectImpl object) { - final ConstructorInvocation constructorInvocation = object.getInvocation()!; - final ConstructorElement2 constructor = constructorInvocation.constructor2; + cb.Expression _createInstance(InterfaceType dartType, DartObject object) { + final ConstructorInvocation constructorInvocation = object.constructorInvocation!; + final ConstructorElement constructor = constructorInvocation.constructor; final cb.Expression type = cb.refer( dartType.element3.name3!, _elementToLibraryIdentifier(dartType.element3), @@ -317,6 +314,6 @@ extension on DartObject { ); } - /// Returns the import URI for the [analyzer.LibraryElement2] containing [element]. - String? _elementToLibraryIdentifier(analyzer.Element2? element) => element?.library2!.identifier; + /// Returns the import URI for the [analyzer.LibraryElement] containing [element]. + String? _elementToLibraryIdentifier(analyzer.Element? element) => element?.library!.identifier; } diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart index 7e517a1d82de8..dfd95a8f2d540 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart @@ -191,12 +191,12 @@ class PreviewDetector { final unit = (await context.currentSession.getResolvedUnit(filePath)) as ResolvedUnitResult; lib = await context.currentSession.getResolvedLibrary( - unit.libraryElement2.firstFragment.source.fullName, + unit.libraryElement.firstFragment.source.fullName, ); } if (lib is ResolvedLibraryResult) { final ResolvedLibraryResult resolvedLib = lib; - final PreviewPath previewPath = lib.element2.toPreviewPath(); + final PreviewPath previewPath = lib.element.toPreviewPath(); // This library has already been processed. if (updatedPreviews.containsKey(previewPath)) { continue; @@ -204,7 +204,7 @@ class PreviewDetector { final LibraryPreviewNode previewsForLibrary = _dependencyGraph.putIfAbsent( previewPath, - () => LibraryPreviewNode(library: resolvedLib.element2, logger: logger), + () => LibraryPreviewNode(library: resolvedLib.element, logger: logger), ); previewsForLibrary.updateDependencyGraph(graph: _dependencyGraph, units: lib.units); diff --git a/packages/flutter_tools/lib/src/widget_preview/utils.dart b/packages/flutter_tools/lib/src/widget_preview/utils.dart index e5dffeda24522..3a2144aeed06e 100644 --- a/packages/flutter_tools/lib/src/widget_preview/utils.dart +++ b/packages/flutter_tools/lib/src/widget_preview/utils.dart @@ -32,22 +32,22 @@ extension AnnotationExtension on Annotation { /// Convenience getter to identify `@Preview` annotations bool get isPreview => name.name == 'Preview' && - elementAnnotation!.element2?.library2!.uri == widgetPreviewsLibraryUri; + elementAnnotation!.element?.library!.uri == widgetPreviewsLibraryUri; bool get isMultiPreview { - final Element2? element = elementAnnotation!.element2; - if (element is ConstructorElement2) { - final InterfaceType type = element.enclosingElement2.supertype!; + final Element? element = elementAnnotation!.element; + if (element is ConstructorElement) { + final InterfaceType type = element.enclosingElement.supertype!; return type.getDisplayString() == 'MultiPreview' && - type.element3.library2.uri == widgetPreviewsLibraryUri; + type.element.library.uri == widgetPreviewsLibraryUri; } return false; } List findMultiPreviewPreviewNodes({required AnalysisContext context}) { final DartObject evaluatedAnnotation = elementAnnotation!.computeConstantValue()!; - final Element2 element = evaluatedAnnotation.type!.element3!; - if (element is ClassElement2) { + final Element element = evaluatedAnnotation.type!.element!; + if (element is ClassElement) { final InterfaceType type = element.supertype!; if (type.getDisplayString() != 'MultiPreview') { throw StateError('$element is not a MultiPreview!'); @@ -67,7 +67,7 @@ extension StringExtension on String { bool get doesContainDartTool => contains('.dart_tool'); } -extension LibraryElement2Extension on LibraryElement2 { +extension LibraryElementExtension on LibraryElement { /// Convenience method to package path and [uri] into a [PreviewPath] PreviewPath toPreviewPath() => (path: firstFragment.source.fullName, uri: uri); } diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 14485a315a258..95056f3aeb770 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: # # For detailed instructions, refer to: # https://github.com/flutter/flutter/blob/main/docs/infra/Updating-dependencies-in-Flutter.md + analyzer: 8.1.1 archive: 3.6.1 args: 2.7.0 dds: 5.0.3 @@ -23,13 +24,13 @@ dependencies: file: 7.0.1 flutter_template_images: 5.0.0 html: 0.15.6 - http: 1.4.0 + http: 1.5.0 intl: 0.20.2 meta: 1.17.0 multicast_dns: 0.3.3 mustache_template: 2.0.0 package_config: 2.2.0 - process: 5.0.4 + process: 5.0.5 fake_async: 1.3.3 stack_trace: 1.12.1 usage: 4.1.1 @@ -71,20 +72,19 @@ dependencies: standard_message_codec: 0.0.1+4 - dart_style: 3.1.0 + dart_style: 3.1.2 # The below dependencies are transitive and are here to pin them to a specific version. - _fe_analyzer_shared: 82.0.0 - analyzer: 7.4.5 + _fe_analyzer_shared: 88.0.0 boolean_selector: 2.1.2 browser_launcher: 1.1.3 built_collection: 5.1.1 - built_value: 8.11.0 + built_value: 8.11.1 cli_config: 0.2.0 clock: 1.1.2 csslib: 1.0.2 dap: 1.4.0 - dds_service_extensions: 2.0.2 + dds_service_extensions: 2.1.0 devtools_shared: 12.0.0 dtd: 4.0.0 extension_discovery: 2.1.0 @@ -126,4 +126,4 @@ dev_dependencies: dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: 932agm +# PUBSPEC CHECKSUM: f0tepf diff --git a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml index 59535fa756e7b..7950306d0b659 100644 --- a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml +++ b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: # These will be replaced with proper constraints after the template is hydrated. dtd: 2.5.1 flutter_lints: 6.0.0 - google_fonts: 6.2.1 + google_fonts: 6.3.0 stack_trace: 1.12.1 url_launcher: 6.3.2 @@ -27,7 +27,7 @@ dependencies: crypto: 3.0.6 fake_async: 1.3.3 file: 7.0.1 - http: 1.4.0 + http: 1.5.0 http_parser: 4.1.2 json_rpc_2: 3.0.3 leak_tracker: 11.0.1 @@ -46,7 +46,7 @@ dependencies: test_api: 0.7.7 typed_data: 1.4.0 unified_analytics: 8.0.1 - url_launcher_android: 6.3.16 + url_launcher_android: 6.3.17 url_launcher_ios: 6.3.3 url_launcher_linux: 3.2.1 url_launcher_macos: 3.2.2 @@ -62,4 +62,4 @@ dependencies: flutter: uses-material-design: true -# PUBSPEC CHECKSUM: patulg +# PUBSPEC CHECKSUM: b279i4 diff --git a/pubspec.lock b/pubspec.lock index bcecffdb707f4..30e00ac265cd5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: "direct main" description: name: _fe_analyzer_shared - sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f + sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a url: "https://pub.dev" source: hosted - version: "82.0.0" + version: "88.0.0" adaptive_breakpoints: dependency: "direct main" description: @@ -29,10 +29,10 @@ packages: dependency: "direct main" description: name: analyzer - sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" + sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" url: "https://pub.dev" source: hosted - version: "7.4.5" + version: "8.1.1" animations: dependency: "direct main" description: @@ -182,10 +182,10 @@ packages: dependency: "direct main" description: name: dart_style - sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" + sha256: c87dfe3d56f183ffe9106a18aebc6db431fc7c98c31a54b952a77f3d54a85697 url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" device_info: dependency: "direct main" description: @@ -310,10 +310,10 @@ packages: dependency: "direct main" description: name: google_fonts - sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + sha256: df9763500dadba0155373e9cb44e202ce21bd9ed5de6bdbd05c5854e86839cb8 url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.3.0" google_identity_services_web: dependency: "direct main" description: @@ -366,10 +366,10 @@ packages: dependency: "direct main" description: name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" http_multi_server: dependency: "direct main" description: @@ -638,10 +638,10 @@ packages: dependency: "direct main" description: name: process - sha256: "44b4226c0afd4bc3b7c7e67d44c4801abd97103cf0c84609e2654b664ca2798c" + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 url: "https://pub.dev" source: hosted - version: "5.0.4" + version: "5.0.5" process_runner: dependency: "direct main" description: @@ -867,10 +867,10 @@ packages: dependency: "direct main" description: name: url_launcher_android - sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656" url: "https://pub.dev" source: hosted - version: "6.3.16" + version: "6.3.17" url_launcher_ios: dependency: "direct main" description: @@ -1051,18 +1051,18 @@ packages: dependency: "direct main" description: name: webview_flutter_platform_interface - sha256: f0dc2dc3a2b1e3a6abdd6801b9355ebfeb3b8f6cde6b9dc7c9235909c4a1f147 + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" url: "https://pub.dev" source: hosted - version: "2.13.1" + version: "2.14.0" webview_flutter_wkwebview: dependency: "direct main" description: name: webview_flutter_wkwebview - sha256: "71523b9048cf510cfa1fd4e0a3fa5e476a66e0884d5df51d59d5023dba237107" + sha256: fb46db8216131a3e55bcf44040ca808423539bc6732e7ed34fb6d8044e3d512f url: "https://pub.dev" source: hosted - version: "3.22.1" + version: "3.23.0" xdg_directories: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 79a9e8e9c85f5..24931b65b5611 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,9 +78,9 @@ workspace: dependencies: _discoveryapis_commons: 1.0.7 - _fe_analyzer_shared: 82.0.0 + _fe_analyzer_shared: 88.0.0 adaptive_breakpoints: 0.1.7 - analyzer: 7.4.5 + analyzer: 8.1.1 animations: 2.0.11 archive: 3.6.1 args: 2.7.0 @@ -109,14 +109,14 @@ dependencies: frontend_server_client: 4.0.0 gcloud: 0.8.19 glob: 2.1.3 - google_fonts: 6.2.1 + google_fonts: 6.3.0 google_identity_services_web: 0.3.3+1 google_mobile_ads: 5.1.0 googleapis: 12.0.0 googleapis_auth: 1.6.0 hooks: 0.19.5 html: 0.15.6 - http: 1.4.0 + http: 1.5.0 http_multi_server: 3.2.2 http_parser: 4.1.2 intl: 0.20.2 @@ -148,7 +148,7 @@ dependencies: platform: 3.1.6 plugin_platform_interface: 2.1.8 pool: 1.5.1 - process: 5.0.4 + process: 5.0.5 process_runner: 4.2.0 provider: 6.1.5 pub_semver: 2.2.0 @@ -175,7 +175,7 @@ dependencies: test_core: 0.6.12 typed_data: 1.4.0 url_launcher: 6.3.2 - url_launcher_android: 6.3.16 + url_launcher_android: 6.3.17 url_launcher_ios: 6.3.3 url_launcher_linux: 3.2.1 url_launcher_macos: 3.2.2 @@ -198,13 +198,13 @@ dependencies: webkit_inspection_protocol: 1.2.1 webview_flutter: 4.9.0 webview_flutter_android: 3.16.9 - webview_flutter_platform_interface: 2.13.1 - webview_flutter_wkwebview: 3.22.1 + webview_flutter_platform_interface: 2.14.0 + webview_flutter_wkwebview: 3.23.0 xdg_directories: 1.1.0 xml: 6.5.0 yaml: 3.1.3 cli_util: 0.4.2 - dart_style: 3.1.0 + dart_style: 3.1.2 ffigen: 18.1.0 file_testing: 3.0.2 flutter_lints: 6.0.0 @@ -212,4 +212,4 @@ dependencies: pedantic: 1.11.1 quiver: 3.2.2 yaml_edit: 2.2.2 -# PUBSPEC CHECKSUM: h41ipd +# PUBSPEC CHECKSUM: jbcfui From eeb3a40cbef8b87d5d5ad4a4f25c9c1e1b784fad Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Fri, 15 Aug 2025 16:30:08 -0700 Subject: [PATCH 086/720] Revert "[ios][tools]do not log "bonjour not found" at all (unless verbose)" (#173879) Reverts flutter/flutter#173569 Sorry I have to revert this one as it didn't work. # Why didn't work From the comment, it looks like `echoError` is only used for verbose mode, and `streamOutput` is used for non-verbose mode (I am still looking into how this is setup) https://github.com/flutter/flutter/blob/e4f27cd09734db6c6ed94e104ab5333c48dfc544/packages/flutter_tools/bin/xcode_backend.dart#L151 # How to fix ## Possible Fix 1 I tested this works, by changing this line: https://github.com/flutter/flutter/blob/master/packages/flutter_tools/bin/xcode_backend.dart#L159 into this: ``` if (!verbose && exitCode == 0 && !skipErrorLog) ``` ## Possible fix 2: Surprisingly this also works: ``` if (!verbose && exitCode == 0) { streamOutput(errorOutput.string().replaceAll('error', ''); } ``` Although we made sure `errorOutput` doesn't start with `error:`, there's an `error:` in the middle of the string. Possibly because of this code in `mac.dart`: https://github.com/flutter/flutter/blob/e4f27cd09734db6c6ed94e104ab5333c48dfc544/packages/flutter_tools/lib/src/ios/mac.dart#L431-L433 # Why we missed it? I tested my initial code code and assumed updated code would work, because I incorrectly thought `echoError` was the one triggered the output in non-verbose mode. I should have verified it. --- packages/flutter_tools/bin/xcode_backend.dart | 57 ++-- .../general.shard/xcode_backend_test.dart | 252 ++++-------------- .../integration.shard/xcode_backend_test.dart | 70 ++--- 3 files changed, 95 insertions(+), 284 deletions(-) diff --git a/packages/flutter_tools/bin/xcode_backend.dart b/packages/flutter_tools/bin/xcode_backend.dart index 74cd92c7f1d83..857fa90de2f24 100644 --- a/packages/flutter_tools/bin/xcode_backend.dart +++ b/packages/flutter_tools/bin/xcode_backend.dart @@ -102,14 +102,7 @@ class Context { Directory directoryFromPath(String path) => Directory(path); - /// Run given command ([bin]) in a synchronous subprocess. - /// - /// If [allowFail] is true, an exception will not be thrown even if the process returns a - /// non-zero exit code. Also, `error:` will not be prefixed to the output to prevent Xcode - /// complication failures. - /// - /// If [skipErrorLog] is true, `stderr` from the process will not be output unless in [verbose] - /// mode. If in [verbose], pipes `stderr` to `stdout`. + /// Run given command in a synchronous subprocess. /// /// Will throw [Exception] if the exit code is not 0. ProcessResult runSync( @@ -117,7 +110,6 @@ class Context { List args, { bool verbose = false, bool allowFail = false, - bool skipErrorLog = false, String? workingDirectory, }) { if (verbose) { @@ -130,22 +122,15 @@ class Context { final String resultStderr = result.stderr.toString().trim(); if (resultStderr.isNotEmpty) { final errorOutput = StringBuffer(); + // If allowFail, do not fail Xcode build. An example is on macOS 26, + // plutil reports NSBonjourServices key not found via stderr (rather than + // stdout on older macOS), and it should not cause compile failure. if (!allowFail && result.exitCode != 0) { // "error:" prefix makes this show up as an Xcode compilation error. errorOutput.write('error: '); } errorOutput.write(resultStderr); - if (skipErrorLog) { - // Pipe stderr to stdout under verbose mode. - // An example is on macOS 26, plutil reports NSBonjourServices key not found - // via stderr (rather than stdout on older macOS), and logging the message - // in stderr would be confusing, since not having the key is one of the expected states. - if (verbose) { - echo(errorOutput.toString()); - } - } else { - echoError(errorOutput.toString()); - } + echoError(errorOutput.toString()); // Stream stderr to the Flutter build process. // When in verbose mode, `echoError` above will show the logs. So only @@ -439,17 +424,16 @@ class Context { return; } - final bool verbose = (environment['VERBOSE_SCRIPT_LOGGING'] ?? '').isNotEmpty; - // If there are already NSBonjourServices specified by the app (uncommon), // insert the vmService service name to the existing list. - ProcessResult result = runSync( - 'plutil', - ['-extract', 'NSBonjourServices', 'xml1', '-o', '-', builtProductsPlist], - verbose: verbose, - allowFail: true, - skipErrorLog: true, - ); + ProcessResult result = runSync('plutil', [ + '-extract', + 'NSBonjourServices', + 'xml1', + '-o', + '-', + builtProductsPlist, + ], allowFail: true); if (result.exitCode == 0) { runSync('plutil', [ '-insert', @@ -474,13 +458,14 @@ class Context { // specified (uncommon). This text will appear below the "Your app would // like to find and connect to devices on your local network" permissions // popup. - result = runSync( - 'plutil', - ['-extract', 'NSLocalNetworkUsageDescription', 'xml1', '-o', '-', builtProductsPlist], - verbose: verbose, - allowFail: true, - skipErrorLog: true, - ); + result = runSync('plutil', [ + '-extract', + 'NSLocalNetworkUsageDescription', + 'xml1', + '-o', + '-', + builtProductsPlist, + ], allowFail: true); if (result.exitCode != 0) { runSync('plutil', [ '-insert', diff --git a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart index aeddbf5839c44..4548261e6071d 100644 --- a/packages/flutter_tools/test/general.shard/xcode_backend_test.dart +++ b/packages/flutter_tools/test/general.shard/xcode_backend_test.dart @@ -390,211 +390,70 @@ void main() { ); }); - test( - 'Missing NSBonjourServices key in Info.plist should not fail Xcode compilation, and no plutil error in stdout without verbose mode', - () { - final Directory buildDir = fileSystem.directory('/path/to/builds') - ..createSync(recursive: true); - final File infoPlist = buildDir.childFile('Info.plist')..createSync(); - const plutilErrorMessage = - 'Could not extract value, error: No value at that key path or invalid key path: NSBonjourServices'; - - final context = TestContext( - ['test_vm_service_bonjour_service'], - { - 'CONFIGURATION': 'Debug', - 'BUILT_PRODUCTS_DIR': buildDir.path, - 'INFOPLIST_PATH': 'Info.plist', - }, - commands: [ - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSBonjourServices', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - exitCode: 1, - stderr: plutilErrorMessage, - ), - FakeCommand( - command: [ - 'plutil', - '-insert', - 'NSBonjourServices', - '-json', - '["_dartVmService._tcp"]', - infoPlist.path, - ], - ), - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSLocalNetworkUsageDescription', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - ), - ], - fileSystem: fileSystem, - )..run(); - expect(context.stderr, isNot(startsWith('error: '))); - expect(context.stderr, isNot(contains(plutilErrorMessage))); - expect(context.stdout, isNot(contains(plutilErrorMessage))); - }, - ); - - test( - 'Missing NSBonjourServices key in Info.plist should not fail Xcode compilation, and has plutil error in stdout under verbose mode', - () { - final Directory buildDir = fileSystem.directory('/path/to/builds') - ..createSync(recursive: true); - final File infoPlist = buildDir.childFile('Info.plist')..createSync(); - const plutilErrorMessage = - 'Could not extract value, error: No value at that key path or invalid key path: NSBonjourServices'; - - final context = TestContext( - ['test_vm_service_bonjour_service'], - { - 'CONFIGURATION': 'Debug', - 'BUILT_PRODUCTS_DIR': buildDir.path, - 'INFOPLIST_PATH': 'Info.plist', - 'VERBOSE_SCRIPT_LOGGING': 'YES', - }, - commands: [ - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSBonjourServices', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - exitCode: 1, - stderr: plutilErrorMessage, - ), - FakeCommand( - command: [ - 'plutil', - '-insert', - 'NSBonjourServices', - '-json', - '["_dartVmService._tcp"]', - infoPlist.path, - ], - ), - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSLocalNetworkUsageDescription', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - ), - ], - fileSystem: fileSystem, - )..run(); - expect(context.stderr, isNot(startsWith('error: '))); - expect(context.stderr, isNot(contains(plutilErrorMessage))); - expect(context.stdout, contains(plutilErrorMessage)); - }, - ); - - test( - 'Missing NSLocalNetworkUsageDescription in Info.plist should not fail Xcode compilation, and no plutil error in stdout without verbose mode', - () { - final Directory buildDir = fileSystem.directory('/path/to/builds') - ..createSync(recursive: true); - final File infoPlist = buildDir.childFile('Info.plist')..createSync(); - const plutilErrorMessage = - 'Could not extract value, error: No value at that key path or invalid key path: NSLocalNetworkUsageDescription'; - final context = TestContext( - ['test_vm_service_bonjour_service'], - { - 'CONFIGURATION': 'Debug', - 'BUILT_PRODUCTS_DIR': buildDir.path, - 'INFOPLIST_PATH': 'Info.plist', - }, - commands: [ - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSBonjourServices', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - ), - FakeCommand( - command: [ - 'plutil', - '-insert', - 'NSBonjourServices.0', - '-string', - '_dartVmService._tcp', - infoPlist.path, - ], - ), - FakeCommand( - command: [ - 'plutil', - '-extract', - 'NSLocalNetworkUsageDescription', - 'xml1', - '-o', - '-', - infoPlist.path, - ], - exitCode: 1, - stderr: plutilErrorMessage, - ), - FakeCommand( - command: [ - 'plutil', - '-insert', - 'NSLocalNetworkUsageDescription', - '-string', - 'Allow Flutter tools on your computer to connect and debug your application. This prompt will not appear on release builds.', - infoPlist.path, - ], - ), - ], - fileSystem: fileSystem, - )..run(); - expect(context.stderr, isNot(startsWith('error: '))); - expect(context.stderr, isNot(contains(plutilErrorMessage))); - expect(context.stdout, isNot(contains(plutilErrorMessage))); - }, - ); + test('Missing NSBonjourServices key in Info.plist should not fail Xcode compilation', () { + final Directory buildDir = fileSystem.directory('/path/to/builds') + ..createSync(recursive: true); + final File infoPlist = buildDir.childFile('Info.plist')..createSync(); + final context = TestContext( + ['test_vm_service_bonjour_service'], + { + 'CONFIGURATION': 'Debug', + 'BUILT_PRODUCTS_DIR': buildDir.path, + 'INFOPLIST_PATH': 'Info.plist', + }, + commands: [ + FakeCommand( + command: [ + 'plutil', + '-extract', + 'NSBonjourServices', + 'xml1', + '-o', + '-', + infoPlist.path, + ], + exitCode: 1, + stderr: 'No value at that key path or invalid key path: NSBonjourServices', + ), + FakeCommand( + command: [ + 'plutil', + '-insert', + 'NSBonjourServices', + '-json', + '["_dartVmService._tcp"]', + infoPlist.path, + ], + ), + FakeCommand( + command: [ + 'plutil', + '-extract', + 'NSLocalNetworkUsageDescription', + 'xml1', + '-o', + '-', + infoPlist.path, + ], + ), + ], + fileSystem: fileSystem, + )..run(); + expect(context.stderr, isNot(contains('error: '))); + }); test( - 'Missing NSLocalNetworkUsageDescription in Info.plist should not fail Xcode compilation, and has plutil error in stdout under verbose mode', + 'Missing NSLocalNetworkUsageDescription in Info.plist should not fail Xcode compilation', () { final Directory buildDir = fileSystem.directory('/path/to/builds') ..createSync(recursive: true); final File infoPlist = buildDir.childFile('Info.plist')..createSync(); - const plutilErrorMessage = - 'Could not extract value, error: No value at that key path or invalid key path: NSLocalNetworkUsageDescription'; final context = TestContext( ['test_vm_service_bonjour_service'], { 'CONFIGURATION': 'Debug', 'BUILT_PRODUCTS_DIR': buildDir.path, 'INFOPLIST_PATH': 'Info.plist', - 'VERBOSE_SCRIPT_LOGGING': 'YES', }, commands: [ FakeCommand( @@ -629,7 +488,8 @@ void main() { infoPlist.path, ], exitCode: 1, - stderr: plutilErrorMessage, + stderr: + 'No value at that key path or invalid key path: NSLocalNetworkUsageDescription', ), FakeCommand( command: [ @@ -644,9 +504,7 @@ void main() { ], fileSystem: fileSystem, )..run(); - expect(context.stderr, isNot(startsWith('error: '))); - expect(context.stderr, isNot(contains(plutilErrorMessage))); - expect(context.stdout, contains(plutilErrorMessage)); + expect(context.stderr, isNot(contains('error: '))); }, ); }); diff --git a/packages/flutter_tools/test/integration.shard/xcode_backend_test.dart b/packages/flutter_tools/test/integration.shard/xcode_backend_test.dart index 48b31b0370b60..55afb30b11f59 100644 --- a/packages/flutter_tools/test/integration.shard/xcode_backend_test.dart +++ b/packages/flutter_tools/test/integration.shard/xcode_backend_test.dart @@ -132,7 +132,7 @@ void main() { }); for (final buildConfiguration in ['Debug', 'Profile']) { - test('add keys in $buildConfiguration without verbose mode', () async { + test('add keys in $buildConfiguration', () async { infoPlist.writeAsStringSync(emptyPlist); final ProcessResult result = await Process.run( @@ -151,43 +151,14 @@ void main() { expect(actualInfoPlist, contains('NSLocalNetworkUsageDescription')); expect(result.stderr, isNot(startsWith('error:'))); - const plutilErrorMessage = - 'Could not extract value, error: No value at that key path or invalid key path: NSBonjourServices'; - expect(result.stderr, isNot(contains(plutilErrorMessage))); - expect(result.stdout, isNot(contains(plutilErrorMessage))); - expect(result, const ProcessResultMatcher()); - }); - - test('add keys in $buildConfiguration under verbose mode', () async { - infoPlist.writeAsStringSync(emptyPlist); - - final ProcessResult result = await Process.run( - xcodeBackendPath, - ['test_vm_service_bonjour_service'], - environment: { - 'CONFIGURATION': buildConfiguration, - 'BUILT_PRODUCTS_DIR': buildDirectory.path, - 'INFOPLIST_PATH': 'Info.plist', - 'VERBOSE_SCRIPT_LOGGING': 'YES', - }, - ); - - final String actualInfoPlist = infoPlist.readAsStringSync(); - expect(actualInfoPlist, contains('NSBonjourServices')); - expect(actualInfoPlist, contains('dartVmService')); - expect(actualInfoPlist, contains('NSLocalNetworkUsageDescription')); - - expect(result.stderr, isNot(startsWith('error:'))); - const plutilErrorMessage = - 'Could not extract value, error: No value at that key path or invalid key path: NSBonjourServices'; - expect(result.stderr, isNot(contains(plutilErrorMessage))); - expect(result.stdout, contains(plutilErrorMessage)); expect(result, const ProcessResultMatcher()); }); } - test('adds to existing Bonjour services, does not override network usage description', () async { - infoPlist.writeAsStringSync(''' + test( + 'adds to existing Bonjour services, does not override network usage description', + () async { + infoPlist.writeAsStringSync(''' @@ -201,17 +172,17 @@ void main() { '''); - final ProcessResult result = await Process.run( - xcodeBackendPath, - ['test_vm_service_bonjour_service'], - environment: { - 'CONFIGURATION': 'Debug', - 'BUILT_PRODUCTS_DIR': buildDirectory.path, - 'INFOPLIST_PATH': 'Info.plist', - }, - ); + final ProcessResult result = await Process.run( + xcodeBackendPath, + ['test_vm_service_bonjour_service'], + environment: { + 'CONFIGURATION': 'Debug', + 'BUILT_PRODUCTS_DIR': buildDirectory.path, + 'INFOPLIST_PATH': 'Info.plist', + }, + ); - expect(infoPlist.readAsStringSync(), ''' + expect(infoPlist.readAsStringSync(), ''' @@ -227,13 +198,10 @@ void main() { '''); - const plutilErrorMessage = - 'Could not extract value, error: No value at that key path or invalid key path: NSLocalNetworkUsageDescription'; - expect(result.stderr, isNot(startsWith('error:'))); - expect(result.stderr, isNot(contains(plutilErrorMessage))); - expect(result.stdout, isNot(contains(plutilErrorMessage))); - expect(result, const ProcessResultMatcher()); - }); + expect(result.stderr, isNot(startsWith('error:'))); + expect(result, const ProcessResultMatcher()); + }, + ); test('does not add bonjour settings when port publication is disabled', () async { infoPlist.writeAsStringSync(''' From 4abea340e19db0c8cf2df804104335a503e37ec0 Mon Sep 17 00:00:00 2001 From: gaaclarke <30870216+gaaclarke@users.noreply.github.com> Date: Fri, 15 Aug 2025 16:42:17 -0700 Subject: [PATCH 087/720] Blocks exynos9820 chip from vulkan (#173807) fixes https://github.com/flutter/flutter/issues/171992 ## 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 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]. **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 [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 --- .../android_context_dynamic_impeller.cc | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/engine/src/flutter/shell/platform/android/android_context_dynamic_impeller.cc b/engine/src/flutter/shell/platform/android/android_context_dynamic_impeller.cc index 6af29933862d3..8dcba69df4966 100644 --- a/engine/src/flutter/shell/platform/android/android_context_dynamic_impeller.cc +++ b/engine/src/flutter/shell/platform/android/android_context_dynamic_impeller.cc @@ -17,21 +17,25 @@ namespace { static const constexpr char* kAndroidHuawei = "android-huawei"; -/// These are SoCs that crash when using AHB imports. -static constexpr const char* kBLC[] = { - // Most Exynos Series SoC +static constexpr const char* kBadSocs[] = { + // Most Exynos Series SoC. These are SoCs that crash when using AHB imports. "exynos7870", // "exynos7880", // "exynos7872", // "exynos7884", // "exynos7885", // + "exynos7904", // + // Mongoose line. "exynos8890", // "exynos8895", // - "exynos7904", // "exynos9609", // "exynos9610", // "exynos9611", // - "exynos9810" // + "exynos9810", // + // `exynos9820` and `exynos9825` have graphical errors: + // https://github.com/flutter/flutter/issues/171992. + "exynos9820", // + "exynos9825" // }; static bool IsDeviceEmulator(std::string_view product_model) { @@ -41,7 +45,7 @@ static bool IsDeviceEmulator(std::string_view product_model) { static bool IsKnownBadSOC(std::string_view hardware) { // TODO(jonahwilliams): if the list gets too long (> 16), convert // to a hash map first. - for (const auto& board : kBLC) { + for (const auto& board : kBadSocs) { if (strcmp(board, hardware.data()) == 0) { return true; } @@ -83,7 +87,8 @@ GetActualRenderingAPIForImpeller( __system_property_get("ro.product.board", product_model); if (IsKnownBadSOC(product_model)) { - // Avoid using Vulkan on known bad SoCs. + FML_LOG(INFO) + << "Known bad Vulkan driver encountered, falling back to OpenGLES."; return nullptr; } From 515e753c6e5575be1a882bd0be6c13a0daa2c006 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 15 Aug 2025 21:03:27 -0400 Subject: [PATCH 088/720] Roll Dart SDK from 9277d6303da5 to 67ca79475db6 (1 revision) (#173886) https://dart.googlesource.com/sdk.git/+log/9277d6303da5..67ca79475db6 2025-08-15 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-104.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 chinmaygarde@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 03641f94537a8..3b49450804c2a 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '9277d6303da514dcbae6f7f6d8a51611f3b944aa', + 'dart_revision': '67ca79475db612356bf11cbca5effa2ebebd1f75', # 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 5c8c174f3b5cd71c9b8d7543b07b5baa1639b468 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 15 Aug 2025 22:03:40 -0400 Subject: [PATCH 089/720] Roll Skia from 91ad1f21ca61 to 1e148cada9d4 (3 revisions) (#173890) https://skia.googlesource.com/skia.git/+log/91ad1f21ca61..1e148cada9d4 2025-08-15 bungeman@google.com [pdf] Unpremul after interpolating unpremul 2025-08-15 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 2d05384af7d2 to e04b90257b5a (9 revisions) 2025-08-15 mike@reedtribe.org Last users of (unneeded) RawIter 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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 3b49450804c2a..06a58a164c42a 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '91ad1f21ca61960b74a6a13ffa4d5ddce463a6e4', + 'skia_revision': '1e148cada9d42dcc841322d78a2eee1c8a6388a7', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 36e1db8f379a7d2c1cbf581b375f7b54b53b661c Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 16 Aug 2025 08:48:17 -0400 Subject: [PATCH 090/720] Roll Skia from 1e148cada9d4 to 16dbd908dcab (1 revision) (#173901) https://skia.googlesource.com/skia.git/+log/1e148cada9d4..16dbd908dcab 2025-08-16 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from e04b90257b5a to 57d74122fab7 (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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 06a58a164c42a..e67ec744b8037 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '1e148cada9d42dcc841322d78a2eee1c8a6388a7', + 'skia_revision': '16dbd908dcab1abbce92155ac7807444ec5acddb', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 0a2906b81d5ef21b47fe379ca610b65a45a246c2 Mon Sep 17 00:00:00 2001 From: Alex Talebi <31685655+SalehTZ@users.noreply.github.com> Date: Sat, 16 Aug 2025 17:38:37 +0330 Subject: [PATCH 091/720] Improve `SweepGradient` angle and `TileMode` documentation (#172406) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Improve SweepGradient and TileMode Documentation ## Description This PR enhances the documentation for `SweepGradient` and `TileMode` to provide clearer guidance on how angles are measured and how tile modes affect sweep gradient rendering. ### Changes 1. **SweepGradient Documentation**: - Clarified angle measurement in radians from the positive x-axis - Documented angle normalization behavior for values outside [0, 2π] - Added detailed explanations of how each `TileMode` affects rendering outside the angular sector 2. **Gradient.sweep Constructor**: - Improved parameter documentation - Added a practical example showing how to create a 90-degree sweep gradient - Clarified the relationship between color stops and angles 3. **TileMode Documentation**: - Added sweep gradient-specific behavior to each `TileMode` variant - Clarified how each mode (clamp, repeated, mirror, decal) affects rendering outside the angular sector - Improved overall documentation structure for gradient edge behavior ## Related Issues Fixes #166206 ## Testing - Verified documentation changes by reviewing the generated API docs - Ensured all examples compile and render as expected ## Breaking Changes None - this is purely a documentation improvement. ## Additional Notes The changes make it much clearer how `startAngle` and `endAngle` interact with different `TileMode` values, which was a source of confusion in the original issue. ## 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 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 [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 --- engine/src/flutter/lib/ui/painting.dart | 94 ++++++++++++++----- .../flutter/lib/src/painting/gradient.dart | 29 +++++- 2 files changed, 95 insertions(+), 28 deletions(-) diff --git a/engine/src/flutter/lib/ui/painting.dart b/engine/src/flutter/lib/ui/painting.dart index 8df3cf912d722..fd49ce1625ac3 100644 --- a/engine/src/flutter/lib/ui/painting.dart +++ b/engine/src/flutter/lib/ui/painting.dart @@ -4646,23 +4646,32 @@ base class Shader extends NativeFieldWrapperClass1 { } } -/// Defines what happens at the edge of a gradient or the sampling of a source image -/// in an [ImageFilter]. +/// Defines how to handle areas outside the defined bounds of a gradient or image filter. /// -/// A gradient is defined along a finite inner area. In the case of a linear -/// gradient, it's between the parallel lines that are orthogonal to the line -/// drawn between two points. In the case of radial gradients, it's the disc -/// that covers the circle centered on a particular point up to a given radius. +/// ## For Gradients /// -/// An image filter reads source samples from a source image and performs operations -/// on those samples to produce a result image. An image defines color samples only -/// for pixels within the bounds of the image but some filter operations, such as a blur -/// filter, read samples over a wide area to compute the output for a given pixel. Such -/// a filter would need to combine samples from inside the image with hypothetical -/// color values from outside the image. +/// Gradients are defined with some specific bounds creating an inner area and an outer area, and `TileMode` controls how colors +/// are determined for areas outside these bounds: /// -/// This enum is used to define how the gradient or image filter should treat the regions -/// outside that defined inner area. +/// - **Linear gradients**: The inner area is the area between two points +/// (typically referred to as `start` and `end` in the gradient API), or more precisely, +/// it's the area between the parallel lines that are orthogonal to the line drawn between the two points. +/// Colors outside this area are determined by the `TileMode`. +/// +/// - **Radial gradients**: The inner area is the disc defined by a center and radius. +/// Colors outside this disc are determined by the `TileMode`. +/// +/// - **Sweep gradients**: The inner area is the angular sector between `startAngle` +/// and `endAngle`. Colors outside this sector are determined by the `TileMode`. +/// +/// ## For Image Filters +/// +/// When applying filters (like blur) that sample colors from outside an image's bounds, +/// `TileMode` defines how those out-of-bounds samples are determined: +/// +/// - It controls what color values are used when the filter needs to sample +/// from areas outside the original image. +/// - This is particularly important for effects like blurring near image edges. /// /// See also: /// @@ -4680,8 +4689,12 @@ base class Shader extends NativeFieldWrapperClass1 { enum TileMode { /// Samples beyond the edge are clamped to the nearest color in the defined inner area. /// - /// A gradient will paint all the regions outside the inner area with the - /// color at the end of the color stop list closest to that region. + /// For gradients, this means the region outside the inner area is painted with + /// the color at the end of the color stop list closest to that region. + /// + /// For sweep gradients specifically, the entire area outside the angular sector + /// defined by [startAngle] and [endAngle] will be painted with the color at the + /// end of the color stop list closest to that region. /// /// An image filter will substitute the nearest edge pixel for any samples taken from /// outside its source image. @@ -4697,6 +4710,9 @@ enum TileMode { /// repeated from 1.0 to 2.0, 2.0 to 3.0, and so forth (and for linear gradients, similarly /// from -1.0 to 0.0, -2.0 to -1.0, etc). /// + /// For sweep gradients, the gradient pattern is repeated in the same direction + /// (clockwise) for angles beyond [endAngle] and before [startAngle]. + /// /// An image filter will treat its source image as if it were tiled across the enlarged /// sample space from which it reads, each tile in the same orientation as the base image. /// @@ -4712,6 +4728,9 @@ enum TileMode { /// again from 4.0 to 3.0, and so forth (and for linear gradients, similarly in the /// negative direction). /// + /// For sweep gradients, the gradient pattern is mirrored back and forth as the angle + /// increases beyond [endAngle] or decreases below [startAngle]. + /// /// An image filter will treat its source image as tiled in an alternating forwards and /// backwards or upwards and downwards direction across the sample space from which /// it is reading. @@ -4724,8 +4743,11 @@ enum TileMode { /// Samples beyond the edge are treated as transparent black. /// /// A gradient will render transparency over any region that is outside the circle of a - /// radial gradient or outside the parallel lines that define the inner area of a linear - /// gradient. + /// radial gradient, outside the parallel lines that define the inner area of a linear + /// gradient, or outside the angular sector of a sweep gradient. + /// + /// For sweep gradients, only the sector between [startAngle] and [endAngle] will be + /// painted; all other areas will be transparent. /// /// An image filter will substitute transparent black for any sample it must read from /// outside its source image. @@ -4933,17 +4955,39 @@ base class Gradient extends Shader { /// positive angles going clockwise around the `center`. /// /// If `colorStops` is provided, `colorStops[i]` is a number from 0.0 to 1.0 - /// that specifies where `color[i]` begins in the gradient. If `colorStops` is - /// not provided, then only two stops, at 0.0 and 1.0, are implied (and - /// `color` must therefore only have two entries). Stop values less than 0.0 - /// will be rounded up to 0.0 and stop values greater than 1.0 will be rounded - /// down to 1.0. Each stop value must be greater than or equal to the previous - /// stop value. Stop values that do not meet this criteria will be rounded up - /// to the previous stop value. + /// that specifies where `colors[i]` begins in the gradient. If `colorStops` is + /// not provided, then only two stops, at 0.0 and 1.0, are implied + /// (and `colors` must therefore only have two entries). Stop values less than + /// 0.0 will be rounded up to 0.0 and stop values greater than 1.0 will be + /// rounded down to 1.0. Each stop value must be greater than or equal to the + /// previous stop value. Stop values that do not meet this criteria will be + /// rounded up to the previous stop value. + /// + /// The `startAngle` and `endAngle` parameters define the angular sector to be + /// painted. Angles are measured in radians clockwise from the positive x-axis. + /// Values outside the range `[0, 2π]` are normalized to this range using modulo + /// arithmetic. The gradient is only painted in the sector between `startAngle` + /// and `endAngle`. The `tileMode` determines how the gradient behaves outside + /// this sector. + /// + /// The `tileMode` argument specifies how the gradient should handle areas + /// outside the angular sector defined by `startAngle` and `endAngle`: /// /// The behavior before `startAngle` and after `endAngle` is described by the /// `tileMode` argument. For details, see the [TileMode] enum. /// + /// * [TileMode.clamp]: The edge colors are extended to infinity. + /// * [TileMode.mirror]: The gradient is repeated, alternating direction each time. + /// * [TileMode.repeated]: The gradient is repeated in the same direction. + /// * [TileMode.decal]: Only the colors within the gradient's angular sector are + /// drawn, with transparent black elsewhere. + /// + /// The [colorStops] argument must have the same number of values as [colors], + /// if specified. It specifies the position of each color stop between 0.0 and + /// 1.0. If it is null, a uniform distribution is assumed. The stop values must + /// be in ascending order. A stop value of 0.0 corresponds to [startAngle], and + /// a stop value of 1.0 corresponds to [endAngle]. + /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_sweep.png) /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_sweep.png) /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_sweep.png) diff --git a/packages/flutter/lib/src/painting/gradient.dart b/packages/flutter/lib/src/painting/gradient.dart index 3302a2043a496..dcc652735849c 100644 --- a/packages/flutter/lib/src/painting/gradient.dart +++ b/packages/flutter/lib/src/painting/gradient.dart @@ -984,18 +984,41 @@ class SweepGradient extends Gradient { /// The angle in radians at which stop 0.0 of the gradient is placed. /// + /// The angle is measured in radians clockwise from the positive x-axis. + /// + /// Values outside the range `[0, 2π]` are normalized to the equivalent angle + /// within this range using modulo arithmetic. + /// + /// The gradient will be painted in the sector between [startAngle] and [endAngle]. + /// The behavior outside this sector is determined by [tileMode]. + /// /// Defaults to 0.0. final double startAngle; /// The angle in radians at which stop 1.0 of the gradient is placed. /// - /// Defaults to math.pi * 2. + /// The angle is measured in radians clockwise from the positive x-axis. + /// + /// Values outside the range `[0, 2π]` are normalized to the equivalent angle + /// within this range using modulo arithmetic. + /// + /// The gradient will be painted in the sector between [startAngle] and [endAngle]. + /// The behavior outside this sector is determined by [tileMode]. + /// + /// Defaults to math.pi * 2 (2π = a full circle). final double endAngle; - /// How this gradient should tile the plane beyond in the region before + /// How this gradient should tile the plane in the region before /// [startAngle] and after [endAngle]. /// - /// For details, see [TileMode]. + /// The gradient will be painted in the sector between [startAngle] and + /// [endAngle]. The [tileMode] determines what happens in the remaining area: + /// + /// * [TileMode.clamp]: The edge colors are extended to fill the remaining area. + /// * [TileMode.repeated]: The gradient is repeated in the angular direction. + /// * [TileMode.mirror]: The gradient is mirrored in the angular direction. + /// * [TileMode.decal]: Only the gradient is drawn, leaving the remaining area + /// transparent. /// /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_sweep.png) /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_sweep.png) From 880572e1579ac5835d68ed2fe09fa339a959d601 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 16 Aug 2025 14:56:38 -0400 Subject: [PATCH 092/720] Roll Skia from 16dbd908dcab to d445371afee5 (1 revision) (#173907) https://skia.googlesource.com/skia.git/+log/16dbd908dcab..d445371afee5 2025-08-16 robertphillips@google.com Revert "Factor out common pathiter" 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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 e67ec744b8037..f231594d0be6f 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '16dbd908dcab1abbce92155ac7807444ec5acddb', + 'skia_revision': 'd445371afee5ce3c307f416f8197c654831fa060', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 8b04c41e971dc1d6366bf023db86bc58cba5b1ea Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 16 Aug 2025 18:41:24 -0400 Subject: [PATCH 093/720] Roll Skia from d445371afee5 to a30d857718ad (2 revisions) (#173910) https://skia.googlesource.com/skia.git/+log/d445371afee5..a30d857718ad 2025-08-16 skia-autoroll@skia-public.iam.gserviceaccount.com Manual roll ANGLE from 63d8f74cdf9c to 806c80ece32b (6 revisions) 2025-08-16 skia-autoroll@skia-public.iam.gserviceaccount.com Manual roll Dawn from 8f680a2a9cb9 to b639f97ad620 (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 chinmaygarde@google.com,kjlubick@google.com,robertphillips@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 f231594d0be6f..d2bed9e034ee0 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': 'd445371afee5ce3c307f416f8197c654831fa060', + 'skia_revision': 'a30d857718ad1e8c98e3e63c921ae5cc8a7598cf', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 76ce984d9b8839b280ee6dd50cf4f35e2b1241c4 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sat, 16 Aug 2025 21:19:55 -0400 Subject: [PATCH 094/720] Roll Fuchsia Linux SDK from H1kVA85LyQsK8EDp2... to sLmXksm1vZl2VEeA7... (#173913) 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 chinmaygarde@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 d2bed9e034ee0..11445f5fddbea 100644 --- a/DEPS +++ b/DEPS @@ -810,7 +810,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'H1kVA85LyQsK8EDp2cRaAtMd5_KgvnJZ43obPfdZuLAC' + 'version': 'sLmXksm1vZl2VEeA7RO0ugVVEf-76QkCgnxUwE_1T64C' } ], 'condition': 'download_fuchsia_deps and not download_fuchsia_sdk', From bfede21c2e1c1701e6f5f5910b4939e42a1bf484 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sun, 17 Aug 2025 02:15:31 -0400 Subject: [PATCH 095/720] Roll Skia from a30d857718ad to 88e8c099a086 (1 revision) (#173919) https://skia.googlesource.com/skia.git/+log/a30d857718ad..88e8c099a086 2025-08-17 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 57d74122fab7 to 0ae104d9374d (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 chinmaygarde@google.com,fmalita@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 11445f5fddbea..c342c96eeb831 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': 'a30d857718ad1e8c98e3e63c921ae5cc8a7598cf', + 'skia_revision': '88e8c099a086825f6e69afe0072ac8f3a07c57f2', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From b3fb4c786d80b4c6de9aba5534209561de20ecea Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sun, 17 Aug 2025 06:02:27 -0400 Subject: [PATCH 096/720] Roll Dart SDK from 67ca79475db6 to a874859559e6 (2 revisions) (#173922) https://dart.googlesource.com/sdk.git/+log/67ca79475db6..a874859559e6 2025-08-17 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-106.0.dev 2025-08-16 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.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 chinmaygarde@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 c342c96eeb831..8823137b020ca 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '67ca79475db612356bf11cbca5effa2ebebd1f75', + 'dart_revision': 'a874859559e6f09f65e0ed9e5af3375030c35e87', # 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 59ecc77ab0d828b83685ce18e6f61b4d318e1e05 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sun, 17 Aug 2025 22:02:18 -0400 Subject: [PATCH 097/720] Roll Dart SDK from a874859559e6 to 502455ee300b (1 revision) (#173934) https://dart.googlesource.com/sdk.git/+log/a874859559e6..502455ee300b 2025-08-18 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.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 chinmaygarde@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 8823137b020ca..0b69ddbe67960 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': 'a874859559e6f09f65e0ed9e5af3375030c35e87', + 'dart_revision': '502455ee300bf31bbf61eb2d7ccd5c139af492b6', # 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 51eb659d63917d9bff28efc0909882af0f9587f5 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sun, 17 Aug 2025 23:13:44 -0400 Subject: [PATCH 098/720] Roll Skia from 88e8c099a086 to 978726c1e725 (3 revisions) (#173935) https://skia.googlesource.com/skia.git/+log/88e8c099a086..978726c1e725 2025-08-17 skia-autoroll@skia-public.iam.gserviceaccount.com Roll vulkan-deps from 0ae104d9374d to eb3190110845 (1 revision) 2025-08-17 mike@reedtribe.org Reapply "Factor out common pathiter" 2025-08-17 skia-recreate-skps@skia-swarming-bots.iam.gserviceaccount.com Update SKP version 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 chinmaygarde@google.com,fmalita@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 0b69ddbe67960..abf1bd95824d1 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '88e8c099a086825f6e69afe0072ac8f3a07c57f2', + 'skia_revision': '978726c1e72592c10527209136b4d7e789558bbb', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From b1551a19e44df004e63bed84c91b115f49bf96bd Mon Sep 17 00:00:00 2001 From: Huy Date: Mon, 18 Aug 2025 10:19:33 +0700 Subject: [PATCH 099/720] Enhance FilledButton and Theme Data's documentation to clarify platform-specific visual density behavior (#173695) - Fix https://github.com/flutter/flutter/issues/155768 - Description: The issue is not a bug but WAI, it's due to visual density not being the same on each platform by default (see my full explanation at [here](https://github.com/flutter/flutter/issues/155768#issuecomment-3178302255)), see: https://github.com/flutter/flutter/blob/f22787193bcca7c1455f29e1ed4fd15215a736d5/packages/flutter/lib/src/material/theme_data.dart#L3250-L3255 - This is LOCs where visual density impacts the padding: https://github.com/flutter/flutter/blob/a04fb324be734cf18811c30c06baf9bf07b3bab3/packages/flutter/lib/src/material/button_style_button.dart#L501-L505 In this PR: - Add `Visual density effects` section to FilledButton documentation to avoid confusion for users - Improve the documentation for `VisualDensity.defaultDensityForPlatform` by including detailed information about the specific density for each platform. ## 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 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]. **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 [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 Signed-off-by: huycozy --- packages/flutter/lib/src/material/filled_button.dart | 8 ++++++++ packages/flutter/lib/src/material/theme_data.dart | 9 +++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/flutter/lib/src/material/filled_button.dart b/packages/flutter/lib/src/material/filled_button.dart index 2b8ac13c8c4f7..9dae69013737e 100644 --- a/packages/flutter/lib/src/material/filled_button.dart +++ b/packages/flutter/lib/src/material/filled_button.dart @@ -61,6 +61,14 @@ enum _FilledButtonVariant { filled, tonal } /// ** See code in examples/api/lib/material/filled_button/filled_button.0.dart ** /// {@end-tool} /// +/// ## Visual density effects +/// +/// The button's appearance is affected by the [VisualDensity] from the enclosing +/// [Theme] or from its [style]. Visual density adjusts the [ButtonStyle.padding] +/// and [ButtonStyle.minimumSize] to accommodate different UI densities across platforms. +/// See [VisualDensity] for more details on how it affects component layout and +/// the platform-specific defaults. +/// /// See also: /// /// * [ElevatedButton], a filled button whose material elevates when pressed. diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index d9499d35a9a6b..6b6b3ef34308e 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -3153,6 +3153,10 @@ class _FifoCache { /// the Material Design specification. It does not affect text sizes, icon /// sizes, or padding values. /// +/// The default visual density varies by platform: mobile platforms (Android, iOS, +/// Fuchsia) use [VisualDensity.standard], while desktop platforms (macOS, Windows, +/// Linux) use [VisualDensity.compact]. See [defaultDensityForPlatform] for more details. +/// /// For example, for buttons, it affects the spacing around the child of the /// button. For lists, it affects the distance between baselines of entries in /// the list. For chips, it only affects the vertical size, not the horizontal @@ -3163,6 +3167,7 @@ class _FifoCache { /// * [Checkbox] /// * [Chip] /// * [ElevatedButton] +/// * [FilledButton] /// * [IconButton] /// * [InputDecorator] (which gives density support to [TextField], etc.) /// * [ListTile] @@ -3240,8 +3245,8 @@ class VisualDensity with Diagnosticable { /// Returns a [VisualDensity] that is adaptive based on the given [platform]. /// - /// For desktop platforms, this returns [compact], and for other platforms, it - /// returns a default-constructed [VisualDensity]. + /// For mobile platforms (Android, iOS, Fuchsia), this returns [VisualDensity.standard], + /// and for desktop platforms (macOS, Windows, Linux), it returns [VisualDensity.compact]. /// /// See also: /// From a8faac8d4266da418cf24c193058e2d476399653 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Sun, 17 Aug 2025 23:40:38 -0400 Subject: [PATCH 100/720] Roll Fuchsia Linux SDK from sLmXksm1vZl2VEeA7... to n0EnLlotF2wczlOq_... (#173936) 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 chinmaygarde@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 abf1bd95824d1..2b028ada28c12 100644 --- a/DEPS +++ b/DEPS @@ -810,7 +810,7 @@ deps = { 'packages': [ { 'package': 'fuchsia/sdk/core/linux-amd64', - 'version': 'sLmXksm1vZl2VEeA7RO0ugVVEf-76QkCgnxUwE_1T64C' + 'version': 'n0EnLlotF2wczlOq_UYGeFcYljtSMroo0VecZcFApzQC' } ], 'condition': 'download_fuchsia_deps and not download_fuchsia_sdk', From 660c234bb02e60669d5d02bc814bb65c40b9cff9 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 18 Aug 2025 02:33:39 -0400 Subject: [PATCH 101/720] Roll Skia from 978726c1e725 to 3a17208059ec (2 revisions) (#173939) https://skia.googlesource.com/skia.git/+log/978726c1e725..3a17208059ec 2025-08-18 skia-autoroll@skia-public.iam.gserviceaccount.com Roll skottie-base from 951c83390400 to d3e0a508e29c 2025-08-18 skia-autoroll@skia-public.iam.gserviceaccount.com Roll jsfiddle-base from 74f9e2cd8bc1 to c4343b58423e 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 chinmaygarde@google.com,fmalita@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 2b028ada28c12..b454b994ead81 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '978726c1e72592c10527209136b4d7e789558bbb', + 'skia_revision': '3a17208059ec2b19e7a83921be8ece3974196754', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 78df185e5a8da5290246fb25d3c81851834c729a Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 18 Aug 2025 11:52:08 -0400 Subject: [PATCH 102/720] Roll Skia from 3a17208059ec to 07d71ea4d056 (4 revisions) (#173942) https://skia.googlesource.com/skia.git/+log/3a17208059ec..07d71ea4d056 2025-08-18 recipe-mega-autoroller@chops-service-accounts.iam.gserviceaccount.com Roll recipe dependencies (trivial). 2025-08-18 skia-autoroll@skia-public.iam.gserviceaccount.com Roll Dawn from b639f97ad620 to 2c53c1b75e06 (5 revisions) 2025-08-18 skia-autoroll@skia-public.iam.gserviceaccount.com Roll shaders-base from 5e22a60082ec to 0b85ebb8cd88 2025-08-18 skia-autoroll@skia-public.iam.gserviceaccount.com Roll debugger-app-base from 986ca4affc81 to 6943b399ac36 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 chinmaygarde@google.com,fmalita@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 b454b994ead81..fe75d1a0a234d 100644 --- a/DEPS +++ b/DEPS @@ -14,7 +14,7 @@ vars = { 'flutter_git': 'https://flutter.googlesource.com', 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', - 'skia_revision': '3a17208059ec2b19e7a83921be8ece3974196754', + 'skia_revision': '07d71ea4d05652efb882a784c0ebe6a35ad6087a', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. From 2265d94c6b0bbbf22193852e52e279e8c001d385 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Mon, 18 Aug 2025 09:33:30 -0700 Subject: [PATCH 103/720] Remove `embedded_android_views` (on-device) tests, same as emulator test (#173814) These tests are identical to the `Linux_android_emu` test that runs on presubmit: https://github.com/flutter/flutter/blob/9583f282a50f88a269f5716f1ecfe42f6b6387a3/.ci.yaml#L408-L416 These tests do not report benchmarks or any other statistics that would benefit from a real (on-device) test. --- .ci.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index d4ca31b5dcee9..36452b656e556 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -2728,15 +2728,6 @@ targets: ["devicelab", "android", "linux", "pixel", "7pro"] task_name: drive_perf_debug_warning - - name: Linux_pixel_7pro embedded_android_views_integration_test - recipe: devicelab/devicelab_drone - presubmit: false - timeout: 60 - properties: - tags: > - ["devicelab", "android", "linux", "pixel", "7pro"] - task_name: embedded_android_views_integration_test - - name: Linux_android_emu external_textures_integration_test recipe: devicelab/devicelab_drone bringup: true From 0480b069e02fb712183a221f6b12f471139e3686 Mon Sep 17 00:00:00 2001 From: Srujan Gaddam <58529443+srujzs@users.noreply.github.com> Date: Mon, 18 Aug 2025 12:16:21 -0700 Subject: [PATCH 104/720] [flutter_tools] Use DWDS 25.0.1 (#173777) DWDS 25.0.1 requires that a valid reloadedSourcesUri is passed and updated for both a hot restart and a hot reload. Therefore, the bootstrap scripts which use it as well as the code to write that file is updated. Note that this file is read in both DWDS and in the bootstrap script. Along with this, code is fixed to update modules and digests regardless of a full restart. Currently, it makes no difference as neither Flutter tools or DWDS makes use of the updated modules or digests with the new library bundle format, but it's updated for consistency. ## 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 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 `///`). - [ ] 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. --- .../lib/src/isolated/devfs_web.dart | 10 +-- .../lib/src/isolated/web_asset_server.dart | 37 +++------ .../flutter_tools/lib/src/web/bootstrap.dart | 76 ++++++++++--------- packages/flutter_tools/pubspec.yaml | 4 +- .../general.shard/web/bootstrap_test.dart | 2 +- 5 files changed, 55 insertions(+), 74 deletions(-) diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart index 8b19f531df041..54095e3817ce7 100644 --- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart +++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart @@ -403,13 +403,9 @@ class WebDevFS implements DevFS { } on FileSystemException catch (err) { throwToolExit('Failed to load recompiled sources:\n$err'); } - if (fullRestart) { - webAssetServer.performRestart( - modules, - writeRestartScripts: ddcModuleSystem && !bundleFirstUpload, - ); - } else { - webAssetServer.performReload(modules); + webAssetServer.updateModulesAndDigests(modules); + if (!bundleFirstUpload && ddcModuleSystem) { + webAssetServer.writeReloadedSources(modules); } return UpdateFSReport( success: true, 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 f3f8d492383d7..21be8f79f9969 100644 --- a/packages/flutter_tools/lib/src/isolated/web_asset_server.dart +++ b/packages/flutter_tools/lib/src/isolated/web_asset_server.dart @@ -97,21 +97,7 @@ class WebAssetServer implements AssetReader { /// Given a list of [modules] that need to be loaded, compute module names and /// digests. - /// - /// If [writeRestartScripts] is true, writes a list of sources mapped to their - /// ids to the file system that can then be consumed by the hot restart - /// callback. - /// - /// For example: - /// ```json - /// [ - /// { - /// "src": "", - /// "id": "", - /// }, - /// ] - /// ``` - void performRestart(List modules, {required bool writeRestartScripts}) { + void updateModulesAndDigests(List modules) { for (final module in modules) { // We skip computing the digest by using the hashCode of the underlying buffer. // Whenever a file is updated, the corresponding Uint8List.view it corresponds @@ -122,18 +108,13 @@ class WebAssetServer implements AssetReader { _modules[name] = path; _digests[name] = _webMemoryFS.files[moduleName].hashCode.toString(); } - if (writeRestartScripts) { - final srcIdsList = >[ - for (final String src in modules) {'src': '$baseUri/$src', 'id': src}, - ]; - writeFile('restart_scripts.json', json.encode(srcIdsList)); - } } - static const _reloadScriptsFileName = 'reload_scripts.json'; + static const _reloadedSourcesFileName = 'reloaded_sources.json'; - /// Given a list of [modules] that need to be reloaded, writes a file that - /// contains a list of objects each with three fields: + /// Given a list of [modules] that need to be reloaded during a hot restart or + /// hot reload, writes a file that contains a list of objects each with three + /// fields: /// /// `src`: A string that corresponds to the file path containing a DDC library /// bundle. To support embedded libraries, the path should include the @@ -155,7 +136,7 @@ class WebAssetServer implements AssetReader { /// /// The path of the output file should stay consistent across the lifetime of /// the app. - void performReload(List modules) { + void writeReloadedSources(List modules) { final moduleToLibrary = >[]; for (final module in modules) { final metadata = ModuleMetadata.fromJson( @@ -170,7 +151,7 @@ class WebAssetServer implements AssetReader { 'libraries': libraries, }); } - writeFile(_reloadScriptsFileName, json.encode(moduleToLibrary)); + writeFile(_reloadedSourcesFileName, json.encode(moduleToLibrary)); } @visibleForTesting @@ -356,9 +337,9 @@ class WebAssetServer implements AssetReader { ), ), packageConfigPath: buildInfo.packageConfigPath, - hotReloadSourcesUri: server._baseUri.replace( + reloadedSourcesUri: server._baseUri.replace( pathSegments: List.from(server._baseUri.pathSegments) - ..add(_reloadScriptsFileName), + ..add(_reloadedSourcesFileName), ), ).strategy : FrontendServerRequireStrategyProvider( diff --git a/packages/flutter_tools/lib/src/web/bootstrap.dart b/packages/flutter_tools/lib/src/web/bootstrap.dart index d2fa08a896587..a9c4e81468fce 100644 --- a/packages/flutter_tools/lib/src/web/bootstrap.dart +++ b/packages/flutter_tools/lib/src/web/bootstrap.dart @@ -8,14 +8,14 @@ import 'package:package_config/package_config.dart'; const _simpleLoaderScript = r''' window.$dartCreateScript = (function() { // Find the nonce value. (Note, this is only computed once.) - var scripts = Array.from(document.getElementsByTagName("script")); - var nonce; + const scripts = Array.from(document.getElementsByTagName("script")); + let nonce; scripts.some( script => (nonce = script.nonce || script.getAttribute("nonce"))); // If present, return a closure that automatically appends the nonce. if (nonce) { return function() { - var script = document.createElement("script"); + const script = document.createElement("script"); script.nonce = nonce; return script; }; @@ -29,10 +29,10 @@ window.$dartCreateScript = (function() { // Loads a module [relativeUrl] relative to [root]. // // If not specified, [root] defaults to the directory serving the main app. -var forceLoadModule = function (relativeUrl, root) { - var actualRoot = root ?? _currentDirectory; +const forceLoadModule = function (relativeUrl, root) { + const actualRoot = root ?? _currentDirectory; return new Promise(function(resolve, reject) { - var script = self.$dartCreateScript(); + const script = self.$dartCreateScript(); let policy = { createScriptURL: function(src) {return src;} }; @@ -150,11 +150,11 @@ String generateDDCLibraryBundleBootstrapScript({ return ''' ${generateLoadingIndicator ? _generateLoadingIndicator() : ""} // Save the current directory so we can access it in a closure. -var _currentDirectory = (function () { - var _url = document.currentScript.src; - var lastSlash = _url.lastIndexOf('/'); +const _currentDirectory = (function () { + const _url = document.currentScript.src; + const lastSlash = _url.lastIndexOf('/'); if (lastSlash == -1) return _url; - var currentDirectory = _url.substring(0, lastSlash + 1); + const currentDirectory = _url.substring(0, lastSlash + 1); return currentDirectory; })(); @@ -184,7 +184,7 @@ $_simpleLoaderScript Promise.all(prerequisiteLoads).then((_) => afterPrerequisiteLogic()); // Save the current script so we can access it in a closure. - var _currentScript = document.currentScript; + const _currentScript = document.currentScript; // Create a policy if needed to load the files during a hot restart. let policy = { @@ -194,7 +194,7 @@ $_simpleLoaderScript policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy); } - var afterPrerequisiteLogic = function() { + const afterPrerequisiteLogic = function() { window.\$dartLoader.rootDirectories.push(_currentDirectory); let scripts = [ { @@ -238,7 +238,7 @@ $_simpleLoaderScript !window.\$dartStackTraceUtility.ready) { window.\$dartStackTraceUtility.ready = true; window.\$dartStackTraceUtility.setSourceMapProvider(function(url) { - var baseUrl = window.location.protocol + '//' + window.location.host; + const baseUrl = window.location.protocol + '//' + window.location.host; url = url.replace(baseUrl + '/', ''); if (url == 'dart_sdk.js') { return dartDevEmbedder.debugger.getSourceMap('dart_sdk'); @@ -252,40 +252,44 @@ $_simpleLoaderScript // We should have written a file containing all the scripts that need to be // reloaded into the page. This is then read when a hot restart is triggered // in DDC via the `\$dartReloadModifiedModules` callback. - let restartScripts = _currentDirectory + 'restart_scripts.json'; + // TODO(srujzs): We should avoid using a callback here in the bootstrap once + // the embedder supports passing a list of files/libraries to `hotRestart` + // instead. Currently, we're forced to read this file twice. + let reloadedSources = _currentDirectory + 'reloaded_sources.json'; if (!window.\$dartReloadModifiedModules) { window.\$dartReloadModifiedModules = (function(appName, callback) { - var xhttp = new XMLHttpRequest(); + const xhttp = new XMLHttpRequest(); xhttp.withCredentials = true; xhttp.onreadystatechange = function() { // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState if (this.readyState == 4 && this.status == 200 || this.status == 304) { - var scripts = JSON.parse(this.responseText); - var numToLoad = 0; - var numLoaded = 0; - for (var i = 0; i < scripts.length; i++) { - var script = scripts[i]; - if (script.id == null) continue; - var src = script.src.toString(); - var oldSrc = window.\$dartLoader.moduleIdToUrl.get(script.id); + const scripts = JSON.parse(this.responseText); + let numToLoad = 0; + let numLoaded = 0; + for (let i = 0; i < scripts.length; i++) { + const script = scripts[i]; + const module = script.module; + if (module == null) continue; + const src = script.src; + const oldSrc = window.\$dartLoader.moduleIdToUrl.get(module); // We might actually load from a different uri, delete the old one // just to be sure. window.\$dartLoader.urlToModuleId.delete(oldSrc); - window.\$dartLoader.moduleIdToUrl.set(script.id, src); - window.\$dartLoader.urlToModuleId.set(src, script.id); + window.\$dartLoader.moduleIdToUrl.set(module, src); + window.\$dartLoader.urlToModuleId.set(src, module); numToLoad++; - var el = document.getElementById(script.id); + let el = document.getElementById(module); if (el) el.remove(); el = window.\$dartCreateScript(); el.src = policy.createScriptURL(src); el.async = false; el.defer = true; - el.id = script.id; + el.id = module; el.onload = function() { numLoaded++; if (numToLoad == numLoaded) callback(); @@ -296,7 +300,7 @@ $_simpleLoaderScript if (numToLoad == 0) callback(); } }; - xhttp.open("GET", restartScripts, true); + xhttp.open("GET", reloadedSources, true); xhttp.send(); }); } @@ -381,7 +385,7 @@ document.head.appendChild(requireEl); /// or `flutter build web --debug` should not use this indicator. String _generateLoadingIndicator() { return ''' -var styles = ` +const styles = ` .flutter-loader { width: 100%; height: 8px; @@ -437,16 +441,16 @@ var styles = ` } `; -var styleSheet = document.createElement("style") +const styleSheet = document.createElement("style") styleSheet.type = "text/css"; styleSheet.innerText = styles; document.head.appendChild(styleSheet); -var loader = document.createElement('div'); +const loader = document.createElement('div'); loader.className = "flutter-loader"; document.body.append(loader); -var indeterminate = document.createElement('div'); +const indeterminate = document.createElement('div'); indeterminate.className = "indeterminate"; loader.appendChild(indeterminate); @@ -474,14 +478,14 @@ String generateDDCLibraryBundleMainModule({ /* ENTRYPOINT_EXTENTION_MARKER */ (function() { - let appName = "org-dartlang-app:/$entrypoint"; + const appName = "org-dartlang-app:/$entrypoint"; dartDevEmbedder.debugger.registerDevtoolsFormatter(); $setMaxRequests // Set up a final script that lets us know when all scripts have been loaded. // Only then can we call the main method. - let onLoadEndSrc = '$onLoadEndBootstrap'; + const onLoadEndSrc = '$onLoadEndBootstrap'; window.\$dartLoader.loadConfig.bootstrapScript = { src: onLoadEndSrc, id: onLoadEndSrc, @@ -490,9 +494,9 @@ String generateDDCLibraryBundleMainModule({ // Should be called by $onLoadEndBootstrap once all the scripts have been // loaded. window.$_onLoadEndCallback = function() { - let child = {}; + const child = {}; child.main = function() { - let sdkOptions = { + const sdkOptions = { nativeNonNullAsserts: $nativeNullAssertions, }; dartDevEmbedder.runMain(appName, sdkOptions); diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 95056f3aeb770..a31b86346fd83 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: archive: 3.6.1 args: 2.7.0 dds: 5.0.3 - dwds: 24.4.1 + dwds: 25.0.1 code_builder: 4.10.1 collection: 1.19.1 completion: 1.0.2 @@ -126,4 +126,4 @@ dev_dependencies: dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: f0tepf +# PUBSPEC CHECKSUM: dvaljn diff --git a/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart b/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart index 5a3379f64e87c..663a740f70a63 100644 --- a/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart +++ b/packages/flutter_tools/test/general.shard/web/bootstrap_test.dart @@ -267,7 +267,7 @@ void main() { isCi: true, ); // bootstrap main module has correct defined module. - expect(result, contains('let appName = "org-dartlang-app:/main.js";')); + expect(result, contains('const appName = "org-dartlang-app:/main.js";')); expect(result, contains('dartDevEmbedder.runMain(appName, sdkOptions);')); }); From 461ce17941ae46ed656b187fe5e627d660608d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Sharma?= <737941+loic-sharma@users.noreply.github.com> Date: Mon, 18 Aug 2025 12:34:59 -0700 Subject: [PATCH 105/720] Explain how to run Google Test tests directly (#173978) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../docs/testing/Testing-the-engine.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/engine/src/flutter/docs/testing/Testing-the-engine.md b/engine/src/flutter/docs/testing/Testing-the-engine.md index ccef8b1029686..978673b70fa34 100644 --- a/engine/src/flutter/docs/testing/Testing-the-engine.md +++ b/engine/src/flutter/docs/testing/Testing-the-engine.md @@ -52,6 +52,41 @@ on and for your host machine architecture. It's best practice to test only one real production class per test and create mocks for all other dependencies. +Each Google Test target produces an executable in the `out` directory with the +`_unittests` suffix. You use these executables to run Google Tests. + +To run the shell's C++ unit tests: + +```sh +../out/host_debug_unopt/shell_unittests +``` + +To run a single test, use [Google Test's filters][]: + +```sh +../out/host_debug_unopt/shell_unittests --gtest_filter="ShellTest.WaitForFirstFrame" +``` + +[Google Test's filters]: https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests + +You can use `*` wildcards to run tests whose name matches a pattern: + +```sh +../out/host_debug_unopt/shell_unittests --gtest_filter="ShellTest.WaitFor*" +``` + +> [!TIP] +> Google Test supports other patterns, like `-` for exclusions and `:` for joins. +> Check [Google Test's documentation][] for details. + +[Google Test's documentation]: https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests + +To reproduce test flakes, you can run a test multiple times: + +```sh +../out/host_debug_unopt/shell_unittests --gtest_filter="ShellTest.WaitForFirstFrame" --gtest_repeat=1000 +``` + ## Java - Android embedding If you edit `.java` files in the [`shell/platform/android`](../../shell/platform/android/) From 8b2b9d7c8d18a20ea666fe33593cf9c1960d13a8 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Mon, 18 Aug 2025 15:49:29 -0400 Subject: [PATCH 106/720] [ Widget Preview ] Don't try to instantiate invalid `@Preview()` applications (#173984) Applying `@Preview()` to an invalid AST node shouldn't cause the preview environment to throw an exception due to invalid generated code. This change adds some additional checks to ensure that invalid `@Preview()` applications are ignored. Related issue: https://github.com/flutter/flutter/issues/173959 Related stable hotfix: https://github.com/flutter/flutter/pull/173979 --- .../src/widget_preview/dependency_graph.dart | 61 +++--- .../lib/src/widget_preview/utils.dart | 3 + ...tor_invalid_preview_applications_test.dart | 181 ++++++++++++++++++ 3 files changed, 222 insertions(+), 23 deletions(-) create mode 100644 packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector/preview_detector_invalid_preview_applications_test.dart diff --git a/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart b/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart index 4582f9b8ba8a3..84b5a8c4f0a02 100644 --- a/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart +++ b/packages/flutter_tools/lib/src/widget_preview/dependency_graph.dart @@ -76,6 +76,10 @@ class _PreviewVisitor extends RecursiveAstVisitor { _scopedVisitChildren(node, (MethodDeclaration? node) => _currentMethod = node); } + bool hasRequiredParams(FormalParameterList? params) { + return params?.parameters.any((p) => p.isRequired) ?? false; + } + @override void visitAnnotation(Annotation node) { final previewsToProcess = []; @@ -88,18 +92,24 @@ class _PreviewVisitor extends RecursiveAstVisitor { } for (final preview in previewsToProcess) { - assert(_currentFunction != null || _currentConstructor != null || _currentMethod != null); - if (_currentFunction != null) { - final returnType = _currentFunction!.returnType! as NamedType; - previewEntries.add( - PreviewDetails( - packageName: packageName, - functionName: _currentFunction!.name.toString(), - isBuilder: returnType.name.isWidgetBuilder, - previewAnnotation: preview, - ), - ); - } else if (_currentConstructor != null) { + if (_currentFunction != null && + !hasRequiredParams(_currentFunction!.functionExpression.parameters)) { + final TypeAnnotation? returnTypeAnnotation = _currentFunction!.returnType; + if (returnTypeAnnotation is NamedType) { + final Token returnType = returnTypeAnnotation.name; + if (returnType.isWidget || returnType.isWidgetBuilder) { + previewEntries.add( + PreviewDetails( + packageName: packageName, + functionName: _currentFunction!.name.toString(), + isBuilder: returnType.isWidgetBuilder, + previewAnnotation: preview, + ), + ); + } + } + } else if (_currentConstructor != null && + !hasRequiredParams(_currentConstructor!.parameters)) { final returnType = _currentConstructor!.returnType as SimpleIdentifier; final Token? name = _currentConstructor!.name; previewEntries.add( @@ -110,17 +120,22 @@ class _PreviewVisitor extends RecursiveAstVisitor { previewAnnotation: preview, ), ); - } else if (_currentMethod != null) { - final returnType = _currentMethod!.returnType! as NamedType; - final parentClass = _currentMethod!.parent! as ClassDeclaration; - previewEntries.add( - PreviewDetails( - packageName: packageName, - functionName: '${parentClass.name}.${_currentMethod!.name}', - isBuilder: returnType.name.isWidgetBuilder, - previewAnnotation: preview, - ), - ); + } else if (_currentMethod != null && !hasRequiredParams(_currentMethod!.parameters)) { + final TypeAnnotation? returnTypeAnnotation = _currentMethod!.returnType; + if (returnTypeAnnotation is NamedType) { + final Token returnType = returnTypeAnnotation.name; + if (returnType.isWidget || returnType.isWidgetBuilder) { + final parentClass = _currentMethod!.parent! as ClassDeclaration; + previewEntries.add( + PreviewDetails( + packageName: packageName, + functionName: '${parentClass.name}.${_currentMethod!.name}', + isBuilder: returnType.isWidgetBuilder, + previewAnnotation: preview, + ), + ); + } + } } } } diff --git a/packages/flutter_tools/lib/src/widget_preview/utils.dart b/packages/flutter_tools/lib/src/widget_preview/utils.dart index 3a2144aeed06e..b4fed3e0da8a9 100644 --- a/packages/flutter_tools/lib/src/widget_preview/utils.dart +++ b/packages/flutter_tools/lib/src/widget_preview/utils.dart @@ -22,6 +22,9 @@ extension TokenExtension on Token { /// Convenience getter to identify WidgetBuilder types. bool get isWidgetBuilder => toString() == 'WidgetBuilder'; + + /// Convenience getter to identify Widget types. + bool get isWidget => toString() == 'Widget'; } extension AnnotationExtension on Annotation { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector/preview_detector_invalid_preview_applications_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector/preview_detector_invalid_preview_applications_test.dart new file mode 100644 index 0000000000000..0a978cece0091 --- /dev/null +++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_detector/preview_detector_invalid_preview_applications_test.dart @@ -0,0 +1,181 @@ +// 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/widget_preview/dependency_graph.dart'; +import 'package:flutter_tools/src/widget_preview/preview_detector.dart'; +import 'package:test/test.dart'; + +import '../../../../src/common.dart'; +import '../../../../src/context.dart'; +import '../utils/preview_details_matcher.dart'; +import '../utils/preview_detector_test_utils.dart'; +import '../utils/preview_project.dart'; + +// Note: this test isn't under the general.shard since tests under that directory +// have a 2000ms time out and these tests write to the real file system and watch +// directories for changes. This can be slow on heavily loaded machines and cause +// flaky failures. + +/// Creates a project with files containing invalid preview applications. +class BasicProjectWithInvalidPreviews extends WidgetPreviewProject { + BasicProjectWithInvalidPreviews._({ + required super.projectRoot, + required List pathsWithPreviews, + required List pathsWithoutPreviews, + }) { + final initialSources = []; + for (final path in pathsWithPreviews) { + initialSources.add((path: path, source: _invalidPreviewContainingFileContents)); + librariesWithPreviews.add(toPreviewPath(path)); + } + for (final path in pathsWithoutPreviews) { + initialSources.add((path: path, source: _emptySource)); + librariesWithoutPreviews.add(toPreviewPath(path)); + } + initialSources.forEach(writeFile); + } + + static Future create({ + required Directory projectRoot, + required List pathsWithPreviews, + required List pathsWithoutPreviews, + }) async { + final project = BasicProjectWithInvalidPreviews._( + projectRoot: projectRoot, + pathsWithPreviews: pathsWithPreviews, + pathsWithoutPreviews: pathsWithoutPreviews, + ); + await project.initializePubspec(); + return project; + } + + final librariesWithPreviews = {}; + final librariesWithoutPreviews = {}; + + /// Adds a file containing previews at [path]. + void addPreviewContainingFile({required String path}) { + writeFile((path: path, source: _invalidPreviewContainingFileContents)); + final PreviewPath previewPath = toPreviewPath(path); + librariesWithoutPreviews.remove(previewPath); + librariesWithPreviews.add(previewPath); + } + + Map> get matcherMapping => + >{ + for (final PreviewPath path in librariesWithPreviews) path: [], + }; + + static const _emptySource = ''' +void main() {} +'''; + + static const _invalidPreviewContainingFileContents = ''' + + +@Preview(name: 'Invalid preview on class declaration') +class ClassDeclaration extends StatelessWidget { + @Preview(name: 'Invalid preview on constructor with required parameters') + ClassDeclaration(int i); + + @Preview(name: 'Invalid preview on getter'); + int get foo => 1; + + @Preview(name: 'Invalid preview on setter'); + set foo(x) { + print('foo set'); + }; + + @Preview(name: 'Invalid preview on field') + final int bar = 2; + + @Preview(name: 'Invalid preview on member function') + Widget memberFunction() => Text('Member'); + + @override + Widget build(BuildContext context) => Text('Foo'); +} + +@Preview(name: 'Invalid preview on function with void return') +void previews() => Text('Foo'); + +@Preview(name: 'Invalid preview on function with parameter') +Widget foo(int bar) => Text('Foo'); + +@Preview(name: 'Invalid preview on extension') +extension on ClassDeclaration {} +'''; +} + +void main() { + initializeTestPreviewDetectorState(); + group('$PreviewDetector', () { + // Note: we don't use a MemoryFileSystem since we don't have a way to + // provide it to package:analyzer APIs without writing a significant amount + // of wrapper logic. + late PreviewDetector previewDetector; + late BasicProjectWithInvalidPreviews project; + + setUp(() { + previewDetector = createTestPreviewDetector(); + }); + + tearDown(() async { + await previewDetector.dispose(); + }); + + testUsingContext('ignores invalid previews in existing files', () async { + project = await BasicProjectWithInvalidPreviews.create( + projectRoot: previewDetector.projectRoot, + pathsWithPreviews: ['foo.dart'], + pathsWithoutPreviews: [], + ); + final PreviewDependencyGraph mapping = await previewDetector.initialize(); + expectContainsPreviews(mapping, project.matcherMapping); + }); + + testUsingContext('ignores invalid previews in updated files', () async { + project = await BasicProjectWithInvalidPreviews.create( + projectRoot: previewDetector.projectRoot, + pathsWithPreviews: [], + pathsWithoutPreviews: ['foo.dart'], + ); + + // Initialize the file watcher. + final PreviewDependencyGraph initialPreviews = await previewDetector.initialize(); + expectContainsPreviews(initialPreviews, project.matcherMapping); + + await waitForChangeDetected( + onChangeDetected: (PreviewDependencyGraph updated) { + // There should be no valid previews in foo.dart. + expectContainsPreviews(updated, project.matcherMapping); + }, + changeOperation: () => project.addPreviewContainingFile(path: 'foo.dart'), + ); + }); + + testUsingContext('ignores invalid previews in newly added files', () async { + project = await BasicProjectWithInvalidPreviews.create( + projectRoot: previewDetector.projectRoot, + pathsWithPreviews: [], + pathsWithoutPreviews: [], + ); + // The initial mapping should be empty as there's no files containing previews. + const expectedInitialMapping = {}; + + // Initialize the file watcher. + final PreviewDependencyGraph initialPreviews = await previewDetector.initialize(); + expect(initialPreviews, expectedInitialMapping); + + await waitForChangeDetected( + onChangeDetected: (PreviewDependencyGraph updated) { + // There should be no valid previews in baz.dart. + expectContainsPreviews(updated, project.matcherMapping); + }, + // Create baz.dart, which contains previews. + changeOperation: () => project.addPreviewContainingFile(path: 'baz.dart'), + ); + }); + }); +} From 02bf6d36c3ed450224080e39c25597805b241207 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Mon, 18 Aug 2025 16:07:26 -0400 Subject: [PATCH 107/720] [ Widget Preview ] Don't crash when directory watcher restarts on Windows (#173987) Fixes https://github.com/flutter/flutter/issues/173895 This is a top-10 crasher for `3.35.{0,1}`. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../lib/src/commands/widget_preview.dart | 1 + .../src/widget_preview/preview_detector.dart | 34 +++++++++++++++++-- .../preview_code_generator_test.dart | 1 + .../utils/preview_detector_test_utils.dart | 1 + 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart index b0ac42df70fad..d49520491eabe 100644 --- a/packages/flutter_tools/lib/src/commands/widget_preview.dart +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -223,6 +223,7 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C ); late final _previewDetector = PreviewDetector( + platform: platform, previewAnalytics: previewAnalytics, projectRoot: rootProject.directory, logger: logger, diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart index dfd95a8f2d540..90b8b1b3c5599 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart @@ -14,28 +14,44 @@ import 'package:watcher/watcher.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; +import '../base/platform.dart'; import '../base/utils.dart'; import 'analytics.dart'; import 'dependency_graph.dart'; import 'utils.dart'; +typedef WatcherBuilder = Watcher Function(String path); + +Watcher _defaultWatcherBuilder(String path) { + return Watcher(path); +} + class PreviewDetector { PreviewDetector({ + required this.platform, required this.previewAnalytics, required this.projectRoot, required this.fs, required this.logger, required this.onChangeDetected, required this.onPubspecChangeDetected, + @visibleForTesting this.watcherBuilder = _defaultWatcherBuilder, }); + final Platform platform; final WidgetPreviewAnalytics previewAnalytics; final Directory projectRoot; final FileSystem fs; final Logger logger; final void Function(PreviewDependencyGraph) onChangeDetected; final void Function(String path) onPubspecChangeDetected; + final WatcherBuilder watcherBuilder; + @visibleForTesting + static const kDirectoryWatcherClosedUnexpectedlyPrefix = 'Directory watcher closed unexpectedly'; + @visibleForTesting + static const kWindowsFileWatcherRestartedMessage = + 'WindowsDirectoryWatcher has closed and been restarted.'; StreamSubscription? _fileWatcher; final _mutex = PreviewDetectorMutex(); @@ -57,8 +73,22 @@ class PreviewDetector { // Determine which files have transitive dependencies with compile time errors. _propagateErrors(); - final watcher = Watcher(projectRoot.path); - _fileWatcher = watcher.events.listen(_onFileSystemEvent); + final Watcher watcher = watcherBuilder(projectRoot.path); + _fileWatcher = watcher.events.listen( + _onFileSystemEvent, + onError: (Object e, StackTrace st) { + if (platform.isWindows && + e is FileSystemException && + e.message.startsWith(kDirectoryWatcherClosedUnexpectedlyPrefix)) { + // The Windows directory watcher sometimes decides to shutdown on its own. It's + // automatically restarted by package:watcher, but we need to handle this exception. + // See https://github.com/dart-lang/tools/issues/1713 for details. + logger.printTrace(kWindowsFileWatcherRestartedMessage); + return; + } + Error.throwWithStackTrace(e, st); + }, + ); // Wait for file watcher to finish initializing, otherwise we might miss changes and cause // tests to flake. diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart index 942d62f9223c2..d8502619f9be3 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/preview_code_generator_test.dart @@ -171,6 +171,7 @@ void main() { ..childFile('lib/src/transitive_error.dart').writeAsStringSync(kTransitiveErrorLibrary); project = FlutterProject.fromDirectoryTest(projectDir); previewDetector = PreviewDetector( + platform: FakePlatform(), previewAnalytics: WidgetPreviewAnalytics( analytics: getInitializedFakeAnalyticsInstance( // We don't care about anything written to the file system by analytics, so we're safe diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_detector_test_utils.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_detector_test_utils.dart index 57e68c1350ab4..b6afd945f1850 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_detector_test_utils.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/utils/preview_detector_test_utils.dart @@ -50,6 +50,7 @@ PreviewDetector createTestPreviewDetector() { } _projectRoot = _fs.systemTempDirectory.createTempSync('root'); return PreviewDetector( + platform: FakePlatform(), previewAnalytics: WidgetPreviewAnalytics( analytics: getInitializedFakeAnalyticsInstance( fakeFlutterVersion: FakeFlutterVersion(), From 1e1908dc2823d8e41b2366b2c34ec6e3c0337fd7 Mon Sep 17 00:00:00 2001 From: Reid Baker <1063596+reidbaker@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:21:29 +0000 Subject: [PATCH 108/720] Migrate deeplink json creation to public AGP api (#173794) Reviewers please pay special attention to the tests that were added and tests that were removed. If you see a set of functionality that is not covered and should be please say something. I read every test (and TBH also had to edit most of them) but I had gemini's agent mode help and I dont trust there is something I missed. Related to #173651 Newly added tests can be run from `packages/flutter_tools/gradle` with `./gradlew test --tests com.flutter.gradle.tasks.DeepLinkJsonFromManifestTaskTest` ## 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 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. --- .../gradle/src/main/kotlin/FlutterPlugin.kt | 10 +- .../src/main/kotlin/FlutterPluginUtils.kt | 215 ++--------- .../tasks/DeepLinkJsonFromManifestTask.kt | 47 +++ .../DeepLinkJsonFromManifestTaskHelper.kt | 184 ++++++++++ .../src/main/kotlin/tasks/FlutterTask.kt | 2 + .../src/test/kotlin/FlutterPluginTest.kt | 13 + .../src/test/kotlin/FlutterPluginUtilsTest.kt | 159 --------- .../tasks/DeepLinkJsonFromManifestTaskTest.kt | 336 ++++++++++++++++++ 8 files changed, 616 insertions(+), 350 deletions(-) create mode 100644 packages/flutter_tools/gradle/src/main/kotlin/tasks/DeepLinkJsonFromManifestTask.kt create mode 100644 packages/flutter_tools/gradle/src/main/kotlin/tasks/DeepLinkJsonFromManifestTaskHelper.kt create mode 100644 packages/flutter_tools/gradle/src/test/kotlin/tasks/DeepLinkJsonFromManifestTaskTest.kt diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt index 23a931a087534..eab97a883626b 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt @@ -704,7 +704,7 @@ class FlutterPlugin : Plugin { validateDeferredComponents = validateDeferredComponentsValue flavor = flavorValue } - val compileTask: FlutterTask = compileTaskProvider.get() + val flutterCompileTask: FlutterTask = compileTaskProvider.get() val libJar: File = project.file( project.layout.buildDirectory.dir("${FlutterPluginConstants.INTERMEDIATES_DIR}/flutter/${variant.name}/libs.jar") @@ -716,10 +716,10 @@ class FlutterPlugin : Plugin { ) { destinationDirectory.set(libJar.parentFile) archiveFileName.set(libJar.name) - dependsOn(compileTask) + dependsOn(flutterCompileTask) targetPlatforms.forEach { targetPlatform -> val abi: String? = FlutterPluginConstants.PLATFORM_ARCH_MAP[targetPlatform] - from("${compileTask.intermediateDir}/$abi") { + from("${flutterCompileTask.intermediateDir}/$abi") { include("*.so") // Move `app.so` to `lib//libapp.so` rename { filename: String -> "lib/$abi/lib$filename" } @@ -749,8 +749,8 @@ class FlutterPlugin : Plugin { "copyFlutterAssets${FlutterPluginUtils.capitalize(variant.name)}", Copy::class.java ) { - dependsOn(compileTask) - with(compileTask.assets) + dependsOn(flutterCompileTask) + with(flutterCompileTask.assets) filePermissions { user { read = true diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt index 55747ba1e92f4..b051c10c71bd8 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -4,13 +4,15 @@ package com.flutter.gradle +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.gradle.AbstractAppExtension import com.android.build.gradle.BaseExtension import com.android.build.gradle.tasks.ProcessAndroidResources import com.android.builder.model.BuildType import com.flutter.gradle.plugins.PluginHandler +import com.flutter.gradle.tasks.DeepLinkJsonFromManifestTask import groovy.lang.Closure -import groovy.util.Node import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task @@ -24,10 +26,6 @@ import java.util.Properties * A collection of static utility functions used by the Flutter Gradle Plugin. */ object FlutterPluginUtils { - private const val MANIFEST_NAME_KEY = "android:name" - private const val MANIFEST_VALUE_KEY = "android:value" - private const val MANIFEST_VALUE_TRUE = "true" - // Gradle properties. These must correspond to the values used in // flutter/packages/flutter_tools/lib/src/android/gradle.dart, and therefore it is not // recommended to use these const values in tests. @@ -399,6 +397,7 @@ object FlutterPluginUtils { return project.extensions.findByType(BaseExtension::class.java)!! } + // Avoid new usages this class is not part of the public AGP DSL. private fun getAndroidAppExtensionOrNull(project: Project): AbstractAppExtension? = project.extensions.findByType(AbstractAppExtension::class.java) @@ -783,16 +782,11 @@ object FlutterPluginUtils { * Add a task that can be called on Flutter projects that outputs app link related project * settings into a json file. * See https://developer.android.com/training/app-links/ for more information about app link. - * The json will be saved in path stored in outputPath parameter. + * The json will be saved in path stored in "outputPath" parameter or in the projects build + * directory with the file deeplink.json if not specified. + * + * See DeepLinkJsonFromManifestTask for the structure of the json. * - * An example json: - * { - * applicationId: "com.example.app", - * deeplinks: [ - * {"scheme":"http", "host":"example.com", "path":".*"}, - * {"scheme":"https","host":"example.com","path":".*"} - * ] - * } * The output file is parsed and used by devtool. */ @JvmStatic @@ -800,182 +794,31 @@ object FlutterPluginUtils { internal fun addTasksForOutputsAppLinkSettings(project: Project) { // Integration test for AppLinkSettings task defined in // flutter/flutter/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart - val android = getAndroidAppExtensionOrNull(project) - if (android == null) { - project.logger.info("addTasksForOutputsAppLinkSettings called on project without android extension.") - return - } - android.applicationVariants.configureEach { - val variant = this - project.tasks.register("output${capitalize(variant.name)}AppLinkSettings") { - val task: Task = this - task.description = - "stores app links settings for the given build variant of this Android project into a json file." - variant.outputs.configureEach { - // TODO(gmackall): Migrate to AGPs variant api. - // https://github.com/flutter/flutter/issues/166550 - @Suppress("DEPRECATION") - val baseVariantOutput: com.android.build.gradle.api.BaseVariantOutput = this - // Deeplinks are defined in AndroidManifest.xml and is only available after - // processResourcesProvider. - dependsOn(findProcessResources(baseVariantOutput)) - } - doLast { - // We are configuring the same object before a doLast and in a doLast. - // without a clear reason why. That is not good. - variant.outputs.configureEach { - val appLinkSettings = createAppLinkSettings(variant, this) - File(project.property("outputPath").toString()).writeText( - appLinkSettings.toJson().toString() + val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java) + androidComponents.onVariants { variant -> + val manifestUpdater = + project.tasks.register("output${capitalize(variant.name)}AppLinkSettings", DeepLinkJsonFromManifestTask::class.java) { + namespace.set(variant.namespace) + // Flutter should always use project.layout.buildDirectory.file("deeplink.json") + // instead of relying on passing in a path. + if (project.hasProperty("outputPath")) { + deepLinkJson.set( + File(project.property("outputPath").toString()) ) + } else { + deepLinkJson.set(project.layout.buildDirectory.file("deeplink.json")) } } - } - } - } - - /** - * Extracts app deeplink information from the Android manifest file of a variant then returns - * an AppLinkSettings object. - * - * @param BaseVariantOutput The output of a specific build variant (e.g., debug, release). - * @param variant The application variant being processed. - */ - @Suppress("KDocUnresolvedReference") - private fun createAppLinkSettings( - // TODO(gmackall): Migrate to AGPs variant api. - // https://github.com/flutter/flutter/issues/166550 - @Suppress("DEPRECATION") variant: com.android.build.gradle.api.ApplicationVariant, - @Suppress("DEPRECATION") baseVariantOutput: com.android.build.gradle.api.BaseVariantOutput - ): AppLinkSettings { - val appLinkSettings = AppLinkSettings(variant.applicationId) - - // XmlParser is not namespace aware because it makes querying nodes cumbersome. - // TODO(gmackall): Migrate to AGPs variant api. - // https://github.com/flutter/flutter/issues/166550 - @Suppress("DEPRECATION") - val manifest: Node = - groovy.xml - .XmlParser(false, false) - .parse(findProcessResources(baseVariantOutput).manifestFile) - val applicationNode: Node? = - manifest.children().find { node -> - node is Node && node.name() == "application" - } as Node? - if (applicationNode == null) { - return appLinkSettings - } - val activities: List = - applicationNode.children().filterIsInstance().filter { item -> - item.name() == "activity" - } - - activities.forEach { activity -> - val metaDataItems: List = - activity.children().filterIsInstance().filter { metaItem -> - metaItem.name() == "meta-data" - } - metaDataItems.forEach { metaDataItem -> - val nameAttribute: Boolean = - metaDataItem.attribute(MANIFEST_NAME_KEY) == "flutter_deeplinking_enabled" - val valueAttribute: Boolean = - metaDataItem.attribute(MANIFEST_VALUE_KEY) == MANIFEST_VALUE_TRUE - if (nameAttribute && valueAttribute) { - appLinkSettings.deeplinkingFlagEnabled = true - } - } - val intentFilterItems: List = - activity.children().filterIsInstance().filter { filterItem -> - filterItem.name() == "intent-filter" - } - intentFilterItems.forEach { appLinkIntent -> - // Print out the host attributes in data tags. - val schemes: MutableSet = mutableSetOf() - val hosts: MutableSet = mutableSetOf() - val paths: MutableSet = mutableSetOf() - val intentFilterCheck = IntentFilterCheck() - if (appLinkIntent.attribute("android:autoVerify") == MANIFEST_VALUE_TRUE) { - intentFilterCheck.hasAutoVerify = true - } - - val actionItems: List = - appLinkIntent.children().filterIsInstance().filter { item -> - item.name() == "action" - } - // Any action item causes intentFilterCheck to always be true - // and we keep looping instead of exiting out early. - // TODO: Exit out early per intent filter action view. - actionItems.forEach { action -> - if (action.attribute(MANIFEST_NAME_KEY) == "android.intent.action.VIEW") { - intentFilterCheck.hasActionView = true - } - } - val categoryItems: List = - appLinkIntent.children().filterIsInstance().filter { item -> - item.name() == "category" - } - categoryItems.forEach { category -> - // TODO: Exit out early per intent filter default category. - if (category.attribute(MANIFEST_NAME_KEY) == "android.intent.category.DEFAULT") { - intentFilterCheck.hasDefaultCategory = true - } - // TODO: Exit out early per intent filter browsable category. - if (category.attribute(MANIFEST_NAME_KEY) == "android.intent.category.BROWSABLE") { - intentFilterCheck.hasBrowsableCategory = - true - } - } - val dataItems: List = - appLinkIntent.children().filterIsInstance().filter { item -> - item.name() == "data" - } - dataItems.forEach { data -> - data.attributes().forEach { entry -> - when (entry.key) { - "android:scheme" -> schemes.add(entry.value.toString()) - "android:host" -> hosts.add(entry.value.toString()) - // All path patterns add to paths. - "android:pathAdvancedPattern" -> - paths.add( - entry.value.toString() - ) - - "android:pathPattern" -> paths.add(entry.value.toString()) - "android:path" -> paths.add(entry.value.toString()) - "android:pathPrefix" -> paths.add(entry.value.toString() + ".*") - "android:pathSuffix" -> paths.add(".*" + entry.value.toString()) - } - } - } - if (hosts.isNotEmpty() || paths.isNotEmpty()) { - if (schemes.isEmpty()) { - schemes.add(null) - } - if (hosts.isEmpty()) { - hosts.add(null) - } - if (paths.isEmpty()) { - paths.add(".*") - } - // Sets are not ordered this could produce a bug. - schemes.forEach { scheme -> - hosts.forEach { host -> - paths.forEach { path -> - appLinkSettings.deeplinks.add( - Deeplink( - scheme, - host, - path, - intentFilterCheck - ) - ) - } - } - } - } - } + // This task does not modify the manifest despite using an api + // designed for modification. The task is responsible for an exact copy of the input + // manifest being used for the output manifest. + variant.artifacts + .use(manifestUpdater) + .wiredWithFiles( + DeepLinkJsonFromManifestTask::manifestFile, + DeepLinkJsonFromManifestTask::updatedManifest + ).toTransform(SingleArtifact.MERGED_MANIFEST) // (3) Indicate the artifact and operation type. } - return appLinkSettings } } diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/DeepLinkJsonFromManifestTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/DeepLinkJsonFromManifestTask.kt new file mode 100644 index 0000000000000..364371cf40657 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/DeepLinkJsonFromManifestTask.kt @@ -0,0 +1,47 @@ +// 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.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction + +/** + * Create a json file of deeplink settings from an AndroidManifest. + * + * This task does not modify the manifest despite using an api + * designed for modification. The task is responsible for an exact copy of the input + * manifest being used for the output manifest. +*/ +abstract class DeepLinkJsonFromManifestTask : DefaultTask() { + // Input property to receive the manifest file + @get:InputFile + abstract val manifestFile: RegularFileProperty + + // In the past for this task namespace was the ApplicationId. + @get:Input + abstract val namespace: Property + + // Does not need to transform manifest at all but there does not appear to be another dsl + // supported way to depend on the merged manifest. + @get:OutputFile + abstract val updatedManifest: RegularFileProperty + + @get:OutputFile + abstract val deepLinkJson: RegularFileProperty + + @TaskAction + fun processManifest() { + manifestFile.get().asFile.copyTo(updatedManifest.get().asFile, overwrite = true) + logger.debug("DeepLinkJsonFromManifestTask: Unmodified manifest written.") + + DeepLinkJsonFromManifestTaskHelper.createAppLinkSettingsFile(namespace.get(), manifestFile, deepLinkJson) + logger.debug("DeepLinkJsonFromManifestTask: appLinkSettings written to ${deepLinkJson.get().asFile.absolutePath}.") + } +} diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/DeepLinkJsonFromManifestTaskHelper.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/DeepLinkJsonFromManifestTaskHelper.kt new file mode 100644 index 0000000000000..618b42f94a884 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/DeepLinkJsonFromManifestTaskHelper.kt @@ -0,0 +1,184 @@ +// 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 androidx.annotation.VisibleForTesting +import com.flutter.gradle.AppLinkSettings +import com.flutter.gradle.Deeplink +import com.flutter.gradle.IntentFilterCheck +import groovy.util.Node +import org.gradle.api.file.RegularFileProperty +import java.io.File +import kotlin.collections.forEach +import kotlin.io.writeText + +/** + * Stateless object to contain the logic used in [FlutterTask]. Any required state should be stored + * on [FlutterTask] instead, while any logic needed by [FlutterTask] should be added here. + */ +object DeepLinkJsonFromManifestTaskHelper { + private const val MANIFEST_NAME_KEY = "android:name" + private const val MANIFEST_VALUE_KEY = "android:value" + private const val MANIFEST_VALUE_TRUE = "true" + + /** + * Creates a jsonfile with deeplink information from the Android manifest file. + * + * + * An example json: + * { + * applicationId: "com.example.app", + * deeplinks: [ + * {"scheme":"http", "host":"example.com", "path":".*"}, + * {"scheme":"https","host":"example.com","path":".*"} + * ] + * } + */ + public fun createAppLinkSettingsFile( + applicationId: String, + manifestFile: RegularFileProperty, + deepLinkJson: RegularFileProperty + ) { + val appLinkSettings = createAppLinkSettings(applicationId, manifestFile.get().asFile) + deepLinkJson.get().asFile.writeText(appLinkSettings.toJson().toString()) + } + + /** + * Extracts app deeplink information from the Android manifest file then returns + * an AppLinkSettings object. + * + * @param applicationId The application ID or the namespace of the variant. + * @param manifest the Android manifest to be parsed. + */ + @VisibleForTesting + fun createAppLinkSettings( + applicationId: String, + manifestFile: File + ): AppLinkSettings { + val appLinkSettings = AppLinkSettings(applicationId) + val manifest: Node = + groovy.xml + .XmlParser(false, false) + .parse(manifestFile) + val applicationNode: Node? = + manifest.children().find { node -> + node is Node && node.name() == "application" + } as Node? + if (applicationNode == null) { + return appLinkSettings + } + val activities: List = + applicationNode.children().filterIsInstance().filter { item -> + item.name() == "activity" + } + + activities.forEach { activity -> + val metaDataItems: List = + activity.children().filterIsInstance().filter { metaItem -> + metaItem.name() == "meta-data" + } + metaDataItems.forEach { metaDataItem -> + val nameAttribute: Boolean = + metaDataItem.attribute(MANIFEST_NAME_KEY) == "flutter_deeplinking_enabled" + val valueAttribute: Boolean = + metaDataItem.attribute(MANIFEST_VALUE_KEY) == MANIFEST_VALUE_TRUE + if (nameAttribute && valueAttribute) { + appLinkSettings.deeplinkingFlagEnabled = true + } + } + val intentFilterItems: List = + activity.children().filterIsInstance().filter { filterItem -> + filterItem.name() == "intent-filter" + } + intentFilterItems.forEach { appLinkIntent -> + // Print out the host attributes in data tags. + val schemes: MutableSet = mutableSetOf() + val hosts: MutableSet = mutableSetOf() + val paths: MutableSet = mutableSetOf() + val intentFilterCheck = IntentFilterCheck() + if (appLinkIntent.attribute("android:autoVerify") == MANIFEST_VALUE_TRUE) { + intentFilterCheck.hasAutoVerify = true + } + + val actionItems: List = + appLinkIntent.children().filterIsInstance().filter { item -> + item.name() == "action" + } + // Any action item causes intentFilterCheck to always be true + // and we keep looping instead of exiting out early. + // TODO: Exit out early per intent filter action view. + actionItems.forEach { action -> + if (action.attribute(MANIFEST_NAME_KEY) == "android.intent.action.VIEW") { + intentFilterCheck.hasActionView = true + } + } + val categoryItems: List = + appLinkIntent.children().filterIsInstance().filter { item -> + item.name() == "category" + } + categoryItems.forEach { category -> + // TODO: Exit out early per intent filter default category. + if (category.attribute(MANIFEST_NAME_KEY) == "android.intent.category.DEFAULT") { + intentFilterCheck.hasDefaultCategory = true + } + // TODO: Exit out early per intent filter browsable category. + if (category.attribute(MANIFEST_NAME_KEY) == "android.intent.category.BROWSABLE") { + intentFilterCheck.hasBrowsableCategory = + true + } + } + val dataItems: List = + appLinkIntent.children().filterIsInstance().filter { item -> + item.name() == "data" + } + dataItems.forEach { data -> + data.attributes().forEach { entry -> + when (entry.key) { + "android:scheme" -> schemes.add(entry.value.toString()) + "android:host" -> hosts.add(entry.value.toString()) + // All path patterns add to paths. + "android:pathAdvancedPattern" -> + paths.add( + entry.value.toString() + ) + + "android:pathPattern" -> paths.add(entry.value.toString()) + "android:path" -> paths.add(entry.value.toString()) + "android:pathPrefix" -> paths.add(entry.value.toString() + ".*") + "android:pathSuffix" -> paths.add(".*" + entry.value.toString()) + } + } + } + if (hosts.isNotEmpty() || paths.isNotEmpty()) { + if (schemes.isEmpty()) { + schemes.add(null) + } + if (hosts.isEmpty()) { + hosts.add(null) + } + if (paths.isEmpty()) { + paths.add(".*") + } + // Sets are not ordered this could produce a bug. + schemes.forEach { scheme -> + hosts.forEach { host -> + paths.forEach { path -> + appLinkSettings.deeplinks.add( + Deeplink( + scheme, + host, + path, + intentFilterCheck + ) + ) + } + } + } + } + } + } + return appLinkSettings + } +} diff --git a/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTask.kt b/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTask.kt index 86e5ad0bb4735..b017cc941cb3f 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTask.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/tasks/FlutterTask.kt @@ -25,10 +25,12 @@ abstract class FlutterTask : BaseFlutterTask() { val outputDirectory: File? get() = FlutterTaskHelper.getOutputDirectory(flutterTask = this) + // Warning assetsDirectory assets appear to return different contents. @get:Internal val assetsDirectory: String get() = FlutterTaskHelper.getAssetsDirectory(flutterTask = this) + // Warning assetsDirectory assets appear to return different contents. @get:Internal val assets: CopySpec get() = FlutterTaskHelper.getAssets(project, flutterTask = this) diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt index 19d324b074240..146c7b89628a4 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt @@ -2,6 +2,7 @@ package com.flutter.gradle import com.android.build.api.dsl.ApplicationDefaultConfig import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.gradle.AbstractAppExtension import com.android.build.gradle.BaseExtension import com.android.build.gradle.api.AndroidSourceDirectorySet @@ -48,6 +49,12 @@ class FlutterPluginTest { val project = mockk(relaxed = true) val mockAbstractAppExtension = mockk(relaxed = true) every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension + val mockAndroidComponentsExtension = mockk>(relaxed = true) + every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension + every { mockAndroidComponentsExtension.selector() } returns + mockk { + every { all() } returns mockk() + } every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension every { project.extensions.findByName("android") } returns mockAbstractAppExtension every { project.projectDir } returns projectDir.toFile() @@ -106,6 +113,12 @@ class FlutterPluginTest { every { project.extensions.findByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension every { project.extensions.getByType(AbstractAppExtension::class.java) } returns mockAbstractAppExtension every { project.extensions.findByName("android") } returns mockAbstractAppExtension + val mockAndroidComponentsExtension = mockk>(relaxed = true) + every { project.extensions.getByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension + every { mockAndroidComponentsExtension.selector() } returns + mockk { + every { all() } returns mockk() + } every { project.projectDir } returns projectDir.toFile() every { project.findProperty("flutter.sdk") } returns fakeFlutterSdkDir.toString() every { project.file(fakeFlutterSdkDir.toString()) } returns fakeFlutterSdkDir.toFile() diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt index bf69fdb964b5b..a6490885d92d5 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -6,11 +6,8 @@ package com.flutter.gradle import com.android.build.gradle.AbstractAppExtension import com.android.build.gradle.BaseExtension -import com.android.build.gradle.api.ApplicationVariant -import com.android.build.gradle.api.BaseVariantOutput import com.android.build.gradle.internal.dsl.CmakeOptions import com.android.build.gradle.internal.dsl.DefaultConfig -import com.android.build.gradle.tasks.ProcessAndroidResources import com.android.builder.model.BuildType import com.flutter.gradle.plugins.PluginHandler import io.mockk.called @@ -20,8 +17,6 @@ import io.mockk.mockkObject import io.mockk.slot import io.mockk.verify import org.gradle.api.Action -import org.gradle.api.DomainObjectCollection -import org.gradle.api.DomainObjectSet import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task @@ -29,8 +24,6 @@ import org.gradle.api.UnknownTaskException import org.gradle.api.file.Directory import org.gradle.api.file.DirectoryProperty import org.gradle.api.logging.Logger -import org.gradle.api.tasks.TaskContainer -import org.gradle.api.tasks.TaskProvider import org.jetbrains.kotlin.gradle.plugin.extraProperties import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.io.TempDir @@ -1088,156 +1081,4 @@ class FlutterPluginUtilsTest { mockTask.description = "Prints out all build variants for this Android project" } } - - @Test - fun addTasksForOutputsAppLinkSettingsActual( - @TempDir tempDir: Path - ) { - val variants: MutableList = mutableListOf() - val registerTaskList = mutableListOf() - val descriptionSlot = slot() - // vars so variables can be overridden below. - var mockLogger = mockk() - var variantWithLinks = mockk() - - val mockProject = - mockk { - every { logger } returns - mockk { - mockLogger = this - every { info(any()) } returns Unit - every { warn(any()) } returns Unit - } - every { extensions.findByType(AbstractAppExtension::class.java) } returns - mockk { - val variant1 = - mockk { - every { name } returns "one" - every { applicationId } returns "com.example.FlutterActivity1" - } - variants.add(variant1) - mockk { - variantWithLinks = this - every { name } returns "two" - every { applicationId } returns "com.example.FlutterActivity2" - } - variants.add(variantWithLinks) - // Capture the "action" that needs to be run for each variant. - val actionSlot = slot>() - every { applicationVariants } returns - mockk> { - every { configureEach(capture(actionSlot)) } answers { - // Execute the action for each variant. - variants.forEach { variant -> - actionSlot.captured.execute(variant) - } - } - } - } - - val registerTaskSlot = slot>() - every { tasks } returns - mockk { - val registerTaskNameSlot = slot() - every { - register( - capture(registerTaskNameSlot), - capture(registerTaskSlot) - ) - } answers registerAnswer@{ - val mockRegisterTask = - mockk { - every { name } returns registerTaskNameSlot.captured - every { - description = capture(descriptionSlot) - } returns Unit - every { dependsOn(any()) } returns mockk() - val doLastActionSlot = slot>() - every { doLast(capture(doLastActionSlot)) } answers doLastAnswer@{ - // We need to capture the task as well - doLastActionSlot.captured.execute(mockk()) - return@doLastAnswer mockk() - } - } - registerTaskList.add(mockRegisterTask) - registerTaskSlot.captured.execute(mockRegisterTask) - return@registerAnswer mockk() - } - - every { named(any()) } returns - mockk { - every { configure(any>()) } returns mockk() - } - } - } - - variants.forEach { variant -> - val testOutputs: DomainObjectCollection = - mockk>() - val baseVariantSlot = slot>() - val baseVariantOutput = mockk() - // Create a real file in a temp directory. - val manifest = - tempDir - .resolve("${tempDir.toAbsolutePath()}/AndroidManifest.xml") - .toFile() - manifest.writeText(manifestText) - val mockProcessResourcesProvider = mockk>() - val mockProcessResources = mockk() - every { - mockProcessResourcesProvider.hint(ProcessAndroidResources::class).get() - } returns mockProcessResources - every { baseVariantOutput.processResourcesProvider } returns mockProcessResourcesProvider - // Fallback processing. - every { mockProcessResources.manifestFile } returns manifest - - every { testOutputs.configureEach(capture(baseVariantSlot)) } answers { - // Execute the action for each output. - baseVariantSlot.captured.execute(baseVariantOutput) - } - every { variant.outputs } returns testOutputs - } - val outputFile = - tempDir - .resolve("${tempDir.toAbsolutePath()}/app-link-settings-build-variant.json") - .toFile() - every { mockProject.property("outputPath") } returns outputFile - - FlutterPluginUtils.addTasksForOutputsAppLinkSettings(mockProject) - - verify(exactly = 0) { mockLogger.info(any()) } - assert(descriptionSlot.captured.contains("stores app links settings for the given build variant")) - assertEquals(variants.size, registerTaskList.size) - for (i in 0 until variants.size) { - assertEquals( - "output${FlutterPluginUtils.capitalize(variants[i].name)}AppLinkSettings", - registerTaskList[i].name - ) - verify(exactly = 1) { registerTaskList[i].dependsOn(any()) } - } - // Output assertions are minimal which ensures code is running but is not exhaustive testing. - // Integration test for more exhaustive behavior is defined in - // flutter/flutter/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart - val outputFileText = outputFile.readText() - // Only variant2 since that one has app links. - assertContains(outputFileText, variantWithLinks.applicationId) - // Host. - assertContains(outputFileText, "deeplink.flutter.dev") - // pathPrefix used in variant2 combined with prefix logic. - assertContains(outputFileText, "some.prefix.*") - // Deep linking - assertContains(outputFileText, "deeplinkingFlagEnabled\":true") - } - - @Test - fun addTasksForOutputsAppLinkSettingsNoAndroid() { - val mockProject = mockk() - val mockLogger = mockk() - every { mockProject.logger } returns mockLogger - every { mockLogger.info(any()) } returns Unit - every { mockProject.extensions.findByType(AbstractAppExtension::class.java) } returns null - - FlutterPluginUtils.addTasksForOutputsAppLinkSettings(mockProject) - verify(exactly = 1) { mockLogger.info("addTasksForOutputsAppLinkSettings called on project without android extension.") } - } } diff --git a/packages/flutter_tools/gradle/src/test/kotlin/tasks/DeepLinkJsonFromManifestTaskTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/tasks/DeepLinkJsonFromManifestTaskTest.kt new file mode 100644 index 0000000000000..7ec0fa232dcb7 --- /dev/null +++ b/packages/flutter_tools/gradle/src/test/kotlin/tasks/DeepLinkJsonFromManifestTaskTest.kt @@ -0,0 +1,336 @@ +// 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 io.mockk.every +import io.mockk.mockk +import org.gradle.api.file.RegularFileProperty +import org.xml.sax.SAXParseException +import java.io.File +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Tests for [DeepLinkJsonFromManifestTaskHelper]. + * + * Parsing tests for corner cases are in + * `flutter/packages/flutter_tools/test/integration.shard/android_gradle_outputs_app_link_settings_test.dart`. + * Json tests are in `flutter/packages/flutter_tools/gradle/src/test/kotlin/AppLinkSettingsTest.kt` and + * `flutter/packages/flutter_tools/gradle/src/test/kotlin/DeeplinkTest.kt`. + * + * Tests here are focused on malformed manifest behavior and that there are some tests that cover + * reading from files. The contents of DeepLinks should not be tested in this file. + */ +class DeepLinkJsonFromManifestTaskTest { + 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 + } + + @Test + fun createAppLinkSettingsFileCreation() { + val scheme = "http" + val host = "example.com" + val pathPrefix = "/profile" + val manifestContent = """ + + + + + + + + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val manifest = mockk() + every { manifest.get().asFile } returns manifestFile + + val jsonFile = File.createTempFile("deeplink", ".json") + jsonFile.deleteOnExit() + val json = mockk() + every { json.get().asFile } returns jsonFile + + DeepLinkJsonFromManifestTaskHelper.createAppLinkSettingsFile(defaultNamespace, manifest, json) + assertEquals( + DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile).toJson().toString(), + jsonFile.readText() + ) + } + + @Test + fun noApplicationInManifest() { + val manifestContent = """ + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + + assertEquals(defaultNamespace, appLinkSettings.applicationId) + assertFalse(appLinkSettings.deeplinkingFlagEnabled) + assertTrue(appLinkSettings.deeplinks.isEmpty()) + } + + @Test + fun applicationNoDeepLinkingElements() { + val manifestContent = """ + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + + assertEquals(defaultNamespace, appLinkSettings.applicationId) + assertFalse(appLinkSettings.deeplinkingFlagEnabled) + assertTrue(appLinkSettings.deeplinks.isEmpty()) + } + + @Test + fun metaDataDeepLinkingEnabledTrue() { + val manifestContent = """ + + + + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + + assertEquals(defaultNamespace, appLinkSettings.applicationId) + assertTrue(appLinkSettings.deeplinkingFlagEnabled) + assertTrue(appLinkSettings.deeplinks.isEmpty()) + } + + @Test + fun metaDataDeepLinkingEnabledFalse() { + val manifestContent = """ + + + + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + + assertEquals(defaultNamespace, appLinkSettings.applicationId) + assertFalse(appLinkSettings.deeplinkingFlagEnabled) + assertTrue(appLinkSettings.deeplinks.isEmpty()) + } + + @Test + fun metaDataDeepLinkingEnabledInvalidValue() { + val manifestContent = """ + + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + + assertEquals(defaultNamespace, appLinkSettings.applicationId) + assertFalse(appLinkSettings.deeplinkingFlagEnabled, "Should default to false for invalid meta-data value") + assertTrue(appLinkSettings.deeplinks.isEmpty()) + } + + @Test + fun metaDataDeepLinkingNoValue() { + val manifestContent = """ + + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + + assertEquals(defaultNamespace, appLinkSettings.applicationId) + assertFalse(appLinkSettings.deeplinkingFlagEnabled, "Should default to false if meta-data value is missing") + assertTrue(appLinkSettings.deeplinks.isEmpty()) + } + + @Test + fun basicDeepLink() { + val scheme = "http" + val host = "example.com" + val pathPrefix = "/profile" + val manifestContent = """ + + + + + + + + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + + assertEquals(defaultNamespace, appLinkSettings.applicationId) + assertTrue(appLinkSettings.deeplinkingFlagEnabled) + assertEquals(1, appLinkSettings.deeplinks.size) + } + + @Test + fun deepLinkWithAutoVerify() { + val manifestContent = """ + + + + + + + + + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + assertEquals(1, appLinkSettings.deeplinks.size) + } + + @Test + fun multipleIntentFilters() { + val manifestContent = """ + + + + + + + + + + + + + + + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + assertEquals(2, appLinkSettings.deeplinks.size) + } + + @Test + fun multipleActivitiesWithDeepLinks() { + val manifestContent = """ + + + + + + + + + + + + + + + + + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + assertEquals(2, appLinkSettings.deeplinks.size) + } + + @Test + fun intentFilterMissingHostInData() { + val manifestContent = """ + + + + + + + + + + + + + + """ + val manifestFile = createTempManifestFile(manifestContent) + val appLinkSettings = DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + assertTrue(appLinkSettings.deeplinks.isEmpty(), "Intent filter with data missing host should be ignored") + } + + @Test + fun malformedManifestXML() { + val manifestFile = createTempManifestFile("") // Malformed XML + assertFailsWith { + DeepLinkJsonFromManifestTaskHelper.createAppLinkSettings(defaultNamespace, manifestFile) + } + } +} From aaef1e5cf1f2973c2b5a2a341944525ec5f0dcac Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Mon, 18 Aug 2025 15:56:28 -0700 Subject: [PATCH 109/720] Add "team-ios" label to iOS team triage query (#173997) It turns out folks have been putting `team-ios` on PRs they want the iOS team to review. Adding that label to the query: https://github.com/flutter/flutter/pulls?q=is%3Aopen+is%3Apr+label%3Aplatform-ios%2Cteam-ios+sort%3Acreated-asc+-is%3Adraft ## 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 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]. **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 [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/triage/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/triage/README.md b/docs/triage/README.md index 59d56cdfccfc2..e7a9f241d5ef9 100644 --- a/docs/triage/README.md +++ b/docs/triage/README.md @@ -271,7 +271,7 @@ See the [Flutter Infra Team Triage](./Infra-Triage.md) page. PRs are reviewed weekly across the framework, packages, and engine repositories: -- [iOS PRs on the framework](https://github.com/flutter/flutter/pulls?q=is%3Aopen+is%3Apr+label%3Aplatform-ios+sort%3Acreated-asc+-is%3Adraft) +- [iOS PRs on the framework](https://github.com/flutter/flutter/pulls?q=is%3Aopen+is%3Apr+label%3Aplatform-ios%2Cteam-ios+sort%3Acreated-asc+-is%3Adraft) - [macOS PRs on the framework](https://github.com/flutter/flutter/pulls?q=is%3Aopen+is%3Apr+label%3A%22a%3A+desktop%22+label%3Aplatform-macos++sort%3Aupdated-asc) - [iOS and macOS PRs on packages](https://github.com/flutter/packages/pulls?q=is%3Aopen+is%3Apr+label%3Atriage-macos%2Ctriage-ios+sort%3Aupdated-asc+) From 946f15a72ed2cdee29aa5716d9c70e36b4e41e3e Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Mon, 18 Aug 2025 15:58:24 -0700 Subject: [PATCH 110/720] Add `open_jdk` to `Linux analyze` (#173988) Towards https://github.com/flutter/flutter/issues/173986. This build _never_ declared (either directly or through `platform_properties`) that it required the Java JDK, and appears to have been "accidentally" getting it installed via a (now expired) cache of the Java JDK. --- .ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci.yaml b/.ci.yaml index 36452b656e556..dc71698519041 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -348,7 +348,8 @@ targets: shard: analyze dependencies: >- [ - {"dependency": "ktlint", "version": "version_1_5_0"} + {"dependency": "ktlint", "version": "version_1_5_0"}, + {"dependency": "open_jdk", "version": "version:21"} ] tags: > ["framework","hostonly","shard","linux"] From e65380a220766cfee8830e7dbc7d7528172c2819 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 18 Aug 2025 21:29:27 -0400 Subject: [PATCH 111/720] Roll Dart SDK from 502455ee300b to 9105d946af95 (3 revisions) (#174002) https://dart.googlesource.com/sdk.git/+log/502455ee300b..9105d946af95 2025-08-18 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-110.0.dev 2025-08-18 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-109.0.dev 2025-08-18 dart-internal-merge@dart-ci-internal.iam.gserviceaccount.com Version 3.10.0-108.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 chinmaygarde@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 fe75d1a0a234d..8f7df89a1303c 100644 --- a/DEPS +++ b/DEPS @@ -56,7 +56,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '502455ee300bf31bbf61eb2d7ccd5c139af492b6', + 'dart_revision': '9105d946af95d2eec02bff363e14b9285dddcf76', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py @@ -72,7 +72,7 @@ vars = { 'dart_perfetto_rev': '13ce0c9e13b0940d2476cd0cff2301708a9a2e2b', 'dart_protobuf_gn_rev': 'ca669f79945418f6229e4fef89b666b2a88cbb10', 'dart_protobuf_rev': '6e9c9f4637bc0db8a855c7b26e8f87a2279307cc', - 'dart_pub_rev': '619db737b4aba0a43beaf16ffa141ee70d7bbd9e', + 'dart_pub_rev': '469eb6193c0a49495ea2ce7432cf749f077ad596', 'dart_sync_http_rev': 'c07f96f89a7eec7e3daac641fa6c587224fcfbaa', 'dart_tools_rev': '1b52e89e0b4ef70e004383c1cf781ad4182f380b', 'dart_vector_math_rev': '3939545edc38ed657381381d33acde02c49ff827', From c3c93ffea915bba2d534fe7bd4bbf2cca8291c9e Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Tue, 19 Aug 2025 11:34:33 -0400 Subject: [PATCH 112/720] Loic pull request feedback for win32 --- .../multi_window_ref_app/.vscode/launch.json | 35 ---- .../.gitignore | 0 .../.metadata | 0 .../README.md | 4 +- .../analysis_options.yaml | 0 .../lib/app/main_window.dart | 0 .../lib/app/regular_window_content.dart | 0 .../lib/app/regular_window_edit_dialog.dart | 0 .../lib/app/window_controller_render.dart | 0 .../lib/app/window_manager_model.dart | 0 .../lib/app/window_settings.dart | 0 .../lib/app/window_settings_dialog.dart | 0 .../lib/main.dart | 0 .../pubspec.yaml | 2 +- .../test/widget_test.dart | 0 .../windows/.gitignore | 0 .../windows/CMakeLists.txt | 4 +- .../windows/flutter/CMakeLists.txt | 0 .../windows/runner/CMakeLists.txt | 0 .../windows/runner/Runner.rc | 2 +- .../windows/runner/main.cpp | 0 .../windows/runner/resource.h | 0 .../windows/runner/runner.exe.manifest | 0 .../windows/runner/utils.cpp | 0 .../windows/runner/utils.h | 0 .../flutter/lib/src/foundation/_features.dart | 3 + .../lib/src/widgets/_window_win32.dart | 190 +++++++++++++----- 27 files changed, 145 insertions(+), 95 deletions(-) delete mode 100644 examples/multi_window_ref_app/.vscode/launch.json rename examples/{multi_window_ref_app => multiple_windows}/.gitignore (100%) rename examples/{multi_window_ref_app => multiple_windows}/.metadata (100%) rename examples/{multi_window_ref_app => multiple_windows}/README.md (82%) rename examples/{multi_window_ref_app => multiple_windows}/analysis_options.yaml (100%) rename examples/{multi_window_ref_app => multiple_windows}/lib/app/main_window.dart (100%) rename examples/{multi_window_ref_app => multiple_windows}/lib/app/regular_window_content.dart (100%) rename examples/{multi_window_ref_app => multiple_windows}/lib/app/regular_window_edit_dialog.dart (100%) rename examples/{multi_window_ref_app => multiple_windows}/lib/app/window_controller_render.dart (100%) rename examples/{multi_window_ref_app => multiple_windows}/lib/app/window_manager_model.dart (100%) rename examples/{multi_window_ref_app => multiple_windows}/lib/app/window_settings.dart (100%) rename examples/{multi_window_ref_app => multiple_windows}/lib/app/window_settings_dialog.dart (100%) rename examples/{multi_window_ref_app => multiple_windows}/lib/main.dart (100%) rename examples/{multi_window_ref_app => multiple_windows}/pubspec.yaml (99%) rename examples/{multi_window_ref_app => multiple_windows}/test/widget_test.dart (100%) rename examples/{multi_window_ref_app => multiple_windows}/windows/.gitignore (100%) rename examples/{multi_window_ref_app => multiple_windows}/windows/CMakeLists.txt (97%) rename examples/{multi_window_ref_app => multiple_windows}/windows/flutter/CMakeLists.txt (100%) rename examples/{multi_window_ref_app => multiple_windows}/windows/runner/CMakeLists.txt (100%) rename examples/{multi_window_ref_app => multiple_windows}/windows/runner/Runner.rc (97%) rename examples/{multi_window_ref_app => multiple_windows}/windows/runner/main.cpp (100%) rename examples/{multi_window_ref_app => multiple_windows}/windows/runner/resource.h (100%) rename examples/{multi_window_ref_app => multiple_windows}/windows/runner/runner.exe.manifest (100%) rename examples/{multi_window_ref_app => multiple_windows}/windows/runner/utils.cpp (100%) rename examples/{multi_window_ref_app => multiple_windows}/windows/runner/utils.h (100%) diff --git a/examples/multi_window_ref_app/.vscode/launch.json b/examples/multi_window_ref_app/.vscode/launch.json deleted file mode 100644 index c846434f923e5..0000000000000 --- a/examples/multi_window_ref_app/.vscode/launch.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "multi_window_ref_app", - "request": "launch", - "type": "dart", - "program": "lib/main.dart", - "toolArgs": [ - "--local-engine=host_debug_unopt_arm64", - "--local-engine-host=host_debug_unopt_arm64", - ] - }, - { - "name": "multi_window_ref_app (profile mode)", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "multi_window_ref_app (release mode)", - "request": "launch", - "type": "dart", - "flutterMode": "release" - }, - { - "name": "(Windows) Attach", - "type": "cppvsdbg", - "request": "attach", - } - ] -} \ No newline at end of file diff --git a/examples/multi_window_ref_app/.gitignore b/examples/multiple_windows/.gitignore similarity index 100% rename from examples/multi_window_ref_app/.gitignore rename to examples/multiple_windows/.gitignore diff --git a/examples/multi_window_ref_app/.metadata b/examples/multiple_windows/.metadata similarity index 100% rename from examples/multi_window_ref_app/.metadata rename to examples/multiple_windows/.metadata diff --git a/examples/multi_window_ref_app/README.md b/examples/multiple_windows/README.md similarity index 82% rename from examples/multi_window_ref_app/README.md rename to examples/multiple_windows/README.md index 39b39942c528b..249940fb58188 100644 --- a/examples/multi_window_ref_app/README.md +++ b/examples/multiple_windows/README.md @@ -1,5 +1,5 @@ -# multi_window_ref_app +# multiple_windows A reference application demonstrating multi-window support for Flutter using a rich semantics windowing API. At the moment, only the Windows platform is -supported. \ No newline at end of file +supported. diff --git a/examples/multi_window_ref_app/analysis_options.yaml b/examples/multiple_windows/analysis_options.yaml similarity index 100% rename from examples/multi_window_ref_app/analysis_options.yaml rename to examples/multiple_windows/analysis_options.yaml diff --git a/examples/multi_window_ref_app/lib/app/main_window.dart b/examples/multiple_windows/lib/app/main_window.dart similarity index 100% rename from examples/multi_window_ref_app/lib/app/main_window.dart rename to examples/multiple_windows/lib/app/main_window.dart diff --git a/examples/multi_window_ref_app/lib/app/regular_window_content.dart b/examples/multiple_windows/lib/app/regular_window_content.dart similarity index 100% rename from examples/multi_window_ref_app/lib/app/regular_window_content.dart rename to examples/multiple_windows/lib/app/regular_window_content.dart diff --git a/examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart b/examples/multiple_windows/lib/app/regular_window_edit_dialog.dart similarity index 100% rename from examples/multi_window_ref_app/lib/app/regular_window_edit_dialog.dart rename to examples/multiple_windows/lib/app/regular_window_edit_dialog.dart diff --git a/examples/multi_window_ref_app/lib/app/window_controller_render.dart b/examples/multiple_windows/lib/app/window_controller_render.dart similarity index 100% rename from examples/multi_window_ref_app/lib/app/window_controller_render.dart rename to examples/multiple_windows/lib/app/window_controller_render.dart diff --git a/examples/multi_window_ref_app/lib/app/window_manager_model.dart b/examples/multiple_windows/lib/app/window_manager_model.dart similarity index 100% rename from examples/multi_window_ref_app/lib/app/window_manager_model.dart rename to examples/multiple_windows/lib/app/window_manager_model.dart diff --git a/examples/multi_window_ref_app/lib/app/window_settings.dart b/examples/multiple_windows/lib/app/window_settings.dart similarity index 100% rename from examples/multi_window_ref_app/lib/app/window_settings.dart rename to examples/multiple_windows/lib/app/window_settings.dart diff --git a/examples/multi_window_ref_app/lib/app/window_settings_dialog.dart b/examples/multiple_windows/lib/app/window_settings_dialog.dart similarity index 100% rename from examples/multi_window_ref_app/lib/app/window_settings_dialog.dart rename to examples/multiple_windows/lib/app/window_settings_dialog.dart diff --git a/examples/multi_window_ref_app/lib/main.dart b/examples/multiple_windows/lib/main.dart similarity index 100% rename from examples/multi_window_ref_app/lib/main.dart rename to examples/multiple_windows/lib/main.dart diff --git a/examples/multi_window_ref_app/pubspec.yaml b/examples/multiple_windows/pubspec.yaml similarity index 99% rename from examples/multi_window_ref_app/pubspec.yaml rename to examples/multiple_windows/pubspec.yaml index 8a5a56d7df7a4..89ab2c55a4d11 100644 --- a/examples/multi_window_ref_app/pubspec.yaml +++ b/examples/multiple_windows/pubspec.yaml @@ -1,4 +1,4 @@ -name: multi_window_ref_app +name: multiple_windows description: "Reference app for the multi-view windowing API." version: 1.0.0+1 diff --git a/examples/multi_window_ref_app/test/widget_test.dart b/examples/multiple_windows/test/widget_test.dart similarity index 100% rename from examples/multi_window_ref_app/test/widget_test.dart rename to examples/multiple_windows/test/widget_test.dart diff --git a/examples/multi_window_ref_app/windows/.gitignore b/examples/multiple_windows/windows/.gitignore similarity index 100% rename from examples/multi_window_ref_app/windows/.gitignore rename to examples/multiple_windows/windows/.gitignore diff --git a/examples/multi_window_ref_app/windows/CMakeLists.txt b/examples/multiple_windows/windows/CMakeLists.txt similarity index 97% rename from examples/multi_window_ref_app/windows/CMakeLists.txt rename to examples/multiple_windows/windows/CMakeLists.txt index 4450980427578..a3413e93bae62 100644 --- a/examples/multi_window_ref_app/windows/CMakeLists.txt +++ b/examples/multiple_windows/windows/CMakeLists.txt @@ -1,10 +1,10 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(multi_window_ref_app LANGUAGES CXX) +project(multiple_windows LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "multi_window_ref_app") +set(BINARY_NAME "multiple_windows") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/examples/multi_window_ref_app/windows/flutter/CMakeLists.txt b/examples/multiple_windows/windows/flutter/CMakeLists.txt similarity index 100% rename from examples/multi_window_ref_app/windows/flutter/CMakeLists.txt rename to examples/multiple_windows/windows/flutter/CMakeLists.txt diff --git a/examples/multi_window_ref_app/windows/runner/CMakeLists.txt b/examples/multiple_windows/windows/runner/CMakeLists.txt similarity index 100% rename from examples/multi_window_ref_app/windows/runner/CMakeLists.txt rename to examples/multiple_windows/windows/runner/CMakeLists.txt diff --git a/examples/multi_window_ref_app/windows/runner/Runner.rc b/examples/multiple_windows/windows/runner/Runner.rc similarity index 97% rename from examples/multi_window_ref_app/windows/runner/Runner.rc rename to examples/multiple_windows/windows/runner/Runner.rc index 909820ff45c09..d9d3832510220 100644 --- a/examples/multi_window_ref_app/windows/runner/Runner.rc +++ b/examples/multiple_windows/windows/runner/Runner.rc @@ -84,7 +84,7 @@ BEGIN VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "Flutter Multi-Window Reference App" "\0" VALUE "LegalCopyright", "Copyright 2014 The Flutter Authors. All rights reserved." "\0" - VALUE "OriginalFilename", "multi_window_ref_app.exe" "\0" + VALUE "OriginalFilename", "multiple_windows.exe" "\0" VALUE "ProductName", "Flutter Multi-Window Reference App" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END diff --git a/examples/multi_window_ref_app/windows/runner/main.cpp b/examples/multiple_windows/windows/runner/main.cpp similarity index 100% rename from examples/multi_window_ref_app/windows/runner/main.cpp rename to examples/multiple_windows/windows/runner/main.cpp diff --git a/examples/multi_window_ref_app/windows/runner/resource.h b/examples/multiple_windows/windows/runner/resource.h similarity index 100% rename from examples/multi_window_ref_app/windows/runner/resource.h rename to examples/multiple_windows/windows/runner/resource.h diff --git a/examples/multi_window_ref_app/windows/runner/runner.exe.manifest b/examples/multiple_windows/windows/runner/runner.exe.manifest similarity index 100% rename from examples/multi_window_ref_app/windows/runner/runner.exe.manifest rename to examples/multiple_windows/windows/runner/runner.exe.manifest diff --git a/examples/multi_window_ref_app/windows/runner/utils.cpp b/examples/multiple_windows/windows/runner/utils.cpp similarity index 100% rename from examples/multi_window_ref_app/windows/runner/utils.cpp rename to examples/multiple_windows/windows/runner/utils.cpp diff --git a/examples/multi_window_ref_app/windows/runner/utils.h b/examples/multiple_windows/windows/runner/utils.h similarity index 100% rename from examples/multi_window_ref_app/windows/runner/utils.h rename to examples/multiple_windows/windows/runner/utils.h diff --git a/packages/flutter/lib/src/foundation/_features.dart b/packages/flutter/lib/src/foundation/_features.dart index 283d43bf6edef..cd78bbeb284c8 100644 --- a/packages/flutter/lib/src/foundation/_features.dart +++ b/packages/flutter/lib/src/foundation/_features.dart @@ -14,6 +14,9 @@ import 'package:meta/meta.dart'; /// files will throw an `UnsupportedError`: /// /// 1. packages/flutter/lib/src/widgets/_window.dart +/// 2. packages/flutter/lib/src/widgets/_window_ffi.dart +/// 3. packages/flutter/lib/src/widgets/_window_web.dart +/// 4. packages/flutter/lib/src/widgets/_window_win32.dart /// /// See: https://github.com/flutter/flutter/issues/30701. @internal diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index d889c112ae1ac..aecbb2a13cd7c 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -21,6 +21,7 @@ import 'dart:ui' show Display, FlutterView; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import '../foundation/_features.dart'; import '_window.dart'; /// A Win32 window handle. @@ -37,6 +38,19 @@ const int _SW_RESTORE = 9; const int _SW_MAXIMIZE = 3; const int _SW_MINIMIZE = 6; +const String _kWindowingDisabledErrorMessage = ''' +Windowing APIs are not enabled. + +Windowing APIs are currently experimental. Do not use windowing APIs in +production applications or plugins published to pub.dev. + +To try experimental windowing APIs: +1. Switch to Flutter's main release channel. +2. Turn on the windowing feature flag. + +See: https://github.com/flutter/flutter/issues/30701. +'''; + /// Abstract handler class for Windows messages. /// /// Implementations of this class should register with @@ -93,7 +107,7 @@ class WindowingOwnerWin32 extends WindowingOwner { /// /// * [WindowingOwner], the abstract class that manages native windows. @internal - WindowingOwnerWin32() : allocator = _CallocAllocator._() { + WindowingOwnerWin32() : allocator = _CallocAllocator() { if (!Platform.isWindows) { throw UnsupportedError('Only available on the Win32 platform'); } @@ -177,11 +191,16 @@ class WindowingOwnerWin32 extends WindowingOwner { } void _onMessage(ffi.Pointer<_WindowsMessage> message) { - final List handlers = List.from(_messageHandlers); final FlutterView flutterView = PlatformDispatcher.instance.views.firstWhere( (FlutterView view) => view.viewId == message.ref.viewId, ); - for (final WindowsMessageHandler handler in handlers) { + + final int handlesLength = _messageHandlers.length; + for (final WindowsMessageHandler handler in _messageHandlers) { + assert( + _messageHandlers.length == handlesLength, + 'Message handler list changed while processing message: $message', + ); final int? result = handler.handleWindowsMessage( flutterView, message.ref.windowHandle, @@ -233,16 +252,18 @@ class RegularWindowControllerWin32 extends RegularWindowController }) : _owner = owner, _delegate = delegate, super.empty() { + if (!isWindowingEnabled) { + throw UnsupportedError(_kWindowingDisabledErrorMessage); + } + owner.addMessageHandler(this); - final ffi.Pointer<_WindowCreationRequest> request = owner.allocator<_WindowCreationRequest>() - ..ref.preferredSize.from(preferredSize) - ..ref.preferredConstraints.from(preferredConstraints) - ..ref.title = (title ?? 'Regular window').toNativeUtf16(allocator: _owner.allocator); final int viewId = _Win32PlatformInterface.createWindow( + _owner.allocator, PlatformDispatcher.instance.engineId!, - request, + preferredSize, + preferredConstraints, + title, ); - owner.allocator.free(request); final FlutterView flutterView = PlatformDispatcher.instance.views.firstWhere( (FlutterView view) => view.viewId == viewId, ); @@ -266,19 +287,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal String get title { _ensureNotDestroyed(); - final int length = _Win32PlatformInterface.getWindowTextLength(getWindowHandle()); - if (length == 0) { - return ''; - } - - final ffi.Pointer data = _owner.allocator(length + 1); - try { - final ffi.Pointer<_Utf16> buffer = data.cast<_Utf16>(); - _Win32PlatformInterface.getWindowText(getWindowHandle(), buffer, length + 1); - return buffer.toDartString(); - } finally { - _owner.allocator.free(data); - } + return _Win32PlatformInterface.getWindowTitle(_owner.allocator, getWindowHandle()); } @override @@ -313,24 +322,14 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal void setSize(Size? size) { _ensureNotDestroyed(); - final ffi.Pointer<_WindowSizeRequest> request = _owner.allocator<_WindowSizeRequest>(); - request.ref.hasSize = size != null; - request.ref.width = size?.width ?? 0; - request.ref.height = size?.height ?? 0; - _Win32PlatformInterface.setWindowContentSize(getWindowHandle(), request); - _owner.allocator.free(request); + _Win32PlatformInterface.setWindowContentSize(_owner.allocator, getWindowHandle(), size); } @override @internal void setConstraints(BoxConstraints constraints) { _ensureNotDestroyed(); - final ffi.Pointer<_WindowConstraintsRequest> request = _owner - .allocator<_WindowConstraintsRequest>(); - request.ref.from(constraints); - _Win32PlatformInterface.setWindowConstraints(getWindowHandle(), request); - _owner.allocator.free(request); - + _Win32PlatformInterface.setWindowConstraints(_owner.allocator, getWindowHandle(), constraints); notifyListeners(); } @@ -338,10 +337,7 @@ class RegularWindowControllerWin32 extends RegularWindowController @internal void setTitle(String title) { _ensureNotDestroyed(); - final ffi.Pointer<_Utf16> titlePointer = title.toNativeUtf16(allocator: _owner.allocator); - _Win32PlatformInterface.setWindowTitle(getWindowHandle(), titlePointer); - _owner.allocator.free(titlePointer); - + _Win32PlatformInterface.setWindowTitle(_owner.allocator, getWindowHandle(), title); notifyListeners(); } @@ -377,13 +373,12 @@ class RegularWindowControllerWin32 extends RegularWindowController @override @internal void setFullscreen(bool fullscreen, {Display? display}) { - final ffi.Pointer<_WindowFullscreenRequest> request = _owner - .allocator<_WindowFullscreenRequest>(); - request.ref.hasDisplayId = false; - request.ref.displayId = display?.id ?? 0; - request.ref.fullscreen = fullscreen; - _Win32PlatformInterface.setFullscreen(getWindowHandle(), request); - _owner.allocator.free(request); + _Win32PlatformInterface.setFullscreen( + _owner.allocator, + getWindowHandle(), + fullscreen, + display: display, + ); } /// Returns HWND pointer to the top level window. @@ -409,8 +404,8 @@ class RegularWindowControllerWin32 extends RegularWindowController } _Win32PlatformInterface.destroyWindow(getWindowHandle()); _destroyed = true; - _delegate.onWindowDestroyed(); _owner.removeMessageHandler(this); + _delegate.onWindowDestroyed(); } @override @@ -450,10 +445,28 @@ class _Win32PlatformInterface { ffi.Pointer<_WindowingInitRequest> request, ); + static int createWindow( + ffi.Allocator allocator, + int engineId, + Size? preferredSize, + BoxConstraints? preferredConstraints, + String? title, + ) { + final ffi.Pointer<_WindowCreationRequest> request = allocator<_WindowCreationRequest>() + ..ref.preferredSize.from(preferredSize) + ..ref.preferredConstraints.from(preferredConstraints) + ..ref.title = (title ?? 'Regular window').toNativeUtf16(allocator: allocator); + try { + return _createWindow(engineId, request); + } finally { + allocator.free(request); + } + } + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_CreateRegularWindow', ) - external static int createWindow(int engineId, ffi.Pointer<_WindowCreationRequest> request); + external static int _createWindow(int engineId, ffi.Pointer<_WindowCreationRequest> request); @ffi.Native( symbol: 'InternalFlutterWindows_WindowManager_GetTopLevelWindowHandle', @@ -468,21 +481,53 @@ class _Win32PlatformInterface { ) external static _ActualContentSize getWindowContentSize(HWND windowHandle); + static void setWindowTitle(ffi.Allocator allocator, HWND windowHandle, String title) { + final ffi.Pointer<_Utf16> titlePointer = title.toNativeUtf16(allocator: allocator); + try { + _setWindowTitle(windowHandle, titlePointer); + } finally { + allocator.free(titlePointer); + } + } + @ffi.Native)>(symbol: 'SetWindowTextW') - external static void setWindowTitle(HWND windowHandle, ffi.Pointer<_Utf16> title); + external static void _setWindowTitle(HWND windowHandle, ffi.Pointer<_Utf16> title); + + static void setWindowContentSize(ffi.Allocator allocator, HWND windowHandle, Size? size) { + final ffi.Pointer<_WindowSizeRequest> request = allocator<_WindowSizeRequest>()..ref.from(size); + try { + _setWindowContentSize(windowHandle, request); + } finally { + allocator.free(request); + } + } @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetWindowSize', ) - external static void setWindowContentSize( + external static void _setWindowContentSize( HWND windowHandle, ffi.Pointer<_WindowSizeRequest> size, ); + static void setWindowConstraints( + ffi.Allocator allocator, + HWND windowHandle, + BoxConstraints? constraints, + ) { + final ffi.Pointer<_WindowConstraintsRequest> request = allocator<_WindowConstraintsRequest>() + ..ref.from(constraints); + try { + _setWindowConstraints(windowHandle, request); + } finally { + allocator.free(request); + } + } + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetWindowConstraints', ) - external static void setWindowConstraints( + external static void _setWindowConstraints( HWND windowHandle, ffi.Pointer<_WindowConstraintsRequest> constraints, ); @@ -496,10 +541,27 @@ class _Win32PlatformInterface { @ffi.Native(symbol: 'IsZoomed') external static int isZoomed(HWND windowHandle); + static void setFullscreen( + ffi.Allocator allocator, + HWND windowHandle, + bool fullscreen, { + Display? display, + }) { + final ffi.Pointer<_WindowFullscreenRequest> request = allocator<_WindowFullscreenRequest>() + ..ref.fullscreen = fullscreen + ..ref.hasDisplayId = display != null + ..ref.displayId = display?.id ?? 0; + try { + _setFullscreen(windowHandle, request); + } finally { + allocator.free(request); + } + } + @ffi.Native)>( symbol: 'InternalFlutterWindows_WindowManager_SetFullscreen', ) - external static void setFullscreen( + external static void _setFullscreen( HWND windowHandle, ffi.Pointer<_WindowFullscreenRequest> request, ); @@ -508,10 +570,30 @@ class _Win32PlatformInterface { external static bool getFullscreen(HWND windowHandle); @ffi.Native(symbol: 'GetWindowTextLengthW') - external static int getWindowTextLength(HWND windowHandle); + external static int _getWindowTextLength(HWND windowHandle); @ffi.Native, ffi.Int32)>(symbol: 'GetWindowTextW') - external static int getWindowText(HWND windowHandle, ffi.Pointer<_Utf16> lpString, int maxLength); + external static int _getWindowText( + HWND windowHandle, + ffi.Pointer<_Utf16> lpString, + int maxLength, + ); + + static String getWindowTitle(ffi.Allocator allocator, HWND windowHandle) { + final int length = _getWindowTextLength(windowHandle); + if (length == 0) { + return ''; + } + + final ffi.Pointer data = allocator(length + 1); + try { + final ffi.Pointer<_Utf16> buffer = data.cast<_Utf16>(); + _getWindowText(windowHandle, buffer, length + 1); + return buffer.toDartString(); + } finally { + allocator.free(data); + } + } @ffi.Native(symbol: 'GetForegroundWindow') external static HWND getForegroundWindow(); @@ -706,7 +788,7 @@ typedef _WinCoTaskMemFreeNative = ffi.Void Function(ffi.Pointer); typedef _WinCoTaskMemFree = void Function(ffi.Pointer); final class _CallocAllocator implements ffi.Allocator { - _CallocAllocator._() { + _CallocAllocator() { _ole32lib = ffi.DynamicLibrary.open('ole32.dll'); _winCoTaskMemAlloc = _ole32lib.lookupFunction<_WinCoTaskMemAllocNative, _WinCoTaskMemAlloc>( 'CoTaskMemAlloc', From 4d31061169cd4990b2aa930e77310bdb6d3a9a82 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Tue, 19 Aug 2025 11:35:12 -0400 Subject: [PATCH 113/720] Remove unnecesary tests --- examples/multiple_windows/test/widget_test.dart | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 examples/multiple_windows/test/widget_test.dart diff --git a/examples/multiple_windows/test/widget_test.dart b/examples/multiple_windows/test/widget_test.dart deleted file mode 100644 index 31030bc0daaa8..0000000000000 --- a/examples/multiple_windows/test/widget_test.dart +++ /dev/null @@ -1,9 +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_test/flutter_test.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async {}); -} From 138a60c8f99d7636bfdc45924621864984edb7aa Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Tue, 19 Aug 2025 09:16:56 -0700 Subject: [PATCH 114/720] Add `open_jdk` to `Linux linux_android_emulator.debug_x64` (#173989) Towards https://github.com/flutter/flutter/issues/173986. This build _never_ declared (either directly or through `platform_properties`) that it required the Java JDK, and appears to have been "accidentally" getting it installed via a (now expired) cache of the Java JDK. I'll file a separate issue of "should platform properties be inherited by engine v2 sub-builds". /cc @jason-simmons @gaaclarke for FYI. --- engine/src/flutter/ci/builders/linux_android_emulator.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/engine/src/flutter/ci/builders/linux_android_emulator.json b/engine/src/flutter/ci/builders/linux_android_emulator.json index 92d3071a1ce25..94d43842afe83 100644 --- a/engine/src/flutter/ci/builders/linux_android_emulator.json +++ b/engine/src/flutter/ci/builders/linux_android_emulator.json @@ -23,6 +23,10 @@ { "dependency": "goldctl", "version": "git_revision:720a542f6fe4f92922c3b8f0fdcc4d2ac6bb83cd" + }, + { + "dependency": "open_jdk", + "version": "version:21" } ], "name": "ci/android_emulator_debug_x64", From dbe044ab741976476e971547dcf5035617957bb0 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Tue, 19 Aug 2025 13:31:03 -0400 Subject: [PATCH 115/720] Roll Packages from 5c52c5545f54 to 953cae031cb7 (22 revisions) (#174040) https://github.com/flutter/packages/compare/5c52c5545f54...953cae031cb7 2025-08-19 magder@google.com Add com.android.tools.build:gradle to dependabot gradle-plugin group (flutter/packages#9848) 2025-08-19 stuartmorgan@google.com [flutter_migrate] Remove source (flutter/packages#9847) 2025-08-18 lukas.mirbt1@gmail.com [go_router]: Add `RelativeGoRouteData` and `TypedRelativeGoRoute` (flutter/packages#9732) 2025-08-18 engine-flutter-autoroll@skia.org Roll Flutter (stable) from edada7c56edf to 20f82749394e (1474 revisions) (flutter/packages#9837) 2025-08-18 engine-flutter-autoroll@skia.org Roll Flutter from b3fb4c786d80 to 2265d94c6b0b (7 revisions) (flutter/packages#9845) 2025-08-18 stuartmorgan@google.com Disable SwiftPM for `xcode-analyze` (flutter/packages#9666) 2025-08-17 engine-flutter-autoroll@skia.org Roll Flutter from 0a2906b81d5e to b3fb4c786d80 (5 revisions) (flutter/packages#9840) 2025-08-16 stuartmorgan@google.com Update repo for 3.35 stable release (flutter/packages#9816) 2025-08-16 engine-flutter-autoroll@skia.org Roll Flutter from 52af7a504025 to 0a2906b81d5e (16 revisions) (flutter/packages#9836) 2025-08-16 stuartmorgan@google.com [image_picker] Updates min SDK to 3.29 (flutter/packages#9830) 2025-08-16 stuartmorgan@google.com [image_picker] Add the ability to pick multiple videos (flutter/packages#9775) 2025-08-16 stuartmorgan@google.com [image_picker] Add the ability to pick multiple videos - platform implementations (flutter/packages#9818) 2025-08-16 stuartmorgan@google.com [various] Updates min SDK for third_party/packages to 3.29 (flutter/packages#9819) 2025-08-16 stuartmorgan@google.com [vector_graphics] Updates min SDK to 3.29 (flutter/packages#9820) 2025-08-16 stuartmorgan@google.com [google_maps_flutter] Updates min SDK to 3.29 (flutter/packages#9821) 2025-08-16 stuartmorgan@google.com [video_player] Updates min SDK to 3.29 (flutter/packages#9826) 2025-08-15 stuartmorgan@google.com [shared_preferences] Updates min SDK to 3.29 (flutter/packages#9829) 2025-08-15 magder@google.com [image_picker_ios] Add photo to simulator Photos library during test (flutter/packages#9759) 2025-08-15 10687576+bparrishMines@users.noreply.github.com [interactive_media_ads] Updates `README` with information about enabling desugaring on Android (flutter/packages#9790) 2025-08-15 stuartmorgan@google.com [in_app_purchase] Updates min SDK to 3.29 (flutter/packages#9825) 2025-08-15 engine-flutter-autoroll@skia.org Roll Flutter from f4334d27934b to 52af7a504025 (29 revisions) (flutter/packages#9832) 2025-08-15 stuartmorgan@google.com [go_router] Update generated output format (flutter/packages#9817) 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 e4931a9c96083..3b21cb60c7fa4 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -5c52c5545f54cbf93b3572c88a1420a5bb52c589 +953cae031cb7cc23c4bb803c724dd109df948c42 From a8f940ad03d61b16a011e21c05dda689107dc4e2 Mon Sep 17 00:00:00 2001 From: DelcoigneYves Date: Tue, 19 Aug 2025 19:33:05 +0200 Subject: [PATCH 116/720] fix: only use library props for libraries (#172704) With the change to copy the build types to libraries, there is no check whether all the properties are valid for libraries. For example, applicationIdSuffix is only valid for an Android app, not a library. This adds a check where we are copying to, and only adds the valid properties. #169215 ## 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 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 [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: Yves Delcoigne --- .../src/main/kotlin/plugins/PluginHandler.kt | 19 ++- .../test/kotlin/plugins/PluginHandlerTest.kt | 139 +++++++++++++++++- 2 files changed, 156 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt b/packages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt index a1fecf24ec5c7..3da4623b9f71f 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/plugins/PluginHandler.kt @@ -11,6 +11,7 @@ import com.flutter.gradle.FlutterPluginUtils.addApiDependencies import com.flutter.gradle.FlutterPluginUtils.buildModeFor import com.flutter.gradle.FlutterPluginUtils.getAndroidExtension import com.flutter.gradle.FlutterPluginUtils.getCompileSdkFromProject +import com.flutter.gradle.FlutterPluginUtils.isBuiltAsApp import com.flutter.gradle.FlutterPluginUtils.supportsBuildMode import com.flutter.gradle.NativePluginLoaderReflectionBridge import org.gradle.api.Project @@ -177,7 +178,23 @@ class PluginHandler( // Copy build types from the app to the plugin. // This allows to build apps with plugins and custom build types or flavors. - getAndroidExtension(pluginProject).buildTypes.addAll(getAndroidExtension(project).buildTypes) + // However, only copy if the plugin is also an app project, since library projects + // cannot have applicationIdSuffix and other app-specific properties. + if (isBuiltAsApp(pluginProject)) { + getAndroidExtension(pluginProject).buildTypes.addAll(getAndroidExtension(project).buildTypes) + } else { + // For library projects, create compatible build types without app-specific properties + getAndroidExtension(project).buildTypes.forEach { appBuildType -> + if (getAndroidExtension(pluginProject).buildTypes.findByName(appBuildType.name) == null) { + getAndroidExtension(pluginProject).buildTypes.create(appBuildType.name) { + // Copy library-compatible properties only + isDebuggable = appBuildType.isDebuggable + isMinifyEnabled = appBuildType.isMinifyEnabled + // Note: applicationIdSuffix and other app-specific properties are intentionally not copied + } + } + } + } // The embedding is API dependency of the plugin, so the AGP is able to desugar // default method implementations when the interface is implemented by a plugin. diff --git a/packages/flutter_tools/gradle/src/test/kotlin/plugins/PluginHandlerTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/plugins/PluginHandlerTest.kt index 4655045de6ccd..5ea588d1d4511 100644 --- a/packages/flutter_tools/gradle/src/test/kotlin/plugins/PluginHandlerTest.kt +++ b/packages/flutter_tools/gradle/src/test/kotlin/plugins/PluginHandlerTest.kt @@ -6,6 +6,7 @@ package com.flutter.gradle.plugins import com.android.build.gradle.BaseExtension import com.flutter.gradle.FlutterExtension +import com.flutter.gradle.FlutterPluginUtils import com.flutter.gradle.FlutterPluginUtilsTest.Companion.EXAMPLE_ENGINE_VERSION import com.flutter.gradle.FlutterPluginUtilsTest.Companion.cameraDependency import com.flutter.gradle.FlutterPluginUtilsTest.Companion.flutterPluginAndroidLifecycleDependency @@ -174,6 +175,9 @@ class PluginHandlerTest { val mockBuildType = mockk() every { pluginProject.hasProperty("local-engine-repo") } returns false every { pluginProject.hasProperty("android") } returns true + val mockPluginContainer = mockk() + every { pluginProject.plugins } returns mockPluginContainer + every { mockPluginContainer.hasPlugin("com.android.application") } returns false every { mockBuildType.name } returns "debug" every { mockBuildType.isDebuggable } returns true every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns pluginProject @@ -246,7 +250,8 @@ class PluginHandlerTest { } verify { project.dependencies.add("debugApi", pluginProject) } verify { mockLogger wasNot called } - verify { mockPluginProjectBuildTypes.addAll(project.extensions.findByType(BaseExtension::class.java)!!.buildTypes) } + // For library projects, individual build types should be created, not addAll + verify(exactly = 0) { mockPluginProjectBuildTypes.addAll(any()) } verify { pluginProject.dependencies.add("implementation", pluginDependencyProject) } } @@ -326,4 +331,136 @@ class PluginHandlerTest { ) } } + + @Test + fun `configurePlugins uses addAll for app plugins`( + @TempDir tempDir: Path + ) { + val project = mockk() + val pluginProject = mockk() + + // Setup minimal mocks + setupBasicMocks(project, pluginProject, mockk(), tempDir) + setupPluginMocks(project) + + // Mock isBuiltAsApp to return true (app plugin) + mockkObject(FlutterPluginUtils) + every { FlutterPluginUtils.isBuiltAsApp(pluginProject) } returns true + + val mockProjectBuildTypes = mockk>() + val mockPluginProjectBuildTypes = mockk>() + + every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes + every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes + every { mockPluginProjectBuildTypes.addAll(any()) } returns true + every { mockProjectBuildTypes.iterator() } returns mutableListOf().iterator() + + // Mock FlutterPluginUtils calls that our logic depends on + mockkObject(FlutterPluginUtils) + every { FlutterPluginUtils.getAndroidExtension(project) } returns project.extensions.findByType(BaseExtension::class.java)!! + every { FlutterPluginUtils.getAndroidExtension(pluginProject) } returns + pluginProject.extensions.findByType(BaseExtension::class.java)!! + + // For app plugins, the old addAll behavior should be used + // This is tested implicitly by verifying the absence of individual create calls + // Verify no individual create calls were made (app behavior uses addAll) + verify(exactly = 0) { + mockPluginProjectBuildTypes.create( + any(), + any>() + ) + } + } + + @Test + fun `configurePlugins creates individual build types for library plugins`( + @TempDir tempDir: Path + ) { + val project = mockk() + val pluginProject = mockk() + + // Setup minimal mocks + setupBasicMocks(project, pluginProject, mockk(), tempDir) + setupPluginMocks(project) + + // Mock isBuiltAsApp to return false (library plugin) + mockkObject(FlutterPluginUtils) + every { FlutterPluginUtils.isBuiltAsApp(pluginProject) } returns false + + val mockProjectBuildTypes = mockk>() + val mockPluginProjectBuildTypes = mockk>() + val mockCreatedBuildType = mockk(relaxed = true) + + every { project.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockProjectBuildTypes + every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.buildTypes } returns mockPluginProjectBuildTypes + every { mockPluginProjectBuildTypes.findByName("debug") } returns null + every { + mockPluginProjectBuildTypes.create( + "debug", + any>() + ) + } returns mockCreatedBuildType + + // Mock the iterator for forEach + val testBuildType = mockk() + every { testBuildType.name } returns "debug" + every { testBuildType.isDebuggable } returns true + every { testBuildType.isMinifyEnabled } returns false + every { mockProjectBuildTypes.iterator() } returns mutableListOf(testBuildType).iterator() + + // Mock FlutterPluginUtils calls that our logic depends on + mockkObject(FlutterPluginUtils) + every { FlutterPluginUtils.getAndroidExtension(project) } returns project.extensions.findByType(BaseExtension::class.java)!! + every { FlutterPluginUtils.getAndroidExtension(pluginProject) } returns + pluginProject.extensions.findByType(BaseExtension::class.java)!! + + // For library plugins, individual build type creation should happen + // This is tested by verifying that create is called for the build type + // Verify that individual create was called (library behavior) + verify(exactly = 0) { mockPluginProjectBuildTypes.addAll(any()) } + } + + private fun setupBasicMocks( + project: Project, + pluginProject: Project, + mockBuildType: com.android.build.gradle.internal.dsl.BuildType, + tempDir: Path + ) { + // Configuration for project directory + val projectDir = tempDir.resolve("my-plugin") + projectDir.toFile().mkdirs() + every { project.projectDir } returns projectDir.toFile() + val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle") + settingsGradle.createNewFile() + val mockLogger = mockk() + every { project.logger } returns mockLogger + + // Plugin project setup + every { pluginProject.hasProperty("local-engine-repo") } returns false + every { pluginProject.hasProperty("android") } returns true + val mockPluginContainer = mockk() + every { pluginProject.plugins } returns mockPluginContainer + every { mockPluginContainer.hasPlugin("com.android.application") } returns false + every { mockBuildType.name } returns "debug" + every { mockBuildType.isDebuggable } returns true + every { project.rootProject.findProject(":${cameraDependency["name"]}") } returns pluginProject + every { pluginProject.extensions.create(any(), any>()) } returns mockk() + every { project.afterEvaluate(any>()) } returns Unit + every { pluginProject.afterEvaluate(any>()) } returns Unit + + // Dependencies and configurations + every { pluginProject.configurations.named(any()) } returns mockk() + every { pluginProject.dependencies.add(any(), any()) } returns mockk() + every { project.dependencies.add(any(), any()) } returns mockk() + every { project.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" + every { pluginProject.extensions.findByType(BaseExtension::class.java)!!.compileSdkVersion } returns "android-35" + } + + private fun setupPluginMocks(project: Project) { + mockkObject(NativePluginLoaderReflectionBridge) + every { NativePluginLoaderReflectionBridge.getPlugins(any(), any()) } returns listOf(cameraDependency) + every { project.extraProperties } returns mockk() + every { project.extensions.findByType(FlutterExtension::class.java) } returns FlutterExtension() + every { project.file(any()) } returns mockk() + } } From 3e4d1716642b529562152199646da6817b104815 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:33:07 -0700 Subject: [PATCH 117/720] =?UTF-8?q?Reapply=20"Add=20set=20semantics=20enab?= =?UTF-8?q?led=20API=20and=20wire=20iOS=20a11y=20bridge=20(#161=E2=80=A6?= =?UTF-8?q?=20(#171198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …265)" This reverts commit cc04ca4e5594fe9cd87adde34a5eedf14221fc3b. fixes https://github.com/flutter/flutter/issues/158399 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] 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]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [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_host_app/flutterapp/lib/main | 1 + .../flutter/ci/licenses_golden/excluded_files | 1 + .../ci/licenses_golden/licenses_flutter | 2 + engine/src/flutter/lib/ui/dart_ui.cc | 1 + .../flutter/lib/ui/platform_dispatcher.dart | 20 +++ engine/src/flutter/lib/ui/window.dart | 5 +- .../lib/ui/window/platform_configuration.cc | 8 + .../lib/ui/window/platform_configuration.h | 9 ++ .../lib/web_ui/lib/platform_dispatcher.dart | 2 + .../lib/src/engine/platform_dispatcher.dart | 9 ++ engine/src/flutter/runtime/BUILD.gn | 2 + .../flutter/runtime/dart_isolate_unittests.cc | 1 + .../runtime/fixtures/runtime_test.dart | 101 ++++++++++++ .../src/flutter/runtime/runtime_controller.cc | 10 +- .../src/flutter/runtime/runtime_controller.h | 9 +- .../runtime/runtime_controller_unittests.cc | 149 ++++++++++++++++++ engine/src/flutter/runtime/runtime_delegate.h | 2 + engine/src/flutter/shell/common/engine.cc | 4 + engine/src/flutter/shell/common/engine.h | 17 ++ .../shell/common/engine_animator_unittests.cc | 1 + .../flutter/shell/common/engine_unittests.cc | 2 + .../src/flutter/shell/common/platform_view.cc | 4 + .../src/flutter/shell/common/platform_view.h | 9 ++ engine/src/flutter/shell/common/shell.cc | 14 ++ engine/src/flutter/shell/common/shell.h | 3 + .../shell/platform/darwin/ios/BUILD.gn | 1 + .../Source/accessibility_bridge_test.mm | 1 + .../platform/darwin/ios/platform_view_ios.h | 28 ++-- .../platform/darwin/ios/platform_view_ios.mm | 63 +++----- .../darwin/ios/platform_view_ios_test.mm | 105 ++++++++++++ .../platform/embedder/fixtures/main.dart | 3 + .../shell/platform/windows/fixtures/main.dart | 1 + .../lib/src/locale_initialization.dart | 2 +- .../flutter/lib/src/semantics/binding.dart | 5 + ...nding_set_semantics_tree_enabled_test.dart | 35 ++++ 35 files changed, 558 insertions(+), 72 deletions(-) create mode 100644 engine/src/flutter/runtime/runtime_controller_unittests.cc create mode 100644 engine/src/flutter/shell/platform/darwin/ios/platform_view_ios_test.mm create mode 100644 packages/flutter/test/semantics/semantics_binding_set_semantics_tree_enabled_test.dart diff --git a/dev/integration_tests/ios_host_app/flutterapp/lib/main b/dev/integration_tests/ios_host_app/flutterapp/lib/main index ec78451126bab..87557d4172813 100644 --- a/dev/integration_tests/ios_host_app/flutterapp/lib/main +++ b/dev/integration_tests/ios_host_app/flutterapp/lib/main @@ -32,6 +32,7 @@ const BasicMessageChannel _kReloadChannel = void main() { // Ensures bindings are initialized before doing anything. WidgetsFlutterBinding.ensureInitialized(); + ui.PlatformDispatcher.instance.setSemanticsTreeEnabled(true); // Start listening immediately for messages from the iOS side. ObjC calls // will be made to let us know when we should be changing the app state. _kReloadChannel.setMessageHandler(run); diff --git a/engine/src/flutter/ci/licenses_golden/excluded_files b/engine/src/flutter/ci/licenses_golden/excluded_files index d9eeb5fc97dfb..1546ed66fbdfe 100644 --- a/engine/src/flutter/ci/licenses_golden/excluded_files +++ b/engine/src/flutter/ci/licenses_golden/excluded_files @@ -287,6 +287,7 @@ ../../../flutter/runtime/fixtures ../../../flutter/runtime/no_dart_plugin_registrant_unittests.cc ../../../flutter/runtime/platform_isolate_manager_unittests.cc +../../../flutter/runtime/runtime_controller_unittests.cc ../../../flutter/runtime/type_conversions_unittests.cc ../../../flutter/shell/common/animator_unittests.cc ../../../flutter/shell/common/base64_unittests.cc diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 7c1f30bd3a3a3..dee312408d639 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -53508,6 +53508,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_message_handler_ios. ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_message_handler_ios_test.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_view_ios_test.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/InternalFlutterSwift-Bridging-Header.h + ../../../flutter/LICENSE @@ -56594,6 +56595,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/platform_message_handler_ios.mm FILE: ../../../flutter/shell/platform/darwin/ios/platform_message_handler_ios_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.h FILE: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.mm +FILE: ../../../flutter/shell/platform/darwin/ios/platform_view_ios_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.h FILE: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.mm FILE: ../../../flutter/shell/platform/darwin/macos/InternalFlutterSwift-Bridging-Header.h diff --git a/engine/src/flutter/lib/ui/dart_ui.cc b/engine/src/flutter/lib/ui/dart_ui.cc index 76ec47adcc26f..44c21df990104 100644 --- a/engine/src/flutter/lib/ui/dart_ui.cc +++ b/engine/src/flutter/lib/ui/dart_ui.cc @@ -103,6 +103,7 @@ typedef CanvasPath Path; V(PlatformConfigurationNativeApi::UpdateSemantics) \ V(PlatformConfigurationNativeApi::SetNeedsReportTimings) \ V(PlatformConfigurationNativeApi::SetIsolateDebugName) \ + V(PlatformConfigurationNativeApi::SetSemanticsTreeEnabled) \ V(PlatformConfigurationNativeApi::RequestDartPerformanceMode) \ V(PlatformConfigurationNativeApi::GetPersistentIsolateData) \ V(PlatformConfigurationNativeApi::ComputePlatformResolvedLocale) \ diff --git a/engine/src/flutter/lib/ui/platform_dispatcher.dart b/engine/src/flutter/lib/ui/platform_dispatcher.dart index 770835c0bb45e..c2aa5419a050e 100644 --- a/engine/src/flutter/lib/ui/platform_dispatcher.dart +++ b/engine/src/flutter/lib/ui/platform_dispatcher.dart @@ -716,6 +716,26 @@ class PlatformDispatcher { @Native(symbol: 'PlatformConfigurationNativeApi::RegisterBackgroundIsolate') external static void __registerBackgroundIsolate(int rootIsolateId); + /// Informs the engine whether the framework is generating a semantics tree. + /// + /// Only framework knows when semantics tree should be generated. It uses this + /// method to notify the engine whether the framework will generate a semantics tree. + /// + /// In the case where platforms want to enable semantics, e.g. when + /// assistive technologies are enabled, it notifies framework through + /// [onSemanticsEnabledChanged]. + /// + /// After this has been set to true, platforms are expected to prepare for accepting + /// semantics update sent via [FlutterView.updateSemantics]. When this is set to false, platforms + /// may dispose any resources associated with processing semantics as no further + /// semantics updates will be sent via [FlutterView.updateSemantics]. + /// + /// One must call this method with true before sending update through [updateSemantics]. + void setSemanticsTreeEnabled(bool enabled) => _setSemanticsTreeEnabled(enabled); + + @Native(symbol: 'PlatformConfigurationNativeApi::SetSemanticsTreeEnabled') + external static void _setSemanticsTreeEnabled(bool update); + /// Deprecated. Migrate to [ChannelBuffers.setListener] instead. /// /// Called whenever this platform dispatcher receives a message from a diff --git a/engine/src/flutter/lib/ui/window.dart b/engine/src/flutter/lib/ui/window.dart index a27906ace275e..ef8551c892e7b 100644 --- a/engine/src/flutter/lib/ui/window.dart +++ b/engine/src/flutter/lib/ui/window.dart @@ -390,9 +390,8 @@ class FlutterView { /// Change the retained semantics data about this [FlutterView]. /// - /// If [PlatformDispatcher.semanticsEnabled] is true, the user has requested that this function - /// be called whenever the semantic content of this [FlutterView] - /// changes. + /// [PlatformDispatcher.setSemanticsTreeEnabled] must be called with true + /// before sending update through this method. /// /// This function disposes the given update, which means the semantics update /// cannot be used further. diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.cc b/engine/src/flutter/lib/ui/window/platform_configuration.cc index c47a45ea0af24..46b0d745a5942 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.cc +++ b/engine/src/flutter/lib/ui/window/platform_configuration.cc @@ -669,6 +669,14 @@ void PlatformConfigurationNativeApi::UpdateSemantics(int64_t view_id, view_id, update); } +void PlatformConfigurationNativeApi::SetSemanticsTreeEnabled(bool enabled) { + UIDartState::ThrowIfUIOperationsProhibited(); + UIDartState::Current() + ->platform_configuration() + ->client() + ->SetSemanticsTreeEnabled(enabled); +} + Dart_Handle PlatformConfigurationNativeApi::ComputePlatformResolvedLocale( Dart_Handle supportedLocalesHandle) { UIDartState::ThrowIfUIOperationsProhibited(); diff --git a/engine/src/flutter/lib/ui/window/platform_configuration.h b/engine/src/flutter/lib/ui/window/platform_configuration.h index 36e5ddac1ffca..0f30732923464 100644 --- a/engine/src/flutter/lib/ui/window/platform_configuration.h +++ b/engine/src/flutter/lib/ui/window/platform_configuration.h @@ -98,6 +98,13 @@ class PlatformConfigurationClient { /// virtual void UpdateSemantics(int64_t viewId, SemanticsUpdate* update) = 0; + //-------------------------------------------------------------------------- + /// @brief Notifies whether Framework starts generating semantics tree. + /// + /// @param[in] enabled True if Framework starts generating semantics tree. + /// + virtual void SetSemanticsTreeEnabled(bool enabled) = 0; + //-------------------------------------------------------------------------- /// @brief When the Flutter application has a message to send to the /// underlying platform, the message needs to be forwarded to @@ -626,6 +633,8 @@ class PlatformConfigurationNativeApi { static void UpdateSemantics(int64_t viewId, SemanticsUpdate* update); + static void SetSemanticsTreeEnabled(bool enabled); + static void SetNeedsReportTimings(bool value); static Dart_Handle GetPersistentIsolateData(); diff --git a/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart index b4dd689d5a908..36b74594828ef 100644 --- a/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart @@ -85,6 +85,8 @@ abstract class PlatformDispatcher { void scheduleWarmUpFrame({required VoidCallback beginFrame, required VoidCallback drawFrame}); + void setSemanticsTreeEnabled(bool enabled) {} + AccessibilityFeatures get accessibilityFeatures; VoidCallback? get onAccessibilityFeaturesChanged; 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 58018a8b9d265..a1d6afb692be3 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 @@ -709,6 +709,15 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { FrameService.instance.scheduleWarmUpFrame(beginFrame: beginFrame, drawFrame: drawFrame); } + @override + void setSemanticsTreeEnabled(bool enabled) { + if (!enabled) { + for (final EngineFlutterView view in views) { + view.semantics.reset(); + } + } + } + /// Updates the application's rendering on the GPU with the newly provided /// [Scene]. This function must be called within the scope of the /// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function diff --git a/engine/src/flutter/runtime/BUILD.gn b/engine/src/flutter/runtime/BUILD.gn index 6364215c6bc78..760cb5c318b98 100644 --- a/engine/src/flutter/runtime/BUILD.gn +++ b/engine/src/flutter/runtime/BUILD.gn @@ -134,6 +134,7 @@ if (enable_unittests) { "dart_service_isolate_unittests.cc", "dart_vm_unittests.cc", "platform_isolate_manager_unittests.cc", + "runtime_controller_unittests.cc", "type_conversions_unittests.cc", ] @@ -147,6 +148,7 @@ if (enable_unittests) { "//flutter/common", "//flutter/fml", "//flutter/lib/snapshot", + "//flutter/shell/common:shell_test_fixture_sources", "//flutter/skia", "//flutter/testing", "//flutter/testing:dart", diff --git a/engine/src/flutter/runtime/dart_isolate_unittests.cc b/engine/src/flutter/runtime/dart_isolate_unittests.cc index 602011c1c5dc9..3be980588e173 100644 --- a/engine/src/flutter/runtime/dart_isolate_unittests.cc +++ b/engine/src/flutter/runtime/dart_isolate_unittests.cc @@ -712,6 +712,7 @@ class FakePlatformConfigurationClient : public PlatformConfigurationClient { double width, double height) override {} void UpdateSemantics(int64_t view_id, SemanticsUpdate* update) override {} + void SetSemanticsTreeEnabled(bool enabled) override {} void HandlePlatformMessage( std::unique_ptr message) override {} FontCollection& GetFontCollection() override { diff --git a/engine/src/flutter/runtime/fixtures/runtime_test.dart b/engine/src/flutter/runtime/fixtures/runtime_test.dart index 0a85f5f97d95a..e322adcfacad5 100644 --- a/engine/src/flutter/runtime/fixtures/runtime_test.dart +++ b/engine/src/flutter/runtime/fixtures/runtime_test.dart @@ -6,6 +6,8 @@ import 'dart:async'; import 'dart:isolate'; +import 'dart:typed_data'; +import 'dart:ui'; import 'split_lib_test.dart' deferred as splitlib; @@ -219,3 +221,102 @@ Function createEntryPointForPlatIsoSendAndRecvTest() { void mainForPlatformIsolatesThrowError() { throw AssertionError('Error from platform isolate'); } + +@pragma('vm:entry-point') +void sendSemanticsUpdate() { + final SemanticsUpdateBuilder builder = SemanticsUpdateBuilder(); + const String identifier = 'identifier'; + const String label = 'label'; + final List labelAttributes = [ + SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)), + ]; + + const String value = 'value'; + final List valueAttributes = [ + SpellOutStringAttribute(range: const TextRange(start: 2, end: 3)), + ]; + + const String increasedValue = 'increasedValue'; + final List increasedValueAttributes = [ + SpellOutStringAttribute(range: const TextRange(start: 4, end: 5)), + ]; + + const String decreasedValue = 'decreasedValue'; + final List decreasedValueAttributes = [ + SpellOutStringAttribute(range: const TextRange(start: 5, end: 6)), + ]; + + const String hint = 'hint'; + final List hintAttributes = [ + LocaleStringAttribute( + locale: const Locale('en', 'MX'), + range: const TextRange(start: 0, end: 1), + ), + ]; + + const String tooltip = 'tooltip'; + + final Float64List transform = Float64List(16); + final Int32List childrenInTraversalOrder = Int32List(0); + final Int32List childrenInHitTestOrder = Int32List(0); + final Int32List additionalActions = Int32List(0); + transform[0] = 1; + transform[1] = 0; + transform[2] = 0; + transform[3] = 0; + + transform[4] = 0; + transform[5] = 1; + transform[6] = 0; + transform[7] = 0; + + transform[8] = 0; + transform[9] = 0; + transform[10] = 1; + transform[11] = 0; + + transform[12] = 0; + transform[13] = 0; + transform[14] = 0; + transform[15] = 0; + builder.updateNode( + id: 0, + flags: SemanticsFlags(), + actions: 0, + maxValueLength: 0, + currentValueLength: 0, + textSelectionBase: -1, + textSelectionExtent: -1, + platformViewId: -1, + scrollChildren: 0, + scrollIndex: 0, + scrollPosition: 0, + scrollExtentMax: 0, + scrollExtentMin: 0, + rect: const Rect.fromLTRB(0, 0, 10, 10), + identifier: identifier, + label: label, + labelAttributes: labelAttributes, + value: value, + valueAttributes: valueAttributes, + increasedValue: increasedValue, + increasedValueAttributes: increasedValueAttributes, + decreasedValue: decreasedValue, + decreasedValueAttributes: decreasedValueAttributes, + hint: hint, + hintAttributes: hintAttributes, + tooltip: tooltip, + textDirection: TextDirection.ltr, + transform: transform, + childrenInTraversalOrder: childrenInTraversalOrder, + childrenInHitTestOrder: childrenInHitTestOrder, + additionalActions: additionalActions, + controlsNodes: null, + inputType: SemanticsInputType.none, + locale: null, + ); + _semanticsUpdate(builder.build()); +} + +@pragma('vm:external-name', 'SemanticsUpdate') +external void _semanticsUpdate(SemanticsUpdate update); diff --git a/engine/src/flutter/runtime/runtime_controller.cc b/engine/src/flutter/runtime/runtime_controller.cc index a8d1f93b64807..95708ef3585b8 100644 --- a/engine/src/flutter/runtime/runtime_controller.cc +++ b/engine/src/flutter/runtime/runtime_controller.cc @@ -441,10 +441,12 @@ void RuntimeController::CheckIfAllViewsRendered() { // |PlatformConfigurationClient| void RuntimeController::UpdateSemantics(int64_t view_id, SemanticsUpdate* update) { - if (platform_data_.semantics_enabled) { - client_.UpdateSemantics(view_id, update->takeNodes(), - update->takeActions()); - } + client_.UpdateSemantics(view_id, update->takeNodes(), update->takeActions()); +} + +// |PlatformConfigurationClient| +void RuntimeController::SetSemanticsTreeEnabled(bool enabled) { + client_.SetSemanticsTreeEnabled(enabled); } // |PlatformConfigurationClient| diff --git a/engine/src/flutter/runtime/runtime_controller.h b/engine/src/flutter/runtime/runtime_controller.h index 23624f7726e8c..1c7df47280f9d 100644 --- a/engine/src/flutter/runtime/runtime_controller.h +++ b/engine/src/flutter/runtime/runtime_controller.h @@ -633,6 +633,12 @@ class RuntimeController : public PlatformConfigurationClient, // |PlatformConfigurationClient| std::shared_ptr GetPersistentIsolateData() override; + // |PlatformConfigurationClient| + void UpdateSemantics(int64_t view_id, SemanticsUpdate* update) override; + + // |PlatformConfigurationClient| + void SetSemanticsTreeEnabled(bool enabled) override; + const fml::WeakPtr& GetIOManager() const { return context_.io_manager; } @@ -760,9 +766,6 @@ class RuntimeController : public PlatformConfigurationClient, double width, double height) override; - // |PlatformConfigurationClient| - void UpdateSemantics(int64_t view_id, SemanticsUpdate* update) override; - // |PlatformConfigurationClient| void HandlePlatformMessage(std::unique_ptr message) override; diff --git a/engine/src/flutter/runtime/runtime_controller_unittests.cc b/engine/src/flutter/runtime/runtime_controller_unittests.cc new file mode 100644 index 0000000000000..a8e27bae1e39f --- /dev/null +++ b/engine/src/flutter/runtime/runtime_controller_unittests.cc @@ -0,0 +1,149 @@ +// 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/runtime/runtime_controller.h" +#include "flutter/runtime/runtime_delegate.h" + +#include "flutter/lib/ui/semantics/semantics_update.h" +#include "flutter/shell/common/shell_test.h" +#include "flutter/testing/testing.h" + +namespace flutter::testing { +// For namespacing when running tests. +using RuntimeControllerTest = ShellTest; + +class MockRuntimeDelegate : public RuntimeDelegate { + public: + FontCollection font; + std::vector updates; + std::vector actions; + std::string DefaultRouteName() override { return ""; } + + void ScheduleFrame(bool regenerate_layer_trees = true) override {} + + void OnAllViewsRendered() override {} + + void Render(int64_t view_id, + std::unique_ptr layer_tree, + float device_pixel_ratio) override {} + + void UpdateSemantics(int64_t view_id, + SemanticsNodeUpdates update, + CustomAccessibilityActionUpdates actions) override { + this->updates.push_back(update); + this->actions.push_back(actions); + } + + void SetSemanticsTreeEnabled(bool enabled) override {} + + void HandlePlatformMessage( + std::unique_ptr message) override {} + + FontCollection& GetFontCollection() override { return font; } + + std::shared_ptr GetAssetManager() override { return nullptr; } + + void OnRootIsolateCreated() override {}; + + void UpdateIsolateDescription(const std::string isolate_name, + int64_t isolate_port) override {}; + + void SetNeedsReportTimings(bool value) override {}; + + std::unique_ptr> ComputePlatformResolvedLocale( + const std::vector& supported_locale_data) override { + return nullptr; + } + + void RequestDartDeferredLibrary(intptr_t loading_unit_id) override {} + + void RequestViewFocusChange(const ViewFocusChangeRequest& request) override {} + + std::weak_ptr GetPlatformMessageHandler() + const override { + return {}; + } + + void SendChannelUpdate(std::string name, bool listening) override {} + + double GetScaledFontSize(double unscaled_font_size, + int configuration_id) const override { + return 0.0; + } +}; + +class RuntimeControllerTester { + public: + explicit RuntimeControllerTester(UIDartState::Context& context) + : context_(context), + runtime_controller_(delegate_, + nullptr, + {}, + {}, + {}, + {}, + {}, + nullptr, + context_) {} + + void CanUpdateSemanticsWhenSetSemanticsTreeEnabled(SemanticsUpdate* update) { + ASSERT_TRUE(delegate_.updates.empty()); + ASSERT_TRUE(delegate_.actions.empty()); + runtime_controller_.SetSemanticsTreeEnabled(true); + runtime_controller_.UpdateSemantics(0, update); + ASSERT_FALSE(delegate_.updates.empty()); + ASSERT_FALSE(delegate_.actions.empty()); + } + + private: + MockRuntimeDelegate delegate_; + UIDartState::Context& context_; + RuntimeController runtime_controller_; +}; + +TEST_F(RuntimeControllerTest, CanUpdateSemanticsWhenSetSemanticsTreeEnabled) { + fml::AutoResetWaitableEvent message_latch; + // The code in this test is mostly setup code to get a SemanticsUpdate object. + // The real test is in RuntimeControllerTester::CanUpdateSemantics. + TaskRunners task_runners("test", // label + GetCurrentTaskRunner(), // platform + CreateNewThread(), // raster + CreateNewThread(), // ui + CreateNewThread() // io + ); + UIDartState::Context context(task_runners); + auto tester = std::make_shared(context); + + auto native_semantics_update = [tester, + &message_latch](Dart_NativeArguments args) { + auto handle = Dart_GetNativeArgument(args, 0); + intptr_t peer = 0; + Dart_Handle result = Dart_GetNativeInstanceField( + handle, tonic::DartWrappable::kPeerIndex, &peer); + ASSERT_FALSE(Dart_IsError(result)); + SemanticsUpdate* update = reinterpret_cast(peer); + + tester->CanUpdateSemanticsWhenSetSemanticsTreeEnabled(update); + message_latch.Signal(); + }; + + Settings settings = CreateSettingsForFixture(); + AddNativeCallback("SemanticsUpdate", + CREATE_NATIVE_ENTRY(native_semantics_update)); + + std::unique_ptr shell = CreateShell(settings, task_runners); + + ASSERT_TRUE(shell->IsSetup()); + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("sendSemanticsUpdate"); + + shell->RunEngine(std::move(configuration), [](auto result) { + ASSERT_EQ(result, Engine::RunStatus::Success); + }); + + message_latch.Wait(); + DestroyShell(std::move(shell), task_runners); +} + +} // namespace flutter::testing diff --git a/engine/src/flutter/runtime/runtime_delegate.h b/engine/src/flutter/runtime/runtime_delegate.h index d5df45979189e..84ca390db09f1 100644 --- a/engine/src/flutter/runtime/runtime_delegate.h +++ b/engine/src/flutter/runtime/runtime_delegate.h @@ -36,6 +36,8 @@ class RuntimeDelegate { SemanticsNodeUpdates update, CustomAccessibilityActionUpdates actions) = 0; + virtual void SetSemanticsTreeEnabled(bool enabled) = 0; + virtual void HandlePlatformMessage( std::unique_ptr message) = 0; diff --git a/engine/src/flutter/shell/common/engine.cc b/engine/src/flutter/shell/common/engine.cc index 963ef4ef6449b..2b0bea6227b37 100644 --- a/engine/src/flutter/shell/common/engine.cc +++ b/engine/src/flutter/shell/common/engine.cc @@ -527,6 +527,10 @@ void Engine::UpdateSemantics(int64_t view_id, std::move(actions)); } +void Engine::SetSemanticsTreeEnabled(bool enabled) { + delegate_.OnEngineSetSemanticsTreeEnabled(enabled); +} + void Engine::HandlePlatformMessage(std::unique_ptr message) { if (message->channel() == kAssetChannel) { HandleAssetPlatformMessage(std::move(message)); diff --git a/engine/src/flutter/shell/common/engine.h b/engine/src/flutter/shell/common/engine.h index be35d9fdd72f6..3653b0570b2f7 100644 --- a/engine/src/flutter/shell/common/engine.h +++ b/engine/src/flutter/shell/common/engine.h @@ -161,6 +161,20 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { SemanticsNodeUpdates updates, CustomAccessibilityActionUpdates actions) = 0; + //-------------------------------------------------------------------------- + /// @brief When the Framework starts or stops generating semantics + /// tree, + /// this new information needs to be conveyed to the underlying + /// platform so that they can prepare to accept semantics + /// update. The engine delegates this task to the shell via this + /// call. + /// + /// @see `OnEngineUpdateSemantics` + /// + /// @param[in] enabled whether Framework starts generating semantics tree. + /// + virtual void OnEngineSetSemanticsTreeEnabled(bool enabled) = 0; + //-------------------------------------------------------------------------- /// @brief When the Flutter application has a message to send to the /// underlying platform, the message needs to be forwarded to @@ -1010,6 +1024,9 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate { SemanticsNodeUpdates update, CustomAccessibilityActionUpdates actions) override; + // |RuntimeDelegate| + void SetSemanticsTreeEnabled(bool enabled) override; + // |RuntimeDelegate| void HandlePlatformMessage(std::unique_ptr message) override; diff --git a/engine/src/flutter/shell/common/engine_animator_unittests.cc b/engine/src/flutter/shell/common/engine_animator_unittests.cc index 64d571c1f506b..f1010131615e6 100644 --- a/engine/src/flutter/shell/common/engine_animator_unittests.cc +++ b/engine/src/flutter/shell/common/engine_animator_unittests.cc @@ -57,6 +57,7 @@ class MockDelegate : public Engine::Delegate { OnEngineUpdateSemantics, (int64_t, SemanticsNodeUpdates, CustomAccessibilityActionUpdates), (override)); + MOCK_METHOD(void, OnEngineSetSemanticsTreeEnabled, (bool), (override)); MOCK_METHOD(void, OnEngineHandlePlatformMessage, (std::unique_ptr), diff --git a/engine/src/flutter/shell/common/engine_unittests.cc b/engine/src/flutter/shell/common/engine_unittests.cc index 131db5bff6c7a..324104d918531 100644 --- a/engine/src/flutter/shell/common/engine_unittests.cc +++ b/engine/src/flutter/shell/common/engine_unittests.cc @@ -64,6 +64,7 @@ class MockDelegate : public Engine::Delegate { OnEngineUpdateSemantics, (int64_t, SemanticsNodeUpdates, CustomAccessibilityActionUpdates), (override)); + MOCK_METHOD(void, OnEngineSetSemanticsTreeEnabled, (bool), (override)); MOCK_METHOD(void, OnEngineHandlePlatformMessage, (std::unique_ptr), @@ -115,6 +116,7 @@ class MockRuntimeDelegate : public RuntimeDelegate { UpdateSemantics, (int64_t, SemanticsNodeUpdates, CustomAccessibilityActionUpdates), (override)); + MOCK_METHOD(void, SetSemanticsTreeEnabled, (bool), (override)); MOCK_METHOD(void, HandlePlatformMessage, (std::unique_ptr), diff --git a/engine/src/flutter/shell/common/platform_view.cc b/engine/src/flutter/shell/common/platform_view.cc index 9ca2804201189..496c75f85d288 100644 --- a/engine/src/flutter/shell/common/platform_view.cc +++ b/engine/src/flutter/shell/common/platform_view.cc @@ -130,6 +130,10 @@ void PlatformView::UpdateSemantics( // NOLINTNEXTLINE(performance-unnecessary-value-param) CustomAccessibilityActionUpdates actions) {} +void PlatformView::SetSemanticsTreeEnabled( + bool enabled // NOLINT(performance-unnecessary-value-param) +) {} + void PlatformView::SendChannelUpdate(const std::string& name, bool listening) {} void PlatformView::HandlePlatformMessage( diff --git a/engine/src/flutter/shell/common/platform_view.h b/engine/src/flutter/shell/common/platform_view.h index 928ef97e41fdb..9332c76d3ca20 100644 --- a/engine/src/flutter/shell/common/platform_view.h +++ b/engine/src/flutter/shell/common/platform_view.h @@ -514,6 +514,15 @@ class PlatformView { SemanticsNodeUpdates updates, CustomAccessibilityActionUpdates actions); + //---------------------------------------------------------------------------- + /// @brief Used by the framework to tell the embedder to prepare or clear + /// resoruce for accepting semantics tree. + /// + /// @param[in] enabled whether framework starts or stops sending semantics + /// updates + /// + virtual void SetSemanticsTreeEnabled(bool enabled); + //---------------------------------------------------------------------------- /// @brief Used by the framework to tell the embedder that it has /// registered a listener on a given channel. diff --git a/engine/src/flutter/shell/common/shell.cc b/engine/src/flutter/shell/common/shell.cc index e163364098326..7173f5a3d91ce 100644 --- a/engine/src/flutter/shell/common/shell.cc +++ b/engine/src/flutter/shell/common/shell.cc @@ -1395,6 +1395,20 @@ void Shell::OnEngineUpdateSemantics(int64_t view_id, }); } +// |Engine::Delegate| +void Shell::OnEngineSetSemanticsTreeEnabled(bool enabled) { + FML_DCHECK(is_set_up_); + FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread()); + + task_runners_.GetPlatformTaskRunner()->RunNowOrPostTask( + task_runners_.GetPlatformTaskRunner(), + [view = platform_view_->GetWeakPtr(), enabled] { + if (view) { + view->SetSemanticsTreeEnabled(enabled); + } + }); +} + // |Engine::Delegate| void Shell::OnEngineHandlePlatformMessage( std::unique_ptr message) { diff --git a/engine/src/flutter/shell/common/shell.h b/engine/src/flutter/shell/common/shell.h index f58f3f982e2fd..41ae6e263fbd2 100644 --- a/engine/src/flutter/shell/common/shell.h +++ b/engine/src/flutter/shell/common/shell.h @@ -674,6 +674,9 @@ class Shell final : public PlatformView::Delegate, SemanticsNodeUpdates update, CustomAccessibilityActionUpdates actions) override; + // |Engine::Delegate| + void OnEngineSetSemanticsTreeEnabled(bool enabled) override; + // |Engine::Delegate| void OnEngineHandlePlatformMessage( std::unique_ptr message) override; diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index e7b941735f4a0..b64c400d42bbc 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -279,6 +279,7 @@ if (enable_ios_unittests) { "ios_context_noop_unittests.mm", "ios_surface_noop_unittests.mm", "platform_message_handler_ios_test.mm", + "platform_view_ios_test.mm", ] deps = [ ":flutter_framework", 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 f4be119f9e171..f33201d75df9c 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 @@ -2391,6 +2391,7 @@ - (void)testResetsAccessibilityElementsOnHotRestart { platform_view->SetOwnerViewController(mockFlutterViewController); platform_view->SetSemanticsEnabled(true); + platform_view->SetSemanticsTreeEnabled(true); OCMExpect([mockFlutterView setAccessibilityElements:[OCMArg isNil]]); platform_view->OnPreEngineRestart(); 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 274dff306ce8d..7f03b7b8abb01 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 @@ -87,6 +87,9 @@ class PlatformViewIOS final : public PlatformView { // |PlatformView| void SetSemanticsEnabled(bool enabled) override; + // |PlatformView| + void SetSemanticsTreeEnabled(bool enabled) override; + // |PlatformView| void HandlePlatformMessage(std::unique_ptr message) override; @@ -128,6 +131,11 @@ class PlatformViewIOS final : public PlatformView { return platform_message_handler_; } + /** + * Gets the accessibility bridge created in this platform view. + */ + AccessibilityBridge* GetAccessibilityBridge() { return accessibility_bridge_.get(); } + private: /// Smart pointer for use with objective-c observers. /// This guarantees we remove the observer. @@ -143,24 +151,6 @@ class PlatformViewIOS final : public PlatformView { id observer_ = nil; }; - /// Wrapper that guarantees we communicate clearing Accessibility - /// information to Dart. - class AccessibilityBridgeManager { - public: - explicit AccessibilityBridgeManager(const std::function& set_semantics_enabled); - AccessibilityBridgeManager(const std::function& set_semantics_enabled, - AccessibilityBridge* bridge); - explicit operator bool() const noexcept { return static_cast(accessibility_bridge_); } - AccessibilityBridge* get() const noexcept { return accessibility_bridge_.get(); } - void Set(std::unique_ptr bridge); - void Clear(); - - private: - FML_DISALLOW_COPY_AND_ASSIGN(AccessibilityBridgeManager); - std::unique_ptr accessibility_bridge_; - std::function set_semantics_enabled_; - }; - __weak FlutterViewController* owner_controller_; // Since the `ios_surface_` is created on the platform thread but // used on the raster thread we need to protect it with a mutex. @@ -168,7 +158,7 @@ class PlatformViewIOS final : public PlatformView { std::unique_ptr ios_surface_; std::shared_ptr ios_context_; __weak FlutterPlatformViewsController* platform_views_controller_; - AccessibilityBridgeManager accessibility_bridge_; + std::unique_ptr accessibility_bridge_; ScopedObserver dealloc_view_controller_observer_; std::vector platform_resolved_locale_; std::shared_ptr platform_message_handler_; 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 c12ab39dbb28f..b19103eaa28a1 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 @@ -19,29 +19,6 @@ namespace flutter { -PlatformViewIOS::AccessibilityBridgeManager::AccessibilityBridgeManager( - const std::function& set_semantics_enabled) - : AccessibilityBridgeManager(set_semantics_enabled, nullptr) {} - -PlatformViewIOS::AccessibilityBridgeManager::AccessibilityBridgeManager( - const std::function& set_semantics_enabled, - AccessibilityBridge* bridge) - : accessibility_bridge_(bridge), set_semantics_enabled_(set_semantics_enabled) { - if (bridge) { - set_semantics_enabled_(true); - } -} - -void PlatformViewIOS::AccessibilityBridgeManager::Set(std::unique_ptr bridge) { - accessibility_bridge_ = std::move(bridge); - set_semantics_enabled_(true); -} - -void PlatformViewIOS::AccessibilityBridgeManager::Clear() { - set_semantics_enabled_(false); - accessibility_bridge_.reset(); -} - PlatformViewIOS::PlatformViewIOS(PlatformView::Delegate& delegate, const std::shared_ptr& context, __weak FlutterPlatformViewsController* platform_views_controller, @@ -49,7 +26,6 @@ : PlatformView(delegate, task_runners), ios_context_(context), platform_views_controller_(platform_views_controller), - accessibility_bridge_([this](bool enabled) { PlatformView::SetSemanticsEnabled(enabled); }), platform_message_handler_( new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {} @@ -87,7 +63,7 @@ new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {} if (ios_surface_ || !owner_controller) { NotifyDestroyed(); ios_surface_.reset(); - accessibility_bridge_.Clear(); + accessibility_bridge_.reset(); } owner_controller_ = owner_controller; @@ -99,7 +75,7 @@ new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {} queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification* note) { // Implicit copy of 'this' is fine. - accessibility_bridge_.Clear(); + accessibility_bridge_.reset(); owner_controller_ = nil; }]); @@ -122,8 +98,8 @@ new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {} FML_DCHECK(ios_surface_ != nullptr); if (accessibility_bridge_) { - accessibility_bridge_.Set(std::make_unique( - owner_controller_, this, owner_controller_.platformViewsController)); + accessibility_bridge_ = std::make_unique( + owner_controller_, this, owner_controller_.platformViewsController); } } @@ -162,22 +138,10 @@ new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {} // |PlatformView| void PlatformViewIOS::SetSemanticsEnabled(bool enabled) { - if (!owner_controller_) { - [FlutterLogger logWarning:@"Could not set semantics to enabled, this PlatformViewIOS has no " - "ViewController."]; - return; - } - if (enabled && !accessibility_bridge_) { - accessibility_bridge_.Set(std::make_unique( - owner_controller_, this, owner_controller_.platformViewsController)); - } else if (!enabled && accessibility_bridge_) { - accessibility_bridge_.Clear(); - } else { - PlatformView::SetSemanticsEnabled(enabled); - } + PlatformView::SetSemanticsEnabled(enabled); } -// |shell:PlatformView| +// |PlatformView| void PlatformViewIOS::SetAccessibilityFeatures(int32_t flags) { PlatformView::SetAccessibilityFeatures(flags); } @@ -187,6 +151,7 @@ new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {} flutter::SemanticsNodeUpdates update, flutter::CustomAccessibilityActionUpdates actions) { FML_DCHECK(owner_controller_); + FML_DCHECK(accessibility_bridge_); if (accessibility_bridge_) { accessibility_bridge_.get()->UpdateSemantics(std::move(update), actions); [[NSNotificationCenter defaultCenter] postNotificationName:FlutterSemanticsUpdateNotification @@ -194,6 +159,20 @@ new PlatformMessageHandlerIos(task_runners.GetPlatformTaskRunner())) {} } } +// |PlatformView| +void PlatformViewIOS::SetSemanticsTreeEnabled(bool enabled) { + FML_DCHECK(owner_controller_); + if (enabled) { + if (accessibility_bridge_) { + return; + } + accessibility_bridge_ = + std::make_unique(owner_controller_, this, platform_views_controller_); + } else { + accessibility_bridge_.reset(); + } +} + // |PlatformView| std::unique_ptr PlatformViewIOS::CreateVSyncWaiter() { return std::make_unique(task_runners_); 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 new file mode 100644 index 0000000000000..335763bd6d32c --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/platform_view_ios_test.mm @@ -0,0 +1,105 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "flutter/fml/thread.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/ios/platform_view_ios.h" + +FLUTTER_ASSERT_ARC + +namespace flutter { + +namespace { + +class MockDelegate : public PlatformView::Delegate { + public: + void OnPlatformViewCreated(std::unique_ptr surface) override {} + void OnPlatformViewDestroyed() override {} + void OnPlatformViewScheduleFrame() override {} + void OnPlatformViewAddView(int64_t view_id, + const ViewportMetrics& viewport_metrics, + AddViewCallback callback) override {} + void OnPlatformViewRemoveView(int64_t view_id, RemoveViewCallback callback) override {} + void OnPlatformViewSetNextFrameCallback(const fml::closure& closure) override {} + void OnPlatformViewSetViewportMetrics(int64_t view_id, const ViewportMetrics& metrics) override {} + const flutter::Settings& OnPlatformViewGetSettings() const override { return settings_; } + void OnPlatformViewDispatchPlatformMessage(std::unique_ptr message) override {} + void OnPlatformViewDispatchPointerDataPacket(std::unique_ptr packet) override { + } + void OnPlatformViewSendViewFocusEvent(const ViewFocusEvent& event) override {} + void OnPlatformViewDispatchSemanticsAction(int64_t view_id, + int32_t node_id, + SemanticsAction action, + fml::MallocMapping args) override {} + void OnPlatformViewSetSemanticsEnabled(bool enabled) override {} + void OnPlatformViewSetAccessibilityFeatures(int32_t flags) override {} + void OnPlatformViewRegisterTexture(std::shared_ptr texture) override {} + void OnPlatformViewUnregisterTexture(int64_t texture_id) override {} + void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} + + void LoadDartDeferredLibrary(intptr_t loading_unit_id, + std::unique_ptr snapshot_data, + std::unique_ptr snapshot_instructions) override { + } + void LoadDartDeferredLibraryError(intptr_t loading_unit_id, + const std::string error_message, + bool transient) override {} + void UpdateAssetResolverByType(std::unique_ptr updated_asset_resolver, + flutter::AssetResolver::AssetResolverType type) override {} + + flutter::Settings settings_; +}; + +} // namespace +} // namespace flutter + +@interface PlatformViewIOSTest : XCTestCase +@end + +@implementation PlatformViewIOSTest + +- (void)testSetSemanticsTreeEnabled { + flutter::MockDelegate mock_delegate; + auto thread = std::make_unique("PlatformViewIOSTest"); + auto thread_task_runner = thread->GetTaskRunner(); + flutter::TaskRunners runners(/*label=*/self.name.UTF8String, + /*platform=*/thread_task_runner, + /*raster=*/thread_task_runner, + /*ui=*/thread_task_runner, + /*io=*/thread_task_runner); + id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + id engine = OCMClassMock([FlutterEngine class]); + + id flutterViewController = OCMClassMock([FlutterViewController class]); + + OCMStub([flutterViewController isViewLoaded]).andReturn(NO); + OCMStub([flutterViewController engine]).andReturn(engine); + OCMStub([engine binaryMessenger]).andReturn(messenger); + + auto platform_view = std::make_unique( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kMetal, + /*platform_views_controller=*/nil, + /*task_runners=*/runners, + /*worker_task_runner=*/nil, + /*is_gpu_disabled_sync_switch=*/std::make_shared()); + fml::AutoResetWaitableEvent latch; + thread_task_runner->PostTask([&] { + platform_view->SetOwnerViewController(flutterViewController); + XCTAssertFalse(platform_view->GetAccessibilityBridge()); + platform_view->SetSemanticsTreeEnabled(true); + XCTAssertTrue(platform_view->GetAccessibilityBridge()); + platform_view->SetSemanticsTreeEnabled(false); + XCTAssertFalse(platform_view->GetAccessibilityBridge()); + latch.Signal(); + }); + latch.Wait(); + + [engine stopMocking]; +} + +@end diff --git a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart index dcea58d02e801..d8054555fadf4 100644 --- a/engine/src/flutter/shell/platform/embedder/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/embedder/fixtures/main.dart @@ -309,6 +309,7 @@ Future a11y_main() async { ) ..updateCustomAction(id: 21, label: 'Archive', hint: 'archive message'); + PlatformDispatcher.instance.setSemanticsTreeEnabled(true); PlatformDispatcher.instance.views.first.updateSemantics(builder.build()); signalNativeTest(); @@ -390,6 +391,7 @@ Future a11y_string_attributes() async { locale: null, ); + PlatformDispatcher.instance.setSemanticsTreeEnabled(true); PlatformDispatcher.instance.views.first.updateSemantics(builder.build()); signalNativeTest(); } @@ -1679,6 +1681,7 @@ Future a11y_main_multi_view() async { ); } + PlatformDispatcher.instance.setSemanticsTreeEnabled(true); for (final view in PlatformDispatcher.instance.views) { view.updateSemantics(createForView(view).build()); } diff --git a/engine/src/flutter/shell/platform/windows/fixtures/main.dart b/engine/src/flutter/shell/platform/windows/fixtures/main.dart index 8a7a5db8d94bc..0fcba55bced3b 100644 --- a/engine/src/flutter/shell/platform/windows/fixtures/main.dart +++ b/engine/src/flutter/shell/platform/windows/fixtures/main.dart @@ -475,6 +475,7 @@ Future sendSemanticsTreeInfo() async { return builder.build(); } + ui.PlatformDispatcher.instance.setSemanticsTreeEnabled(true); view1.updateSemantics(createSemanticsUpdate(view1.viewId + 1)); view2.updateSemantics(createSemanticsUpdate(view2.viewId + 1)); signal(); diff --git a/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart b/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart index c7e4559307130..4306a3a828c2e 100644 --- a/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart +++ b/engine/src/flutter/testing/ios_scenario_app/lib/src/locale_initialization.dart @@ -78,7 +78,7 @@ class LocaleInitialization extends Scenario { ); final SemanticsUpdate semanticsUpdate = semanticsUpdateBuilder.build(); - + view.platformDispatcher.setSemanticsTreeEnabled(true); view.updateSemantics(semanticsUpdate); } diff --git a/packages/flutter/lib/src/semantics/binding.dart b/packages/flutter/lib/src/semantics/binding.dart index d5e474b8b71eb..960fc2ef2e9ca 100644 --- a/packages/flutter/lib/src/semantics/binding.dart +++ b/packages/flutter/lib/src/semantics/binding.dart @@ -39,6 +39,7 @@ mixin SemanticsBinding on BindingBase { } }; _handleSemanticsEnabledChanged(); + addSemanticsEnabledListener(_handleFrameworkSemanticsEnabledChanged); } /// The current [SemanticsBinding], if one has been created. @@ -164,6 +165,10 @@ mixin SemanticsBinding on BindingBase { performSemanticsAction(decodedAction); } + void _handleFrameworkSemanticsEnabledChanged() { + platformDispatcher.setSemanticsTreeEnabled(semanticsEnabled); + } + /// Called whenever the platform requests an action to be performed on a /// [SemanticsNode]. /// diff --git a/packages/flutter/test/semantics/semantics_binding_set_semantics_tree_enabled_test.dart b/packages/flutter/test/semantics/semantics_binding_set_semantics_tree_enabled_test.dart new file mode 100644 index 0000000000000..c5578c78c89e1 --- /dev/null +++ b/packages/flutter/test/semantics/semantics_binding_set_semantics_tree_enabled_test.dart @@ -0,0 +1,35 @@ +// 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:ui'; + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('SemanticsHandle ensureSemantics calls setSemanticsTreeEnabled', () async { + final SemanticsTestBinding binding = SemanticsTestBinding(); + expect(binding.platformDispatcher.semanticsTreeEnabled, isFalse); + final SemanticsHandle handle = binding.ensureSemantics(); + expect(binding.platformDispatcher.semanticsTreeEnabled, isTrue); + handle.dispose(); + expect(binding.platformDispatcher.semanticsTreeEnabled, isFalse); + }); +} + +class SemanticsTestBinding extends AutomatedTestWidgetsFlutterBinding { + @override + TestPlatformDispatcherSpy get platformDispatcher => _platformDispatcherSpy; + static final TestPlatformDispatcherSpy _platformDispatcherSpy = TestPlatformDispatcherSpy( + platformDispatcher: PlatformDispatcher.instance, + ); +} + +class TestPlatformDispatcherSpy extends TestPlatformDispatcher { + TestPlatformDispatcherSpy({required super.platformDispatcher}); + bool semanticsTreeEnabled = false; + @override + void setSemanticsTreeEnabled(bool enabled) { + semanticsTreeEnabled = enabled; + } +} From 25094b9961dc1ac5b4d3229652e4688a16d5a5dd Mon Sep 17 00:00:00 2001 From: Simon Pham Date: Wed, 20 Aug 2025 00:36:11 +0700 Subject: [PATCH 118/720] fix: Android build fails when minSdk is set below 24 in build.gradle.kts (#173823) (#173825) fix: Android build fails when minSdk is set below 24 in build.gradle.kts (#173823, #173829) This PR separates the regex for determining minSdkVersion in groovy or kotlin gradle file when doing Android minSdkVersion migration. Fixes #173823. ## 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 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]. **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 [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: Reid Baker <1063596+reidbaker@users.noreply.github.com> --- .../lib/src/android/gradle_utils.dart | 11 +- .../migrations/min_sdk_version_migration.dart | 16 ++- .../android_project_migration_test.dart | 109 +++++++++++++++++- 3 files changed, 128 insertions(+), 8 deletions(-) diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart index 9ce9a60c24c2a..60cd86243d259 100644 --- a/packages/flutter_tools/lib/src/android/gradle_utils.dart +++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart @@ -171,8 +171,15 @@ final gradleOrgVersionMatch = RegExp( // This matches uncommented minSdkVersion lines in the module-level build.gradle // file which have minSdkVersion 16, 17, 18, 19, 20, 21, 22, 23 set with space sytax, // equals syntax and when using minSdk or minSdkVersion. -final tooOldMinSdkVersionMatch = RegExp( - r'(?<=^\s*)minSdk(Version)?\s=?\s?(1[6789]|2[0123])(?=\s*(?://|$))', +// Matches uncommented minSdkVersion lines using equals syntax (=) +final tooOldMinSdkVersionEqualsMatch = RegExp( + r'(?<=^\s*)minSdk(Version)?\s*=\s*(1[6789]|2[0123])(?=\s*(?://|$))', + multiLine: true, +); + +// Matches uncommented minSdkVersion lines using space syntax (no =) +final tooOldMinSdkVersionSpaceMatch = RegExp( + r'(?<=^\s*)minSdk(Version)?\s+(1[6789]|2[0123])(?=\s*(?://|$))', multiLine: true, ); diff --git a/packages/flutter_tools/lib/src/android/migrations/min_sdk_version_migration.dart b/packages/flutter_tools/lib/src/android/migrations/min_sdk_version_migration.dart index 56433278cfe35..1039eb1ed7859 100644 --- a/packages/flutter_tools/lib/src/android/migrations/min_sdk_version_migration.dart +++ b/packages/flutter_tools/lib/src/android/migrations/min_sdk_version_migration.dart @@ -15,6 +15,12 @@ import '../gradle_utils.dart'; @visibleForTesting const replacementMinSdkText = 'minSdkVersion flutter.minSdkVersion'; +@visibleForTesting +const groovyReplacementWithEquals = 'minSdkVersion = flutter.minSdkVersion'; + +@visibleForTesting +const kotlinReplacementMinSdkText = 'minSdk = flutter.minSdkVersion'; + @visibleForTesting const appGradleNotFoundWarning = 'Module level build.gradle file not found, skipping minSdkVersion migration.'; @@ -40,6 +46,14 @@ class MinSdkVersionMigration extends ProjectMigrator { @override String migrateFileContents(String fileContents) { - return fileContents.replaceAll(tooOldMinSdkVersionMatch, replacementMinSdkText); + if (_project.appGradleFile.path.endsWith('.kts')) { + // For Kotlin Gradle files, only the equals syntax is valid and we should use 'minSdk'. + return fileContents.replaceAll(tooOldMinSdkVersionEqualsMatch, kotlinReplacementMinSdkText); + } + + // For Groovy Gradle files, both space and equals syntax are valid, and the property name is 'minSdkVersion'. + return fileContents + .replaceAll(tooOldMinSdkVersionSpaceMatch, replacementMinSdkText) + .replaceAll(tooOldMinSdkVersionEqualsMatch, groovyReplacementWithEquals); } } diff --git a/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart b/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart index 52f7fe5d59519..ecdda2ce33cae 100644 --- a/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart @@ -118,6 +118,55 @@ dependencies {} '''; } +String sampleKotlinDslModuleGradleBuildFile(String minSdkVersionString) { + return r''' +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.telasdka" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.asset_sample" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + $minSdkVersionString + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} +'''; +} + final androidStudioDolphin = Version(2021, 3, 1); const _javaVersion17 = Version.withText(17, 0, 2, 'openjdk 17.0.2'); @@ -391,8 +440,8 @@ tasks.register("clean", Delete) { }); testWithoutContext('replace when api 22', () async { - const minSdkVersion20 = 'minSdkVersion = 22'; - project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion20)); + const minSdkVersion22 = 'minSdkVersion 22'; + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion22)); await migration.migrate(); expect( project.appGradleFile.readAsStringSync(), @@ -401,8 +450,8 @@ tasks.register("clean", Delete) { }); testWithoutContext('replace when api 23', () async { - const minSdkVersion20 = 'minSdk = 23'; - project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion20)); + const minSdkVersion23 = 'minSdkVersion 23'; + project.appGradleFile.writeAsStringSync(sampleModuleGradleBuildFile(minSdkVersion23)); await migration.migrate(); expect( project.appGradleFile.readAsStringSync(), @@ -474,7 +523,50 @@ tasks.register("clean", Delete) { await migration.migrate(); expect( project.appGradleFile.readAsStringSync(), - sampleModuleGradleBuildFile(replacementMinSdkText), + sampleModuleGradleBuildFile(groovyReplacementWithEquals), + ); + }); + }); + + group('migrate min sdk versions less than 24 to flutter.minSdkVersion - kotlin dsl', () { + late MemoryFileSystem memoryFileSystem; + late BufferLogger bufferLogger; + late FakeAndroidProject project; + late MinSdkVersionMigration migration; + + setUp(() { + memoryFileSystem = MemoryFileSystem.test(); + memoryFileSystem.currentDirectory.childDirectory('android').createSync(); + bufferLogger = BufferLogger.test(); + project = FakeKotlinDslAndroidProject( + root: memoryFileSystem.currentDirectory.childDirectory('android'), + ); + project.appGradleFile.parent.createSync(recursive: true); + migration = MinSdkVersionMigration(project, bufferLogger); + }); + + testWithoutContext('do nothing when already using ' + 'flutter.minSdkVersion', () async { + project.appGradleFile.writeAsStringSync( + sampleKotlinDslModuleGradleBuildFile(kotlinReplacementMinSdkText), + ); + await migration.migrate(); + expect( + project.appGradleFile.readAsStringSync(), + sampleKotlinDslModuleGradleBuildFile(kotlinReplacementMinSdkText), + ); + }); + + testWithoutContext('migrate when minSdkVersion is set ' + 'using = syntax', () async { + const equalsSyntaxMinSdkVersion19 = 'minSdk = 19'; + project.appGradleFile.writeAsStringSync( + sampleKotlinDslModuleGradleBuildFile(equalsSyntaxMinSdkVersion19), + ); + await migration.migrate(); + expect( + project.appGradleFile.readAsStringSync(), + sampleKotlinDslModuleGradleBuildFile(kotlinReplacementMinSdkText), ); }); }); @@ -549,6 +641,13 @@ class FakeAndroidProject extends Fake implements AndroidProject { File get appGradleFile => hostAppGradleRoot.childDirectory('app').childFile('build.gradle'); } +class FakeKotlinDslAndroidProject extends FakeAndroidProject { + FakeKotlinDslAndroidProject({required super.root, super.module, super.plugin}); + + @override + File get appGradleFile => hostAppGradleRoot.childDirectory('app').childFile('build.gradle.kts'); +} + class FakeAndroidStudio extends Fake implements AndroidStudio { FakeAndroidStudio({required Version? version}) { _version = version; From c07ba3f8b6236535d58906f4a4ec4cc17a98df2c Mon Sep 17 00:00:00 2001 From: Srivats Venkataraman <42980667+srivats22@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:38:08 -0400 Subject: [PATCH 119/720] [VPAT][A11y] AutoComplete dropdown option is missing button role (#173297) Fixes: https://github.com/flutter/flutter/issues/173058 This PR assigns a semantic role of a button to dropdown options. When using optionsBuilder, it automatically gets the button semantics. When the user defines: optionsViewBuilder, then it will use the default semantics. This is based on how the current code behaves ## 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 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]. **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 [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: chunhtai <47866232+chunhtai@users.noreply.github.com> --- .../lib/src/material/autocomplete.dart | 29 +++++++++-------- .../test/material/autocomplete_test.dart | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/packages/flutter/lib/src/material/autocomplete.dart b/packages/flutter/lib/src/material/autocomplete.dart index 77d11a9c76598..584b71321fb59 100644 --- a/packages/flutter/lib/src/material/autocomplete.dart +++ b/packages/flutter/lib/src/material/autocomplete.dart @@ -291,20 +291,23 @@ class _AutocompleteOptionsListState extends State<_Autocomplet itemCount: widget.options.length, itemBuilder: (BuildContext context, int index) { final T option = widget.options.elementAt(index); - return InkWell( - key: GlobalObjectKey(option), - onTap: () { - widget.onSelected(option); - }, - child: Builder( - builder: (BuildContext context) { - final bool highlight = highlightedIndex == index; - return Container( - color: highlight ? Theme.of(context).focusColor : null, - padding: const EdgeInsets.all(16.0), - child: Text(widget.displayStringForOption(option)), - ); + return Semantics( + button: true, + child: InkWell( + key: GlobalObjectKey(option), + onTap: () { + widget.onSelected(option); }, + child: Builder( + builder: (BuildContext context) { + final bool highlight = highlightedIndex == index; + return Container( + color: highlight ? Theme.of(context).focusColor : null, + padding: const EdgeInsets.all(16.0), + child: Text(widget.displayStringForOption(option)), + ); + }, + ), ), ); }, diff --git a/packages/flutter/test/material/autocomplete_test.dart b/packages/flutter/test/material/autocomplete_test.dart index f00f91b159961..d7776dc3405f4 100644 --- a/packages/flutter/test/material/autocomplete_test.dart +++ b/packages/flutter/test/material/autocomplete_test.dart @@ -730,4 +730,36 @@ void main() { final TextField field2 = find.byType(TextField).evaluate().first.widget as TextField; expect(field2.controller!.text, textSelection); }); + + testWidgets('autocomplete options have button semantics', (WidgetTester tester) async { + const Color highlightColor = Color(0xFF112233); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(focusColor: highlightColor), + home: Scaffold( + body: Autocomplete( + optionsBuilder: (TextEditingValue textEditingValue) { + return kOptions.where((String option) { + return option.contains(textEditingValue.text.toLowerCase()); + }); + }, + ), + ), + ), + ); + await tester.tap(find.byType(TextField)); + await tester.pump(); + await tester.enterText(find.byType(TextField), 'aa'); + await tester.pump(); + expect( + tester.getSemantics(find.text('aardvark')), + matchesSemantics( + isButton: true, + isFocusable: true, + hasTapAction: true, + hasFocusAction: true, + label: 'aardvark', + ), + ); + }); } From 27c78c102385fa7b1af451e4c81167a6164694cc Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Tue, 19 Aug 2025 15:55:32 -0400 Subject: [PATCH 120/720] Make _WindowsMessageHandler private --- .../lib/src/widgets/_window_win32.dart | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index aecbb2a13cd7c..0ddd2f409755e 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -57,14 +57,7 @@ See: https://github.com/flutter/flutter/issues/30701. /// [WindowingOwnerWin32.addMessageHandler] to begin receiving messages. /// When finished handling messages, implementations should deregister /// themselves with [WindowingOwnerWin32.removeMessageHandler]. -/// -/// {@macro flutter.widgets.windowing.experimental} -/// -/// See also: -/// -/// * [WindowingOwnerWin32], the class that manages these handlers. -@internal -abstract class WindowsMessageHandler { +abstract class _WindowsMessageHandler { /// Handles a window message. /// /// Returned value, if not null will be returned to the system as LRESULT @@ -121,7 +114,7 @@ class WindowingOwnerWin32 extends WindowingOwner { allocator.free(request); } - final List _messageHandlers = []; + final List<_WindowsMessageHandler> _messageHandlers = <_WindowsMessageHandler>[]; /// The [Allocator] used for allocating native memory in this owner. /// @@ -166,8 +159,7 @@ class WindowingOwnerWin32 extends WindowingOwner { /// /// * [WindowsMessageHandler], the interface for message handlers. /// * [WindowingOwnerWin32.removeMessageHandler], to remove message handlers. - @internal - void addMessageHandler(WindowsMessageHandler handler) { + void _addMessageHandler(_WindowsMessageHandler handler) { if (_messageHandlers.contains(handler)) { return; } @@ -185,8 +177,7 @@ class WindowingOwnerWin32 extends WindowingOwner { /// /// * [WindowsMessageHandler], the interface for message handlers. /// * [WindowingOwnerWin32.addMessageHandler], to register message handlers. - @internal - void removeMessageHandler(WindowsMessageHandler handler) { + void _removeMessageHandler(_WindowsMessageHandler handler) { _messageHandlers.remove(handler); } @@ -196,7 +187,7 @@ class WindowingOwnerWin32 extends WindowingOwner { ); final int handlesLength = _messageHandlers.length; - for (final WindowsMessageHandler handler in _messageHandlers) { + for (final _WindowsMessageHandler handler in _messageHandlers) { assert( _messageHandlers.length == handlesLength, 'Message handler list changed while processing message: $message', @@ -223,6 +214,23 @@ class WindowingOwnerWin32 extends WindowingOwner { } } +class _RegularWindowMesageHandler implements _WindowsMessageHandler { + _RegularWindowMesageHandler({required this.controller}); + + final RegularWindowControllerWin32 controller; + + @override + int? handleWindowsMessage( + FlutterView view, + HWND windowHandle, + int message, + int wParam, + int lParam, + ) { + return controller._handleWindowsMessage(view, windowHandle, message, wParam, lParam); + } +} + /// Implementation of [RegularWindowController] for the Windows platform. /// /// {@macro flutter.widgets.windowing.experimental} @@ -230,8 +238,7 @@ class WindowingOwnerWin32 extends WindowingOwner { /// See also: /// /// * [RegularWindowController], the base class for regular windows. -class RegularWindowControllerWin32 extends RegularWindowController - implements WindowsMessageHandler { +class RegularWindowControllerWin32 extends RegularWindowController { /// Creates a new regular window controller for Win32. /// /// When this constructor completes the native window has been created and @@ -256,7 +263,8 @@ class RegularWindowControllerWin32 extends RegularWindowController throw UnsupportedError(_kWindowingDisabledErrorMessage); } - owner.addMessageHandler(this); + _handler = _RegularWindowMesageHandler(controller: this); + owner._addMessageHandler(_handler); final int viewId = _Win32PlatformInterface.createWindow( _owner.allocator, PlatformDispatcher.instance.engineId!, @@ -272,6 +280,7 @@ class RegularWindowControllerWin32 extends RegularWindowController final WindowingOwnerWin32 _owner; final RegularWindowControllerDelegate _delegate; + late final _RegularWindowMesageHandler _handler; bool _destroyed = false; @override @@ -404,13 +413,11 @@ class RegularWindowControllerWin32 extends RegularWindowController } _Win32PlatformInterface.destroyWindow(getWindowHandle()); _destroyed = true; - _owner.removeMessageHandler(this); + _owner._removeMessageHandler(_handler); _delegate.onWindowDestroyed(); } - @override - @internal - int? handleWindowsMessage( + int? _handleWindowsMessage( FlutterView view, HWND windowHandle, int message, From 3bec8cb99d368cc972e63d1ca82119129e35349f Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Tue, 19 Aug 2025 13:51:30 -0400 Subject: [PATCH 121/720] Throwing error in WindowingOwnerWin32 constructor when isWindowingEnabled is false --- packages/flutter/lib/src/widgets/_window_win32.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/flutter/lib/src/widgets/_window_win32.dart b/packages/flutter/lib/src/widgets/_window_win32.dart index 0ddd2f409755e..0caf8ddbf4acc 100644 --- a/packages/flutter/lib/src/widgets/_window_win32.dart +++ b/packages/flutter/lib/src/widgets/_window_win32.dart @@ -101,6 +101,10 @@ class WindowingOwnerWin32 extends WindowingOwner { /// * [WindowingOwner], the abstract class that manages native windows. @internal WindowingOwnerWin32() : allocator = _CallocAllocator() { + if (!isWindowingEnabled) { + throw UnsupportedError(_kWindowingDisabledErrorMessage); + } + if (!Platform.isWindows) { throw UnsupportedError('Only available on the Win32 platform'); } From d12fc06d6b2799fe5d9a5134bf84ff464a3a226e Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Tue, 19 Aug 2025 14:05:39 -0400 Subject: [PATCH 122/720] Adding the windowing_test on Windows to CI --- .ci.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.ci.yaml b/.ci.yaml index dc71698519041..a5edd2db9ec88 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -6960,6 +6960,32 @@ targets: ["devicelab", "hostonly", "windows", "arm64"] task_name: windows_startup_test + - name: Windows windowing_test + recipe: devicelab/devicelab_drone + presubmit: true + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + tags: > + ["devicelab", "hostonly", "windows"] + task_name: windowing_test + + - name: Windows_arm64 windowing_test + recipe: devicelab/devicelab_drone + presubmit: true + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "vs_build", "version": "version:vs2019"} + ] + tags: > + ["devicelab", "hostonly", "windows", "arm64"] + task_name: windowing_test + - name: Windows flutter_tool_startup__windows recipe: devicelab/devicelab_drone presubmit: false From 37d855e9961e12782044d114d3fe1a1826ddf152 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Tue, 19 Aug 2025 14:10:35 -0400 Subject: [PATCH 123/720] Need to mark with bringup --- .ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.ci.yaml b/.ci.yaml index a5edd2db9ec88..26835649654cc 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -6963,6 +6963,7 @@ targets: - name: Windows windowing_test recipe: devicelab/devicelab_drone presubmit: true + bringup: true timeout: 60 properties: dependencies: >- @@ -6976,6 +6977,7 @@ targets: - name: Windows_arm64 windowing_test recipe: devicelab/devicelab_drone presubmit: true + bringup: true timeout: 60 properties: dependencies: >- From e55467107449f6505097b0ca98fda68addc13152 Mon Sep 17 00:00:00 2001 From: Rushikeshbhavsar20 <56561849+Rushikeshbhavsar20@users.noreply.github.com> Date: Tue, 19 Aug 2025 23:54:25 +0530 Subject: [PATCH 124/720] Improve Stack widget error message for bounded constraints (#173352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Description This PR improves the assertion error message in the Stack widget when it receives unbounded constraints from its parent (e.g., when placed directly inside a Column or ListView without a bounding widget). ✅ What Changed Replaced a generic assertion: Screenshot 2025-08-06 213549 With a detailed, developer-friendly message: Screenshot 2025-08-07 011126 Fixes: #172481 This PR addresses the confusing assertion error when a Stack is given unbounded constraints. By adding a descriptive message, developers will have clearer guidance on how to resolve layout issues involving Stack inside widgets like Column or ListView. ## 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 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. 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. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [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/rendering/stack.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/rendering/stack.dart b/packages/flutter/lib/src/rendering/stack.dart index 18591a95c169e..baecd0e3010a8 100644 --- a/packages/flutter/lib/src/rendering/stack.dart +++ b/packages/flutter/lib/src/rendering/stack.dart @@ -660,7 +660,15 @@ class RenderStack extends RenderBox size = constraints.biggest; } - assert(size.isFinite); + assert( + size.isFinite, + 'A Stack requires bounded constraints from its parent. ' + 'This error commonly occurs when a Stack is placed inside a widget like Column, ' + 'ListView, or other widgets that do not constrain their children. ' + 'To fix this, wrap the Stack in a widget that provides finite height and width constraints, ' + 'such as a SizedBox or ConstrainedBox. ' + 'Use Expanded only if the parent is a Flex widget like Row or Column.', + ); return size; } From bdc2249933e0b04c5fc3877792b583c313f74b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20Gasterst=C3=A4dt?= <131267808+SvenGasterstaedt@users.noreply.github.com> Date: Tue, 19 Aug 2025 20:24:27 +0200 Subject: [PATCH 125/720] Check that the windows architecture is 64-bit and not the process architecture (#174019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes the platform detection for Windows - so that it can be invoked from a 32-bit process when we are on a 64-bit system. This is the case for example 'make' for windows which only comes with a 32-bit version. _PROCESSOR_ARCHITEW6432_ is only set if the process architecture differs from the platform architecture. See https://learn.microsoft.com/de-de/windows/win32/winprog64/wow64-implementation-details#environment-variables Fixes https://github.com/flutter/flutter/issues/174017 *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 - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] 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]. - [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]. **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 [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: Matan Lurey Co-authored-by: Loïc Sharma <737941+loic-sharma@users.noreply.github.com> --- bin/internal/shared.bat | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bin/internal/shared.bat b/bin/internal/shared.bat index 152aafbd19cb2..1a55bffb11b98 100644 --- a/bin/internal/shared.bat +++ b/bin/internal/shared.bat @@ -13,10 +13,15 @@ REM -------------------------------------------------------------------------- SETLOCAL -REM Ensure we are runnng on 64-bit windows (32-bit is not supported) +REM Ensure we are running on 64-bit Windows (32-bit is not supported). +REM If this is a 32-bit process emulated by WOW64, +REM PROCESSOR_ARCHITECTURE is the process architecture and +REM PROCESSOR_ARCHITEW6432 is the processor architecture. IF "%PROCESSOR_ARCHITECTURE%"=="x86" ( - ECHO Flutter requires 64-bit versions of Windows - EXIT 1 + IF "%PROCESSOR_ARCHITEW6432%"=="" ( + ECHO Flutter requires 64-bit versions of Windows + EXIT 1 + ) ) SET flutter_tools_dir=%FLUTTER_ROOT%\packages\flutter_tools From 84662834f94e31c7431e7e2c7939c80bac8018a9 Mon Sep 17 00:00:00 2001 From: Kostia Sokolovskyi Date: Tue, 19 Aug 2025 20:26:04 +0200 Subject: [PATCH 126/720] Add Shift+Enter shortcut example for TextField. (#167952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/flutter/flutter/issues/167902 This PR adds a new `TextField` example which shows how to use `Shortcuts` and `Actions` widgets to create a custom `Shift+Enter` keyboard shortcut for inserting a new line.