From 72ee26e314f471012ee4ee60b5cf1831c0ed6a45 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Wed, 9 Apr 2025 15:57:49 -0700 Subject: [PATCH] Initialize Flutter Beta (`flutter-3.32-candidate.0`) (#166783) Closes https://github.com/flutter/flutter/issues/166811. ```sh $ dev/conductor/bin/conductor start \ --candidate-branch=flutter-3.32-candidate.0 \ --release-channel=beta \ --github-username=matanlurey \ --dart-revision=0d6811928830b87e36a0f49eb7fe554c308d3699 ``` --- .ci.yaml | 1 + DEPS | 16 +- bin/internal/flutter_packages.version | 2 +- bin/internal/release-candidate-branch.version | 1 + .../flutter_gallery/pubspec.yaml | 4 +- dev/integration_tests/link_hook/pubspec.yaml | 4 +- docs/roadmap/Roadmap.md | 2 +- .../flutter/ci/licenses_golden/licenses_dart | 4 +- .../flutter/ci/licenses_golden/licenses_skia | 2 +- .../display_list/aiks_dl_basic_unittests.cc | 18 ++ .../display_list/aiks_dl_blur_unittests.cc | 51 ---- .../flutter/impeller/display_list/paint.cc | 3 +- engine/src/flutter/impeller/geometry/matrix.h | 13 ++ .../impeller/geometry/matrix_unittests.cc | 9 + .../renderer/backend/vulkan/allocator_vk.cc | 19 +- .../backend/vulkan/allocator_vk_unittests.cc | 20 ++ .../backend/vulkan/texture_source_vk.h | 1 - .../lib/src/engine/semantics/scrollable.dart | 186 +++++++-------- .../test/engine/semantics/semantics_test.dart | 132 ++++++----- engine/src/flutter/pubspec.yaml | 4 - engine/src/flutter/web_sdk/pubspec.yaml | 4 - .../sliver/sliver_ensure_semantics.0.dart | 207 +++++++++++++++++ .../sliver_ensure_semantics.0_test.dart | 14 ++ .../flutter/lib/src/material/carousel.dart | 9 + .../flutter/lib/src/material/list_tile.dart | 18 +- .../lib/src/material/list_tile_theme.dart | 14 +- .../lib/src/rendering/proxy_sliver.dart | 8 + .../flutter/lib/src/rendering/sliver.dart | 23 ++ .../rendering/sliver_multi_box_adaptor.dart | 12 + .../lib/src/rendering/sliver_tree.dart | 3 +- .../flutter/lib/src/rendering/viewport.dart | 20 +- packages/flutter/lib/src/widgets/sliver.dart | 45 ++++ .../test/cupertino/date_picker_test.dart | 6 +- .../flutter/test/material/list_tile_test.dart | 132 +++++++++++ .../test/material/list_tile_theme_test.dart | 219 ++++++++++++++++++ .../test/widgets/color_filter_test.dart | 4 +- .../test/widgets/selectable_text_test.dart | 5 +- .../test/widgets/shader_mask_test.dart | 4 +- .../test/widgets/sliver_tree_test.dart | 44 ++++ .../flutter/test/widgets/slivers_test.dart | 95 +++++--- packages/flutter_tools/lib/executable.dart | 2 - .../lib/src/commands/widget_preview.dart | 56 ----- .../lib/src/compute_dev_dependencies.dart | 8 +- .../lib/src/custom_devices/custom_device.dart | 24 +- packages/flutter_tools/lib/src/dart/pub.dart | 28 ++- .../src/runner/flutter_command_runner.dart | 7 - .../lib/src/widget_preview/dtd_services.dart | 101 -------- packages/flutter_tools/pubspec.yaml | 4 +- .../static/custom-devices.schema.json | 4 +- .../templates/template_manifest.json | 1 - .../lib/src/dtd_services.dart.tmpl | 33 --- .../src/widget_preview_rendering.dart.tmpl | 3 - .../widget_preview_scaffold/pubspec.yaml.tmpl | 1 - .../widget_preview/widget_preview_test.dart | 3 - .../permeable/widget_preview_test.dart | 3 - .../compute_dev_dependencies_test.dart | 28 +++ .../custom_devices/custom_device_test.dart | 95 ++++++++ .../general.shard/dart/pub_deps_test.dart | 12 +- .../widget_preview_test.dart | 48 +--- .../lib/src/dtd_services.dart | 33 --- .../lib/src/widget_preview_rendering.dart | 3 - .../widget_preview_scaffold/pubspec.yaml | 14 +- 62 files changed, 1258 insertions(+), 631 deletions(-) create mode 100644 bin/internal/release-candidate-branch.version create mode 100644 examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart create mode 100644 examples/api/test/widgets/sliver/sliver_ensure_semantics.0_test.dart delete mode 100644 packages/flutter_tools/lib/src/widget_preview/dtd_services.dart delete mode 100644 packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd_services.dart.tmpl delete mode 100644 packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/dtd_services.dart diff --git a/.ci.yaml b/.ci.yaml index 4033c1fcfa..2571271d5e 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -5132,6 +5132,7 @@ targets: - name: Mac_ios microbenchmarks_ios recipe: devicelab/devicelab_drone presubmit: false + bringup: true timeout: 60 properties: tags: > diff --git a/DEPS b/DEPS index d4eb7b945f..c57b9b2fe5 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': '7b929584566c2c20f59d692301a40e55528a83a7', + 'skia_revision': 'ac01f9306a0c08acf128d37bbd7b3e199525cc40', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. @@ -56,16 +56,16 @@ 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': '87965ab4864e444c521023820eb06e569d007059', + 'dart_revision': '0d6811928830b87e36a0f49eb7fe554c308d3699', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py 'dart_binaryen_rev': 'b4bdcc33115b31758c56b83bb9de4642c411a042', - 'dart_boringssl_rev': 'e68438b863afaa3e81e1771b91819817780f3b60', + 'dart_boringssl_rev': '8d8df26fc54f6e5fb0bf404a76ce973c0413eafc', 'dart_core_rev': '7a80178ca72b01b0efb99a9a9a654d83ca21d6b7', - 'dart_devtools_rev': 'f10e8df8c517fb0412b9a66c626581867c9c267d', + 'dart_devtools_rev': '1fb2f4ce5099042b7f2dfa93dec675a21861d21f', 'dart_ecosystem_rev': '391a80ccb774cfebe4865bcd7e933d1ab016eea5', - 'dart_http_rev': '32d5ffcc8d0d5ce9a3a76a293230fa70ec2cc88f', + 'dart_http_rev': '6fabf06b90d962cf9a6c009bbe919902ff1a1471', 'dart_i18n_rev': 'de1943629469719bf34269bf90fcdbe9334a73f3', 'dart_libprotobuf_rev': '24487dd1045c7f3d64a21f38a3f0c06cc4cf2edb', 'dart_perfetto_rev': '13ce0c9e13b0940d2476cd0cff2301708a9a2e2b', @@ -73,7 +73,7 @@ vars = { 'dart_protobuf_rev': '1aaa332af75c61ff32739821f7ec52186ff18d4c', 'dart_pub_rev': 'b2c03b448a47fdd52800609b9222cd737be3a934', 'dart_sync_http_rev': 'dc54465f07d9652875deeade643256dafa2fbc6c', - 'dart_tools_rev': '62bc13bc086a66ce9a6a3e64865c82d17a1379b3', + 'dart_tools_rev': '8d49319b95912a5dff3d7ba4cd861458c55c5608', 'dart_vector_math_rev': 'f08d7d2652e9ecf7d8f8605d9983335174511c95', 'dart_web_rev': '5a39fdc396ae40344308975140343c23b6863261', 'dart_webdev_rev': '697f2f7f56517b0678c6256e0834778905acfc0d', @@ -308,7 +308,7 @@ deps = { Var('chromium_git') + '/external/github.com/WebAssembly/binaryen.git@b4bdcc33115b31758c56b83bb9de4642c411a042', 'engine/src/flutter/third_party/dart/third_party/devtools': - {'dep_type': 'cipd', 'packages': [{'package': 'dart/third_party/flutter/devtools', 'version': 'git_revision:f10e8df8c517fb0412b9a66c626581867c9c267d'}]}, + {'dep_type': 'cipd', 'packages': [{'package': 'dart/third_party/flutter/devtools', 'version': 'git_revision:1fb2f4ce5099042b7f2dfa93dec675a21861d21f'}]}, 'engine/src/flutter/third_party/dart/third_party/pkg/core': Var('dart_git') + '/core.git' + '@' + Var('dart_core_rev'), @@ -332,7 +332,7 @@ deps = { Var('dart_git') + '/leak_tracker.git@f5620600a5ce1c44f65ddaa02001e200b096e14c', 'engine/src/flutter/third_party/dart/third_party/pkg/native': - Var('dart_git') + '/native.git@75f3408cd72c1e217f162fb5e43f4cdf1d34e71d', + Var('dart_git') + '/native.git@4928765d7681ad8a4211c5977afd2f93f50b3f65', 'engine/src/flutter/third_party/dart/third_party/pkg/protobuf': Var('dart_git') + '/protobuf.git' + '@' + Var('dart_protobuf_rev'), diff --git a/bin/internal/flutter_packages.version b/bin/internal/flutter_packages.version index 78e0e167d4..052f78fd6d 100644 --- a/bin/internal/flutter_packages.version +++ b/bin/internal/flutter_packages.version @@ -1 +1 @@ -267ac7b66308ad34b6ce14c1f5399ab0691f9ede +2405f6a2b7e1664e2779030c6b651676ecbe7651 diff --git a/bin/internal/release-candidate-branch.version b/bin/internal/release-candidate-branch.version new file mode 100644 index 0000000000..d8027b669f --- /dev/null +++ b/bin/internal/release-candidate-branch.version @@ -0,0 +1 @@ +flutter-3.32-candidate.0 diff --git a/dev/integration_tests/flutter_gallery/pubspec.yaml b/dev/integration_tests/flutter_gallery/pubspec.yaml index 488f700520..c4d0019608 100644 --- a/dev/integration_tests/flutter_gallery/pubspec.yaml +++ b/dev/integration_tests/flutter_gallery/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: characters: 1.4.0 # 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" csslib: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - html: 0.15.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + html: 0.15.5+1 # 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.16.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" @@ -272,4 +272,4 @@ flutter: - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Regular.ttf - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Light.ttf -# PUBSPEC CHECKSUM: deed +# PUBSPEC CHECKSUM: 054a diff --git a/dev/integration_tests/link_hook/pubspec.yaml b/dev/integration_tests/link_hook/pubspec.yaml index dfac7e28b0..290b0c80ed 100644 --- a/dev/integration_tests/link_hook/pubspec.yaml +++ b/dev/integration_tests/link_hook/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: dev_dependencies: ffi: 2.1.4 - ffigen: 18.0.0 + ffigen: 18.1.0 flutter_lints: 5.0.0 test: 1.25.15 @@ -68,4 +68,4 @@ dev_dependencies: webkit_inspection_protocol: 1.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" yaml_edit: 2.2.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" -# PUBSPEC CHECKSUM: a861 +# PUBSPEC CHECKSUM: bf62 diff --git a/docs/roadmap/Roadmap.md b/docs/roadmap/Roadmap.md index 70a7604fd6..7d3ce83d85 100644 --- a/docs/roadmap/Roadmap.md +++ b/docs/roadmap/Roadmap.md @@ -2,7 +2,7 @@ In the interest of transparency, we want to share high-level details of our road Our plans will evolve over time based on customer feedback and new market opportunities. We will use our surveys and feedback on GitHub issues to prioritize work. The list here shouldn't be viewed either as exhaustive nor a promise that we will complete all this work. If you have feedback about what you think we should work on, we encourage you to get in touch by [filing an issue](https://github.com/flutter/flutter/issues/new/choose), or using the "thumbs-up" emoji reaction on an issue's first comment. Because Flutter is an open source project, we invite contributions both towards the themes presented below and in other areas. -_If you are a contributor or team of contributors with long-term plans for [contributing to Flutter](../../CONTRIBUTING.md), and would like your planned efforts reflected in the roadmap, please reach out to Hixie (ian@hixie.ch)._ +_If you are a contributor or team of contributors with long-term plans for [contributing to Flutter](../../CONTRIBUTING.md), and would like your planned efforts reflected in the roadmap, please reach via email to `roadmap-input@flutter.dev`._ # 2025 diff --git a/engine/src/flutter/ci/licenses_golden/licenses_dart b/engine/src/flutter/ci/licenses_golden/licenses_dart index 6680f27705..df73f9d8f9 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_dart +++ b/engine/src/flutter/ci/licenses_golden/licenses_dart @@ -1,4 +1,4 @@ -Signature: 7582ce3fcdf2e515db0132d7c32bf1b3 +Signature: c96f6439e1d3cadfbb6adc790efdcc37 ==================================================================================================== LIBRARY: dart @@ -4862,7 +4862,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. -You may obtain a copy of this library's Source Code Form from: https://dart.googlesource.com/sdk/+/4293d50dd30d5469fac05af7470148977327bbe8 +You may obtain a copy of this library's Source Code Form from: https://dart.googlesource.com/sdk/+/0d6811928830b87e36a0f49eb7fe554c308d3699 /third_party/fallback_root_certificates/ ==================================================================================================== diff --git a/engine/src/flutter/ci/licenses_golden/licenses_skia b/engine/src/flutter/ci/licenses_golden/licenses_skia index 935f8d33d2..63ffe75d7d 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_skia +++ b/engine/src/flutter/ci/licenses_golden/licenses_skia @@ -1,4 +1,4 @@ -Signature: cc16851918508797c4c52f81aeae260a +Signature: 6385fd7cd2f7be2e7ceee9d4b05eaf04 ==================================================================================================== LIBRARY: etc1 diff --git a/engine/src/flutter/impeller/display_list/aiks_dl_basic_unittests.cc b/engine/src/flutter/impeller/display_list/aiks_dl_basic_unittests.cc index 2f9090e2be..1767eaa826 100644 --- a/engine/src/flutter/impeller/display_list/aiks_dl_basic_unittests.cc +++ b/engine/src/flutter/impeller/display_list/aiks_dl_basic_unittests.cc @@ -938,6 +938,7 @@ TEST_P(AiksTest, ImageColorSourceEffectTransform) { // Scale { + builder.Save(); builder.Translate(100, 0); builder.Scale(100, 100); DlPaint paint; @@ -948,6 +949,23 @@ TEST_P(AiksTest, ImageColorSourceEffectTransform) { DlImageSampling::kNearestNeighbor, &matrix)); builder.DrawRect(DlRect::MakeLTRB(0, 0, 1, 1), paint); + builder.Restore(); + } + + // Perspective + { + builder.Save(); + builder.Translate(150, 150); + DlPaint paint; + + DlMatrix matrix = + DlMatrix::MakePerspective(Radians{0.5}, ISize{200, 200}, 0.05, 1); + paint.setColorSource(DlColorSource::MakeImage( + texture, DlTileMode::kRepeat, DlTileMode::kRepeat, + DlImageSampling::kNearestNeighbor, &matrix)); + + builder.DrawRect(DlRect::MakeLTRB(0, 0, 200, 200), paint); + builder.Restore(); } ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); diff --git a/engine/src/flutter/impeller/display_list/aiks_dl_blur_unittests.cc b/engine/src/flutter/impeller/display_list/aiks_dl_blur_unittests.cc index c7d67aa4e0..04243ff599 100644 --- a/engine/src/flutter/impeller/display_list/aiks_dl_blur_unittests.cc +++ b/engine/src/flutter/impeller/display_list/aiks_dl_blur_unittests.cc @@ -1253,57 +1253,6 @@ TEST_P(AiksTest, BlurredRectangleWithShader) { ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); } -TEST_P(AiksTest, GaussianBlurWithoutDecalSupport) { - if (GetParam() != PlaygroundBackend::kMetal) { - GTEST_SKIP() - << "This backend doesn't yet support setting device capabilities."; - } - if (!WillRenderSomething()) { - // Sometimes these tests are run without playgrounds enabled which is - // pointless for this test since we are asserting that - // `SupportsDecalSamplerAddressMode` is called. - GTEST_SKIP() << "This test requires playgrounds."; - } - - std::shared_ptr old_capabilities = - GetContext()->GetCapabilities(); - auto mock_capabilities = std::make_shared(); - EXPECT_CALL(*mock_capabilities, SupportsDecalSamplerAddressMode()) - .Times(::testing::AtLeast(1)) - .WillRepeatedly(::testing::Return(false)); - FLT_FORWARD(mock_capabilities, old_capabilities, GetDefaultColorFormat); - FLT_FORWARD(mock_capabilities, old_capabilities, GetDefaultStencilFormat); - FLT_FORWARD(mock_capabilities, old_capabilities, - GetDefaultDepthStencilFormat); - FLT_FORWARD(mock_capabilities, old_capabilities, SupportsOffscreenMSAA); - FLT_FORWARD(mock_capabilities, old_capabilities, - SupportsImplicitResolvingMSAA); - FLT_FORWARD(mock_capabilities, old_capabilities, SupportsReadFromResolve); - FLT_FORWARD(mock_capabilities, old_capabilities, SupportsFramebufferFetch); - FLT_FORWARD(mock_capabilities, old_capabilities, SupportsSSBO); - FLT_FORWARD(mock_capabilities, old_capabilities, SupportsCompute); - FLT_FORWARD(mock_capabilities, old_capabilities, - SupportsTextureToTextureBlits); - FLT_FORWARD(mock_capabilities, old_capabilities, GetDefaultGlyphAtlasFormat); - FLT_FORWARD(mock_capabilities, old_capabilities, SupportsTriangleFan); - FLT_FORWARD(mock_capabilities, old_capabilities, SupportsPrimitiveRestart); - ASSERT_TRUE(SetCapabilities(mock_capabilities).ok()); - - auto texture = DlImageImpeller::Make(CreateTextureForFixture("boston.jpg")); - - DisplayListBuilder builder; - builder.Scale(GetContentScale().x * 0.5, GetContentScale().y * 0.5); - - DlPaint paint; - paint.setColor(DlColor::kBlack()); - builder.DrawPaint(paint); - - auto blur_filter = DlImageFilter::MakeBlur(20, 20, DlTileMode::kDecal); - paint.setImageFilter(blur_filter); - builder.DrawImage(texture, DlPoint(200, 200), {}, &paint); - ASSERT_TRUE(OpenPlaygroundHere(builder.Build())); -} - // This addresses a bug where tiny blurs could result in mip maps that beyond // the limits for the textures used for blurring. // See also: b/323402168 diff --git a/engine/src/flutter/impeller/display_list/paint.cc b/engine/src/flutter/impeller/display_list/paint.cc index ef99300258..93d8d694fa 100644 --- a/engine/src/flutter/impeller/display_list/paint.cc +++ b/engine/src/flutter/impeller/display_list/paint.cc @@ -197,7 +197,8 @@ std::shared_ptr Paint::CreateContents() const { image_color_source->vertical_tile_mode()); auto sampler_descriptor = skia_conversions::ToSamplerDescriptor(image_color_source->sampling()); - auto effect_transform = image_color_source->matrix(); + // See https://github.com/flutter/flutter/issues/165205 + flutter::DlMatrix effect_transform = image_color_source->matrix().To3x3(); auto contents = std::make_shared(); contents->SetOpacityFactor(color.alpha); diff --git a/engine/src/flutter/impeller/geometry/matrix.h b/engine/src/flutter/impeller/geometry/matrix.h index fb4dc95e86..eb032b292b 100644 --- a/engine/src/flutter/impeller/geometry/matrix.h +++ b/engine/src/flutter/impeller/geometry/matrix.h @@ -247,6 +247,19 @@ struct Matrix { // clang-format on } + // Converts the second row/col to identity to make this an equivalent + // to a Skia 3x3 Matrix. + constexpr Matrix To3x3() const { + // clang-format off + return Matrix( + m[0], m[1], 0, m[3], + m[4], m[5], 0, m[7], + 0, 0, 1, 0, + m[12], m[13], 0, m[15] + ); + // clang-format on + } + constexpr Matrix Translate(const Vector3& t) const { // clang-format off return Matrix(m[0], m[1], m[2], m[3], diff --git a/engine/src/flutter/impeller/geometry/matrix_unittests.cc b/engine/src/flutter/impeller/geometry/matrix_unittests.cc index 6c2ef35228..719affb438 100644 --- a/engine/src/flutter/impeller/geometry/matrix_unittests.cc +++ b/engine/src/flutter/impeller/geometry/matrix_unittests.cc @@ -313,5 +313,14 @@ TEST(MatrixTest, MakeScaleTranslate) { Matrix::MakeTranslation({0, 0, 0}) * Matrix::MakeScale({0, 0, 0}))); } +TEST(MatrixTest, To3x3) { + Matrix x(1.0, 0.0, 4.0, 0.0, // + 0.0, 1.0, 4.0, 0.0, // + 6.0, 5.0, 111.0, 7.0, // + 0.0, 0.0, 9.0, 1.0); + + EXPECT_TRUE(MatrixNear(x.To3x3(), Matrix())); +} + } // namespace testing } // namespace impeller diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk.cc index f66bbab100..9bd546340a 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk.cc @@ -5,6 +5,7 @@ #include "impeller/renderer/backend/vulkan/allocator_vk.h" #include +#include #include "flutter/fml/memory/ref_ptr.h" #include "flutter/fml/trace_event.h" @@ -12,6 +13,7 @@ #include "impeller/core/formats.h" #include "impeller/renderer/backend/vulkan/capabilities_vk.h" #include "impeller/renderer/backend/vulkan/device_buffer_vk.h" +#include "impeller/renderer/backend/vulkan/device_holder_vk.h" #include "impeller/renderer/backend/vulkan/formats_vk.h" #include "impeller/renderer/backend/vulkan/texture_vk.h" #include "vulkan/vulkan_enums.hpp" @@ -405,9 +407,10 @@ class AllocatedTextureSourceVK final : public TextureSourceVK { return; } - resource_.Swap(ImageResource(ImageVMA{allocator, allocation, image}, - std::move(image_view), - std::move(rt_image_view))); + resource_.Swap(ImageResource( + ImageVMA{allocator, allocation, image}, std::move(image_view), + std::move(rt_image_view), context.GetResourceAllocator(), + context.GetDeviceHolder())); is_valid_ = true; } @@ -429,6 +432,8 @@ class AllocatedTextureSourceVK final : public TextureSourceVK { private: struct ImageResource { + std::shared_ptr device_holder; + std::shared_ptr allocator; UniqueImageVMA image; vk::UniqueImageView image_view; vk::UniqueImageView rt_image_view; @@ -437,8 +442,12 @@ class AllocatedTextureSourceVK final : public TextureSourceVK { ImageResource(ImageVMA p_image, vk::UniqueImageView p_image_view, - vk::UniqueImageView p_rt_image_view) - : image(p_image), + vk::UniqueImageView p_rt_image_view, + std::shared_ptr allocator, + std::shared_ptr device_holder) + : device_holder(std::move(device_holder)), + allocator(std::move(allocator)), + image(p_image), image_view(std::move(p_image_view)), rt_image_view(std::move(p_rt_image_view)) {} diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk_unittests.cc b/engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk_unittests.cc index 94cb0dec5b..0cf8930207 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk_unittests.cc +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/allocator_vk_unittests.cc @@ -5,6 +5,7 @@ #include "flutter/testing/testing.h" // IWYU pragma: keep #include "gtest/gtest.h" #include "impeller/base/allocation_size.h" +#include "impeller/core/allocator.h" #include "impeller/core/device_buffer.h" #include "impeller/core/device_buffer_descriptor.h" #include "impeller/core/formats.h" @@ -73,6 +74,25 @@ TEST(AllocatorVKTest, MemoryTypeSelectionTwoHeap) { EXPECT_EQ(AllocatorVK::FindMemoryTypeIndex(4, properties), -1); } +TEST(AllocatorVKTest, ImageResourceKeepsVulkanDeviceAlive) { + std::shared_ptr texture; + std::weak_ptr weak_allocator; + { + auto const context = MockVulkanContextBuilder().Build(); + weak_allocator = context->GetResourceAllocator(); + auto allocator = context->GetResourceAllocator(); + + texture = allocator->CreateTexture(TextureDescriptor{ + .storage_mode = StorageMode::kDevicePrivate, + .format = PixelFormat::kR8G8B8A8UNormInt, + .size = {1, 1}, + }); + context->Shutdown(); + } + + ASSERT_TRUE(weak_allocator.lock()); +} + #ifdef IMPELLER_DEBUG TEST(AllocatorVKTest, RecreateSwapchainWhenSizeChanges) { diff --git a/engine/src/flutter/impeller/renderer/backend/vulkan/texture_source_vk.h b/engine/src/flutter/impeller/renderer/backend/vulkan/texture_source_vk.h index d1498d0ba2..77c78be160 100644 --- a/engine/src/flutter/impeller/renderer/backend/vulkan/texture_source_vk.h +++ b/engine/src/flutter/impeller/renderer/backend/vulkan/texture_source_vk.h @@ -12,7 +12,6 @@ #include "impeller/renderer/backend/vulkan/shared_object_vk.h" #include "impeller/renderer/backend/vulkan/vk.h" #include "impeller/renderer/backend/vulkan/yuv_conversion_vk.h" -#include "vulkan/vulkan_handles.hpp" namespace impeller { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart index c5fb4cf202..90814fc004 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/scrollable.dart @@ -1,6 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:ui/src/engine.dart'; @@ -9,20 +10,10 @@ import 'package:ui/ui.dart' as ui; /// Implements vertical and horizontal scrolling functionality for semantics /// objects. /// -/// Scrolling is implemented using a "joystick" method. The absolute value of -/// "scrollTop" in HTML is not important. We only need to know in whether the -/// value changed in the positive or negative direction. If it changes in the -/// positive direction we send a [ui.SemanticsAction.scrollUp]. Otherwise, we -/// send [ui.SemanticsAction.scrollDown]. The actual scrolling is then handled -/// by the framework and we receive a [ui.SemanticsUpdate] containing the new -/// [scrollPosition] and child positions. -/// -/// "scrollTop" or "scrollLeft" is always reset to an arbitrarily chosen non- -/// zero "neutral" scroll position value. This is done so we have a -/// predictable range of DOM scroll position values. When the amount of -/// contents is less than the size of the viewport the browser snaps -/// "scrollTop" back to zero. If there is more content than available in the -/// viewport "scrollTop" may take positive values. +/// Scrolling is controlled by sending the current DOM scroll position in a +/// [ui.SemanticsAction.scrollToOffset] to the framework where it applies the +/// value to its scrollable and the engine receives a [ui.SemanticsUpdate] +/// containing the new [SemanticsObject.scrollPosition] and child positions. class SemanticScrollable extends SemanticRole { SemanticScrollable(SemanticsObject semanticsObject) : super.withBasics( @@ -39,81 +30,61 @@ class SemanticScrollable extends SemanticRole { /// Disables browser-driven scrolling in the presence of pointer events. GestureModeCallback? _gestureModeListener; - /// DOM element used as a workaround for: https://github.com/flutter/flutter/issues/104036 - /// - /// When the assistive technology gets to the last element of the scrollable - /// list, the browser thinks the scrollable area doesn't have any more content, - /// so it overrides the value of "scrollTop"/"scrollLeft" with zero. As a result, - /// the user can't scroll back up/left. - /// - /// As a workaround, we add this DOM element and set its size to - /// [canonicalNeutralScrollPosition] so the browser believes - /// that the scrollable area still has some more content, and doesn't override - /// scrollTop/scrollLetf with zero. + /// DOM element used to indicate to the browser the total quantity of available + /// content under this scrollable area. This element is sized based on the + /// total scroll extent calculated by scrollExtentMax - scrollExtentMin + rect.height + /// of the [SemanticsObject] managed by this scrollable. final DomElement _scrollOverflowElement = createDomElement('flt-semantics-scroll-overflow'); /// Listens to HTML "scroll" gestures detected by the browser. /// - /// This gesture is converted to [ui.SemanticsAction.scrollUp] or - /// [ui.SemanticsAction.scrollDown], depending on the direction. + /// When the browser detects a "scroll" gesture we send the updated DOM scroll position + /// to the framework in a [ui.SemanticsAction.scrollToOffset]. @visibleForTesting DomEventListener? scrollListener; - /// The value of the "scrollTop" or "scrollLeft" property of this object's - /// [element] that has zero offset relative to the [scrollPosition]. - int _effectiveNeutralScrollPosition = 0; - /// Whether this scrollable can scroll vertically or horizontally. bool get _canScroll => semanticsObject.isVerticalScrollContainer || semanticsObject.isHorizontalScrollContainer; + /// The previous value of the "scrollTop" or "scrollLeft" property of this object's + /// [element], used to determine if the content was scrolled. + int _previousDomScrollPosition = 0; + /// Responds to browser-detected "scroll" gestures. void _recomputeScrollPosition() { - if (_domScrollPosition != _effectiveNeutralScrollPosition) { + if (_domScrollPosition != _previousDomScrollPosition) { if (!EngineSemantics.instance.shouldAcceptBrowserGesture('scroll')) { return; } - final bool doScrollForward = _domScrollPosition > _effectiveNeutralScrollPosition; - _neutralizeDomScrollPosition(); + + _previousDomScrollPosition = _domScrollPosition; + _updateScrollableState(); semanticsObject.recomputePositionAndSize(); semanticsObject.updateChildrenPositionAndSize(); final int semanticsId = semanticsObject.id; - if (doScrollForward) { - if (semanticsObject.isVerticalScrollContainer) { - EnginePlatformDispatcher.instance.invokeOnSemanticsAction( - viewId, - semanticsId, - ui.SemanticsAction.scrollUp, - null, - ); - } else { - assert(semanticsObject.isHorizontalScrollContainer); - EnginePlatformDispatcher.instance.invokeOnSemanticsAction( - viewId, - semanticsId, - ui.SemanticsAction.scrollLeft, - null, - ); - } + final Float64List offsets = Float64List(2); + + // Either SemanticsObject.isVerticalScrollContainer or + // SemanticsObject.isHorizontalScrollContainer should be + // true otherwise scrollToOffset cannot be called. + if (semanticsObject.isVerticalScrollContainer) { + offsets[0] = 0.0; + offsets[1] = element.scrollTop; } else { - if (semanticsObject.isVerticalScrollContainer) { - EnginePlatformDispatcher.instance.invokeOnSemanticsAction( - viewId, - semanticsId, - ui.SemanticsAction.scrollDown, - null, - ); - } else { - assert(semanticsObject.isHorizontalScrollContainer); - EnginePlatformDispatcher.instance.invokeOnSemanticsAction( - viewId, - semanticsId, - ui.SemanticsAction.scrollRight, - null, - ); - } + assert(semanticsObject.isHorizontalScrollContainer); + offsets[0] = element.scrollLeft; + offsets[1] = 0.0; } + + final ByteData? message = const StandardMessageCodec().encodeMessage(offsets); + EnginePlatformDispatcher.instance.invokeOnSemanticsAction( + viewId, + semanticsId, + ui.SemanticsAction.scrollToOffset, + message, + ); } } @@ -122,6 +93,22 @@ class SemanticScrollable extends SemanticRole { // Scrolling is controlled by setting overflow-y/overflow-x to 'scroll`. The // default overflow = "visible" needs to be unset. semanticsObject.element.style.overflow = ''; + // On macOS the scrollbar behavior which can be set in the settings application + // may sometimes insert scrollbars into an application when a peripheral like a + // mouse or keyboard is plugged in. This causes the clientHeight or clientWidth + // of the scrollable DOM element to be offset by the width of the scrollbar. + // This causes issues in the vertical scrolling context because the max scroll + // extent is calculated by the element's scrollHeight - clientHeight, so when + // the clientHeight is offset by scrollbar width the browser may there is + // a greater scroll extent then what is actually available. + // + // The scrollbar is already made transparent in SemanticsRole._initElement so here + // set scrollbar-width to "none" to prevent it from affecting the max scroll extent. + // + // Support for scrollbar-width was only added to Safari v18.2+, so versions before + // that may still experience overscroll issues when macOS inserts scrollbars + // into the application. + semanticsObject.element.style.scrollbarWidth = 'none'; _scrollOverflowElement.style ..position = 'absolute' @@ -136,7 +123,15 @@ class SemanticScrollable extends SemanticRole { super.update(); semanticsObject.owner.addOneTimePostUpdateCallback(() { - _neutralizeDomScrollPosition(); + if (_canScroll) { + final double? scrollPosition = semanticsObject.scrollPosition; + assert(scrollPosition != null); + if (scrollPosition != _domScrollPosition) { + element.scrollTop = scrollPosition!; + _previousDomScrollPosition = _domScrollPosition; + } + } + _updateScrollableState(); semanticsObject.recomputePositionAndSize(); semanticsObject.updateChildrenPositionAndSize(); }); @@ -183,56 +178,38 @@ class SemanticScrollable extends SemanticRole { } } - /// Resets the scroll position (top or left) to the neutral value. - /// - /// The scroll position of the scrollable HTML node that's considered to - /// have zero offset relative to Flutter's notion of scroll position is - /// referred to as "neutral scroll position". - /// - /// We always set the scroll position to a non-zero value in order to - /// be able to scroll in the negative direction. When scrollTop/scrollLeft is - /// zero the browser will refuse to scroll back even when there is more - /// content available. - void _neutralizeDomScrollPosition() { + void _updateScrollableState() { // This value is arbitrary. - const int canonicalNeutralScrollPosition = 10; final ui.Rect? rect = semanticsObject.rect; if (rect == null) { printWarning('Warning! the rect attribute of semanticsObject is null'); return; } + final double? scrollExtentMax = semanticsObject.scrollExtentMax; + final double? scrollExtentMin = semanticsObject.scrollExtentMin; + assert(scrollExtentMax != null); + assert(scrollExtentMin != null); + final double scrollExtentTotal = + scrollExtentMax! - + scrollExtentMin! + + (semanticsObject.isVerticalScrollContainer ? rect.height : rect.width); + // Place the _scrollOverflowElement at the beginning of the content + // and size it based on the total scroll extent so the browser + // knows how much scrollable content there is. if (semanticsObject.isVerticalScrollContainer) { - // Place the _scrollOverflowElement at the end of the content and - // make sure that when we neutralize the scrolling position, - // it doesn't scroll into the visible area. - final int verticalOffset = rect.height.ceil() + canonicalNeutralScrollPosition; _scrollOverflowElement.style - ..transform = 'translate(0px,${verticalOffset}px)' - ..width = '${rect.width.round()}px' - ..height = '${canonicalNeutralScrollPosition}px'; - - element.scrollTop = canonicalNeutralScrollPosition.toDouble(); - // Read back because the effective value depends on the amount of content. - _effectiveNeutralScrollPosition = element.scrollTop.toInt(); + ..width = '0px' + ..height = '${scrollExtentTotal.toStringAsFixed(1)}px'; semanticsObject - ..verticalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble() + ..verticalScrollAdjustment = element.scrollTop ..horizontalScrollAdjustment = 0.0; } else if (semanticsObject.isHorizontalScrollContainer) { - // Place the _scrollOverflowElement at the end of the content and - // make sure that when we neutralize the scrolling position, - // it doesn't scroll into the visible area. - final int horizontalOffset = rect.width.ceil() + canonicalNeutralScrollPosition; _scrollOverflowElement.style - ..transform = 'translate(${horizontalOffset}px,0px)' - ..width = '${canonicalNeutralScrollPosition}px' - ..height = '${rect.height.round()}px'; - - element.scrollLeft = canonicalNeutralScrollPosition.toDouble(); - // Read back because the effective value depends on the amount of content. - _effectiveNeutralScrollPosition = element.scrollLeft.toInt(); + ..width = '${scrollExtentTotal.toStringAsFixed(1)}px' + ..height = '0px'; semanticsObject ..verticalScrollAdjustment = 0.0 - ..horizontalScrollAdjustment = _effectiveNeutralScrollPosition.toDouble(); + ..horizontalScrollAdjustment = element.scrollLeft; } else { _scrollOverflowElement.style ..transform = 'translate(0px,0px)' @@ -240,7 +217,6 @@ class SemanticScrollable extends SemanticRole { ..height = '0px'; element.scrollLeft = 0.0; element.scrollTop = 0.0; - _effectiveNeutralScrollPosition = 0; semanticsObject ..verticalScrollAdjustment = 0.0 ..horizontalScrollAdjustment = 0.0; diff --git a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart index 10420cc21e..f1b153b9f2 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -1612,7 +1612,7 @@ void _testVerticalScrolling() { '''); final DomElement scrollable = findScrollable(owner()); - expect(scrollable.scrollTop, isPositive); + expect(scrollable.scrollTop, 0); semantics().semanticsEnabled = false; }); @@ -1649,8 +1649,8 @@ void _testVerticalScrolling() { expect(scrollable, isNotNull); // When there's less content than the available size the neutral scrollTop - // is still a positive number. - expect(scrollable.scrollTop, isPositive); + // is 0. + expect(scrollable.scrollTop, 0); semantics().semanticsEnabled = false; }); @@ -1703,18 +1703,7 @@ void _testVerticalScrolling() { final DomElement scrollable = owner().debugSemanticsTree![0]!.element; expect(scrollable, isNotNull); - - // When there's more content than the available size the neutral scrollTop - // is greater than 0 with a maximum of 10 or 9. - int browserMaxScrollDiff = 0; - // The max scroll value varies between `9` and `10` for Safari desktop - // browsers. - if (ui_web.browser.browserEngine == ui_web.BrowserEngine.webkit && - ui_web.browser.operatingSystem == ui_web.OperatingSystem.macOs) { - browserMaxScrollDiff = 1; - } - - expect(scrollable.scrollTop >= (10 - browserMaxScrollDiff), isTrue); + expect(scrollable.scrollTop, 0); Future capturedEventFuture = captureSemanticsEvent(); scrollable.scrollTop = 20; @@ -1722,21 +1711,44 @@ void _testVerticalScrolling() { ui.SemanticsActionEvent capturedEvent = await capturedEventFuture; expect(capturedEvent.nodeId, 0); - expect(capturedEvent.type, ui.SemanticsAction.scrollUp); - expect(capturedEvent.arguments, isNull); - // Engine semantics returns scroll top back to neutral. - expect(scrollable.scrollTop >= (10 - browserMaxScrollDiff), isTrue); + expect(capturedEvent.type, ui.SemanticsAction.scrollToOffset); + expect(capturedEvent.arguments, isNotNull); + final Float64List expectedOffset = Float64List(2); + expectedOffset[0] = 0.0; + expectedOffset[1] = 20.0; + Float64List message = + const StandardMessageCodec().decodeMessage(capturedEvent.arguments! as ByteData) + as Float64List; + expect(message, expectedOffset); + + // Update scrollPosition to scrollTop value. + final ui.SemanticsUpdateBuilder builder2 = ui.SemanticsUpdateBuilder(); + updateNode( + builder2, + scrollPosition: 20.0, + flags: 0 | ui.SemanticsFlag.hasImplicitScrolling.index, + actions: 0 | ui.SemanticsAction.scrollUp.index | ui.SemanticsAction.scrollDown.index, + transform: Matrix4.identity().toFloat64(), + rect: const ui.Rect.fromLTRB(0, 0, 50, 100), + childrenInHitTestOrder: Int32List.fromList([1, 2, 3]), + childrenInTraversalOrder: Int32List.fromList([1, 2, 3]), + ); + owner().updateSemantics(builder2.build()); capturedEventFuture = captureSemanticsEvent(); scrollable.scrollTop = 5; capturedEvent = await capturedEventFuture; - expect(scrollable.scrollTop >= (5 - browserMaxScrollDiff), isTrue); + expect(scrollable.scrollTop, 5); expect(capturedEvent.nodeId, 0); - expect(capturedEvent.type, ui.SemanticsAction.scrollDown); - expect(capturedEvent.arguments, isNull); - // Engine semantics returns scroll top back to neutral. - expect(scrollable.scrollTop >= (10 - browserMaxScrollDiff), isTrue); + expect(capturedEvent.type, ui.SemanticsAction.scrollToOffset); + expect(capturedEvent.arguments, isNotNull); + expectedOffset[0] = 0.0; + expectedOffset[1] = 5.0; + message = + const StandardMessageCodec().decodeMessage(capturedEvent.arguments! as ByteData) + as Float64List; + expect(message, expectedOffset); }); test('scrollable switches to pointer event mode on a wheel event', () async { @@ -1783,27 +1795,22 @@ void _testVerticalScrolling() { final DomElement scrollable = owner().debugSemanticsTree![0]!.element; expect(scrollable, isNotNull); - void expectNeutralPosition() { - // Browsers disagree on the exact value, but it's always close to 10. - expect((scrollable.scrollTop - 10).abs(), lessThan(2)); - } - - // Initially, starting with a neutral scroll position, everything should be + // Initially, starting at "scrollTop" 0, everything should be // in browser gesture mode, react to DOM scroll events, and generate // semantic actions. - expectNeutralPosition(); + expect(scrollable.scrollTop, 0); expect(semantics().gestureMode, GestureMode.browserGestures); scrollable.scrollTop = 20; expect(scrollable.scrollTop, 20); await Future.delayed(const Duration(milliseconds: 100)); expect(actionLog, hasLength(1)); final capturedEvent = actionLog.removeLast(); - expect(capturedEvent.type, ui.SemanticsAction.scrollUp); + expect(capturedEvent.type, ui.SemanticsAction.scrollToOffset); - // Now, starting with a neutral mode, observing a DOM "wheel" event should + // Now, starting at the "scrollTop" 20 we set, observing a DOM "wheel" event should // swap into pointer event mode, and the scrollable becomes a plain clip, // i.e. `overflow: hidden`. - expectNeutralPosition(); + expect(scrollable.scrollTop, 20); expect(semantics().gestureMode, GestureMode.browserGestures); expect(scrollable.style.overflowY, 'scroll'); @@ -1870,8 +1877,8 @@ void _testHorizontalScrolling() { expect(scrollable, isNotNull); // When there's less content than the available size the neutral - // scrollLeft is still a positive number. - expect(scrollable.scrollLeft, isPositive); + // scrollLeft is still 0. + expect(scrollable.scrollLeft, 0); semantics().semanticsEnabled = false; }); @@ -1924,17 +1931,7 @@ void _testHorizontalScrolling() { final DomElement scrollable = findScrollable(owner()); expect(scrollable, isNotNull); - - // When there's more content than the available size the neutral scrollTop - // is greater than 0 with a maximum of 10. - int browserMaxScrollDiff = 0; - // The max scroll value varies between `9` and `10` for Safari desktop - // browsers. - if (ui_web.browser.browserEngine == ui_web.BrowserEngine.webkit && - ui_web.browser.operatingSystem == ui_web.OperatingSystem.macOs) { - browserMaxScrollDiff = 1; - } - expect(scrollable.scrollLeft >= (10 - browserMaxScrollDiff), isTrue); + expect(scrollable.scrollLeft, 0); Future capturedEventFuture = captureSemanticsEvent(); scrollable.scrollLeft = 20; @@ -1942,21 +1939,44 @@ void _testHorizontalScrolling() { ui.SemanticsActionEvent capturedEvent = await capturedEventFuture; expect(capturedEvent.nodeId, 0); - expect(capturedEvent.type, ui.SemanticsAction.scrollLeft); - expect(capturedEvent.arguments, isNull); - // Engine semantics returns scroll position back to neutral. - expect(scrollable.scrollLeft >= (10 - browserMaxScrollDiff), isTrue); + expect(capturedEvent.type, ui.SemanticsAction.scrollToOffset); + expect(capturedEvent.arguments, isNotNull); + final Float64List expectedOffset = Float64List(2); + expectedOffset[0] = 20.0; + expectedOffset[1] = 0.0; + Float64List message = + const StandardMessageCodec().decodeMessage(capturedEvent.arguments! as ByteData) + as Float64List; + expect(message, expectedOffset); + + // Update scrollPosition to scrollLeft value. + final ui.SemanticsUpdateBuilder builder2 = ui.SemanticsUpdateBuilder(); + updateNode( + builder2, + scrollPosition: 20.0, + flags: 0 | ui.SemanticsFlag.hasImplicitScrolling.index, + actions: 0 | ui.SemanticsAction.scrollLeft.index | ui.SemanticsAction.scrollRight.index, + transform: Matrix4.identity().toFloat64(), + rect: const ui.Rect.fromLTRB(0, 0, 50, 100), + childrenInHitTestOrder: Int32List.fromList([1, 2, 3]), + childrenInTraversalOrder: Int32List.fromList([1, 2, 3]), + ); + owner().updateSemantics(builder2.build()); capturedEventFuture = captureSemanticsEvent(); scrollable.scrollLeft = 5; capturedEvent = await capturedEventFuture; - expect(scrollable.scrollLeft >= (5 - browserMaxScrollDiff), isTrue); + expect(scrollable.scrollLeft, 5); expect(capturedEvent.nodeId, 0); - expect(capturedEvent.type, ui.SemanticsAction.scrollRight); - expect(capturedEvent.arguments, isNull); - // Engine semantics returns scroll top back to neutral. - expect(scrollable.scrollLeft >= (10 - browserMaxScrollDiff), isTrue); + expect(capturedEvent.type, ui.SemanticsAction.scrollToOffset); + expect(capturedEvent.arguments, isNotNull); + expectedOffset[0] = 5.0; + expectedOffset[1] = 0.0; + message = + const StandardMessageCodec().decodeMessage(capturedEvent.arguments! as ByteData) + as Float64List; + expect(message, expectedOffset); }); } diff --git a/engine/src/flutter/pubspec.yaml b/engine/src/flutter/pubspec.yaml index 5b105e8e08..b6619a1363 100644 --- a/engine/src/flutter/pubspec.yaml +++ b/engine/src/flutter/pubspec.yaml @@ -166,8 +166,6 @@ dependency_overrides: path: ./third_party/pkg/googleapis/discoveryapis_commons _fe_analyzer_shared: path: ./third_party/dart/pkg/_fe_analyzer_shared - _macros: - path: ./third_party/dart/pkg/_macros analyzer: path: ./third_party/dart/pkg/analyzer archive: @@ -222,8 +220,6 @@ dependency_overrides: path: ./third_party/dart/pkg/kernel logging: path: ./third_party/dart/third_party/pkg/core/pkgs/logging - macros: - path: ./third_party/dart/pkg/macros matcher: path: ./third_party/dart/third_party/pkg/test/pkgs/matcher meta: diff --git a/engine/src/flutter/web_sdk/pubspec.yaml b/engine/src/flutter/web_sdk/pubspec.yaml index fb1def8b2a..d46d1bd38e 100644 --- a/engine/src/flutter/web_sdk/pubspec.yaml +++ b/engine/src/flutter/web_sdk/pubspec.yaml @@ -18,8 +18,6 @@ dev_dependencies: dependency_overrides: # Must include all transitive dependencies from the "any" packages above. _fe_analyzer_shared: path: ../third_party/dart/pkg/_fe_analyzer_shared - _macros: - path: ../third_party/dart/pkg/_macros analyzer: path: ../third_party/dart/pkg/analyzer args: @@ -38,8 +36,6 @@ dependency_overrides: # Must include all transitive dependencies from the "any" path: ../third_party/dart/third_party/pkg/tools/pkgs/file glob: path: ../third_party/dart/third_party/pkg/tools/pkgs/glob - macros: - path: ../third_party/dart/pkg/macros meta: path: ../third_party/dart/pkg/meta package_config: diff --git a/examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart b/examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart new file mode 100644 index 0000000000..defdb691c2 --- /dev/null +++ b/examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart @@ -0,0 +1,207 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +/// Flutter code sample for [SliverEnsureSemantics]. + +void main() => runApp(const SliverEnsureSemanticsExampleApp()); + +class SliverEnsureSemanticsExampleApp extends StatelessWidget { + const SliverEnsureSemanticsExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp(home: SliverEnsureSemanticsExample()); + } +} + +class SliverEnsureSemanticsExample extends StatefulWidget { + const SliverEnsureSemanticsExample({super.key}); + + @override + State createState() => _SliverEnsureSemanticsExampleState(); +} + +class _SliverEnsureSemanticsExampleState extends State { + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + backgroundColor: theme.colorScheme.inversePrimary, + title: const Text('SliverEnsureSemantics Demo'), + ), + body: Center( + child: CustomScrollView( + semanticChildCount: 106, + slivers: [ + SliverEnsureSemantics( + sliver: SliverToBoxAdapter( + child: IndexedSemantics( + index: 0, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Semantics( + header: true, + headingLevel: 3, + child: Text('Steps to reproduce', style: theme.textTheme.headlineSmall), + ), + const Text('Issue description'), + Semantics( + header: true, + headingLevel: 3, + child: Text('Expected Results', style: theme.textTheme.headlineSmall), + ), + Semantics( + header: true, + headingLevel: 3, + child: Text('Actual Results', style: theme.textTheme.headlineSmall), + ), + Semantics( + header: true, + headingLevel: 3, + child: Text('Code Sample', style: theme.textTheme.headlineSmall), + ), + Semantics( + header: true, + headingLevel: 3, + child: Text('Screenshots', style: theme.textTheme.headlineSmall), + ), + Semantics( + header: true, + headingLevel: 3, + child: Text('Logs', style: theme.textTheme.headlineSmall), + ), + ], + ), + ), + ), + ), + ), + ), + SliverFixedExtentList( + itemExtent: 44.0, + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return Card( + child: Padding(padding: const EdgeInsets.all(8.0), child: Text('Item $index')), + ); + }, + childCount: 50, + semanticIndexOffset: 1, + ), + ), + SliverEnsureSemantics( + sliver: SliverToBoxAdapter( + child: IndexedSemantics( + index: 51, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Semantics(header: true, child: const Text('Footer 1')), + ), + ), + ), + ), + ), + SliverEnsureSemantics( + sliver: SliverToBoxAdapter( + child: IndexedSemantics( + index: 52, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Semantics(header: true, child: const Text('Footer 2')), + ), + ), + ), + ), + ), + SliverEnsureSemantics( + sliver: SliverToBoxAdapter( + child: IndexedSemantics( + index: 53, + child: Semantics(link: true, child: const Text('Link #1')), + ), + ), + ), + SliverEnsureSemantics( + sliver: SliverToBoxAdapter( + child: IndexedSemantics( + index: 54, + child: OverflowBar( + children: [ + TextButton(onPressed: () {}, child: const Text('Button 1')), + TextButton(onPressed: () {}, child: const Text('Button 2')), + ], + ), + ), + ), + ), + SliverEnsureSemantics( + sliver: SliverToBoxAdapter( + child: IndexedSemantics( + index: 55, + child: Semantics(link: true, child: const Text('Link #2')), + ), + ), + ), + SliverEnsureSemantics( + sliver: SliverSemanticsList( + sliver: SliverFixedExtentList( + itemExtent: 44.0, + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return Semantics( + role: SemanticsRole.listItem, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text('Second List Item $index'), + ), + ), + ); + }, + childCount: 50, + semanticIndexOffset: 56, + ), + ), + ), + ), + SliverEnsureSemantics( + sliver: SliverToBoxAdapter( + child: IndexedSemantics( + index: 107, + child: Semantics(link: true, child: const Text('Link #3')), + ), + ), + ), + ], + ), + ), + ); + } +} + +// A sliver that assigns the role of SemanticsRole.list to its sliver child. +class SliverSemanticsList extends SingleChildRenderObjectWidget { + const SliverSemanticsList({super.key, required Widget sliver}) : super(child: sliver); + + @override + RenderSliverSemanticsList createRenderObject(BuildContext context) => RenderSliverSemanticsList(); +} + +class RenderSliverSemanticsList extends RenderProxySliver { + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.role = SemanticsRole.list; + } +} diff --git a/examples/api/test/widgets/sliver/sliver_ensure_semantics.0_test.dart b/examples/api/test/widgets/sliver/sliver_ensure_semantics.0_test.dart new file mode 100644 index 0000000000..651d98d6f8 --- /dev/null +++ b/examples/api/test/widgets/sliver/sliver_ensure_semantics.0_test.dart @@ -0,0 +1,14 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_api_samples/widgets/sliver/sliver_ensure_semantics.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('SliverEnsureSemantics example', (WidgetTester tester) async { + await tester.pumpWidget(const example.SliverEnsureSemanticsExampleApp()); + + expect(find.text('SliverEnsureSemantics Demo'), findsOneWidget); + }); +} diff --git a/packages/flutter/lib/src/material/carousel.dart b/packages/flutter/lib/src/material/carousel.dart index 4e7fcbd58c..1a36f50809 100644 --- a/packages/flutter/lib/src/material/carousel.dart +++ b/packages/flutter/lib/src/material/carousel.dart @@ -107,6 +107,15 @@ import 'theme.dart'; /// Here is an example to show different carousel layouts that [CarouselView] /// and [CarouselView.weighted] can build. /// +/// On desktop and web running on desktop platforms, dragging to scroll with a mouse +/// is disabled by default to align with natural behavior. +/// +/// To further align expected behavior like this, mouse input can scroll horizontally +/// by pressing the shift key while scrolling with the mouse wheel. +/// +/// This key-driven behavior is dictated by the [ScrollBehavior.pointerAxisModifiers], +/// while [ScrollBehavior.dragDevices] manages what devices can drag a scrollable. +/// /// ** See code in examples/api/lib/material/carousel/carousel.0.dart ** /// {@end-tool} /// diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index e2c9ee21c1..7cdeef8db9 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -393,7 +393,7 @@ class ListTile extends StatelessWidget { this.title, this.subtitle, this.trailing, - this.isThreeLine = false, + this.isThreeLine, this.dense, this.visualDensity, this.shape, @@ -425,7 +425,7 @@ class ListTile extends StatelessWidget { this.minTileHeight, this.titleAlignment, this.internalAddSemanticForOnTap = true, - }) : assert(!isThreeLine || subtitle != null); + }) : assert(isThreeLine != true || subtitle != null); /// A widget to display before the title. /// @@ -482,7 +482,12 @@ class ListTile extends StatelessWidget { /// /// When using a [Text] widget for [title] and [subtitle], you can enforce /// line limits using [Text.maxLines]. - final bool isThreeLine; + /// + /// See also: + /// + /// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s + /// [ListTileThemeData]. + final bool? isThreeLine; /// {@template flutter.material.ListTile.dense} /// Whether this list tile is part of a vertically dense list. @@ -987,7 +992,11 @@ class ListTile extends StatelessWidget { trailing: trailingIcon, isDense: _isDenseLayout(theme, tileTheme), visualDensity: visualDensity ?? tileTheme.visualDensity ?? theme.visualDensity, - isThreeLine: isThreeLine, + isThreeLine: + isThreeLine ?? + tileTheme.isThreeLine ?? + theme.listTileTheme.isThreeLine ?? + false, textDirection: textDirection, titleBaselineType: titleStyle.textBaseline ?? defaults.titleTextStyle!.textBaseline!, @@ -1021,7 +1030,6 @@ class ListTile extends StatelessWidget { ifTrue: 'THREE_LINE', ifFalse: 'TWO_LINE', showName: true, - defaultValue: false, ), ); properties.add( diff --git a/packages/flutter/lib/src/material/list_tile_theme.dart b/packages/flutter/lib/src/material/list_tile_theme.dart index 3dd6ce9d6c..03b1d6570e 100644 --- a/packages/flutter/lib/src/material/list_tile_theme.dart +++ b/packages/flutter/lib/src/material/list_tile_theme.dart @@ -73,6 +73,7 @@ class ListTileThemeData with Diagnosticable { this.minTileHeight, this.titleAlignment, this.controlAffinity, + this.isThreeLine, }); /// Overrides the default value of [ListTile.dense]. @@ -139,6 +140,9 @@ class ListTileThemeData with Diagnosticable { /// or [ExpansionTile.controlAffinity] or [SwitchListTile.controlAffinity] or [RadioListTile.controlAffinity]. final ListTileControlAffinity? controlAffinity; + /// If specified, overrides the default value of [ListTile.isThreeLine]. + final bool? isThreeLine; + /// Creates a copy of this object with the given fields replaced with the /// new values. ListTileThemeData copyWith({ @@ -187,6 +191,7 @@ class ListTileThemeData with Diagnosticable { visualDensity: visualDensity ?? this.visualDensity, titleAlignment: titleAlignment ?? this.titleAlignment, controlAffinity: controlAffinity ?? this.controlAffinity, + isThreeLine: isThreeLine ?? this.isThreeLine, ); } @@ -221,6 +226,7 @@ class ListTileThemeData with Diagnosticable { visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity, titleAlignment: t < 0.5 ? a?.titleAlignment : b?.titleAlignment, controlAffinity: t < 0.5 ? a?.controlAffinity : b?.controlAffinity, + isThreeLine: t < 0.5 ? a?.isThreeLine : b?.isThreeLine, ); } @@ -247,6 +253,7 @@ class ListTileThemeData with Diagnosticable { visualDensity, titleAlignment, controlAffinity, + isThreeLine, ]); @override @@ -278,7 +285,8 @@ class ListTileThemeData with Diagnosticable { other.mouseCursor == mouseCursor && other.visualDensity == visualDensity && other.titleAlignment == titleAlignment && - other.controlAffinity == controlAffinity; + other.controlAffinity == controlAffinity && + other.isThreeLine == isThreeLine; } @override @@ -337,6 +345,7 @@ class ListTileThemeData with Diagnosticable { defaultValue: null, ), ); + properties.add(DiagnosticsProperty('isThreeLine', isThreeLine, defaultValue: null)); } } @@ -573,6 +582,7 @@ class ListTileTheme extends InheritedTheme { MaterialStateProperty? mouseCursor, VisualDensity? visualDensity, ListTileControlAffinity? controlAffinity, + bool? isThreeLine, required Widget child, }) { return Builder( @@ -603,6 +613,7 @@ class ListTileTheme extends InheritedTheme { mouseCursor: mouseCursor ?? parent.mouseCursor, visualDensity: visualDensity ?? parent.visualDensity, controlAffinity: controlAffinity ?? parent.controlAffinity, + isThreeLine: isThreeLine ?? parent.isThreeLine, ), child: child, ); @@ -627,6 +638,7 @@ class ListTileTheme extends InheritedTheme { horizontalTitleGap: horizontalTitleGap, minVerticalPadding: minVerticalPadding, minLeadingWidth: minLeadingWidth, + isThreeLine: _data?.isThreeLine, ), child: child, ); diff --git a/packages/flutter/lib/src/rendering/proxy_sliver.dart b/packages/flutter/lib/src/rendering/proxy_sliver.dart index 84be9b46ca..a1d8f6353b 100644 --- a/packages/flutter/lib/src/rendering/proxy_sliver.dart +++ b/packages/flutter/lib/src/rendering/proxy_sliver.dart @@ -42,6 +42,14 @@ abstract class RenderProxySliver extends RenderSliver this.child = child; } + @override + Rect get semanticBounds { + if (child != null) { + return child!.semanticBounds; + } + return super.semanticBounds; + } + @override void setupParentData(RenderObject child) { if (child.parentData is! SliverPhysicalParentData) { diff --git a/packages/flutter/lib/src/rendering/sliver.dart b/packages/flutter/lib/src/rendering/sliver.dart index 0a944edba3..c0f5d99ee9 100644 --- a/packages/flutter/lib/src/rendering/sliver.dart +++ b/packages/flutter/lib/src/rendering/sliver.dart @@ -5,6 +5,7 @@ /// @docImport 'package:flutter/material.dart'; /// /// @docImport 'proxy_box.dart'; +/// @docImport 'proxy_sliver.dart'; /// @docImport 'sliver_fill.dart'; /// @docImport 'sliver_grid.dart'; /// @docImport 'sliver_list.dart'; @@ -1306,6 +1307,28 @@ List _debugCompareFloats( /// than zero, then it should override [childCrossAxisPosition]. For example /// [RenderSliverGrid] overrides this method. abstract class RenderSliver extends RenderObject { + /// Whether this sliver should be included in the semantics tree. + /// + /// This value is used by [RenderViewportBase] to ensure a sliver is + /// included in the semantics tree regardless of its geometry. + /// + /// A [RenderSliver] should override this value to `true` to ensure + /// its child is included in the semantics tree. For example if your + /// sliver is under a [RenderViewport] you may want to wrap it with + /// a [SliverEnsureSemantics] to ensure that: + /// + /// 1. It is still visited by [RenderViewportBase.visitChildrenForSemantics] + /// regardless of its geometry. This includes cases where your sliver is outside + /// the current viewport and cache extent. + /// 2. Its semantic information is not clipped out by the [RenderViewport] in + /// [RenderViewportBase.describeSemanticsClip] or [RenderViewportBase.describeApproximatePaintClip]. + /// + /// If a given [RenderSliver] does not provide a valid [semanticBounds] it will still + /// be dropped from the semantics tree. + /// + /// Defaults to `false`. + bool get ensureSemantics => false; + // layout input @override SliverConstraints get constraints => super.constraints as SliverConstraints; diff --git a/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart b/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart index 90885e4c11..4f6071eaef 100644 --- a/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart +++ b/packages/flutter/lib/src/rendering/sliver_multi_box_adaptor.dart @@ -425,6 +425,18 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver // Do not visit children in [_keepAliveBucket]. } + @override + Rect get semanticBounds { + // If we laid out the first child but this sliver is not visible, we report the + // semantic bounds of this sliver as the bounds of the first child. This is necessary + // for accessibility technologies to reach this sliver even when it is outside + // the current viewport and cache extent. + if (geometry != null && !geometry!.visible && firstChild != null && firstChild!.hasSize) { + return firstChild!.paintBounds; + } + return super.semanticBounds; + } + /// Called during layout to create and add the child with the given index and /// scroll offset. /// diff --git a/packages/flutter/lib/src/rendering/sliver_tree.dart b/packages/flutter/lib/src/rendering/sliver_tree.dart index f88df192d6..6cc82bb425 100644 --- a/packages/flutter/lib/src/rendering/sliver_tree.dart +++ b/packages/flutter/lib/src/rendering/sliver_tree.dart @@ -332,7 +332,8 @@ class RenderTreeSliver extends RenderSliverVariedExtentList { while (child != null && indexOf(child) <= index) { final double mainAxisDelta = childMainAxisPosition(child); final TreeSliverNodeParentData parentData = child.parentData! as TreeSliverNodeParentData; - final Offset childOffset = Offset(parentData.depth * indentation, parentData.layoutOffset!); + final Offset childOffset = + Offset(parentData.depth * indentation, parentData.layoutOffset!) + offset; // If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child)) // does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden. diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index 09af6c11f1..2661717e45 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -314,7 +314,10 @@ abstract class RenderViewportBase sliver.geometry!.visible || sliver.geometry!.cacheExtent > 0.0, + (RenderSliver sliver) => + sliver.geometry!.visible || + sliver.geometry!.cacheExtent > 0.0 || + sliver.ensureSemantics, ) .forEach(visitor); } @@ -671,6 +674,12 @@ abstract class RenderViewportBase 0.0)) { + // Return null here so we don't end up clipping out a semantics node rect + // for a sliver child when we explicitly want it to be included in the semantics tree. + return null; + } + switch (clipBehavior) { case Clip.none: return null; @@ -716,7 +725,14 @@ abstract class RenderViewportBase 0.0)) { + // Return null here so we don't end up clipping out a semantics node rect + // for a sliver child when we explicitly want it to be included in the semantics tree. + return null; + } if (_calculatedCacheExtent == null) { return semanticBounds; } diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index 4f96cb9f16..f6d14ebf78 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -1771,3 +1771,48 @@ class _SliverMainAxisGroupElement extends MultiChildRenderObjectElement { .forEach(visitor); } } + +/// A sliver that ensures its sliver child is included in the semantics tree. +/// +/// This sliver ensures that its child sliver is still visited by the [RenderViewport] +/// when constructing the semantics tree, and is not clipped out of the semantics tree by +/// the [RenderViewport] when it is outside the current viewport and outside the cache extent. +/// +/// The child sliver may still be excluded from the semantics tree if its [RenderSliver] does +/// not provide a valid [RenderSliver.semanticBounds]. This sliver does not guarantee its +/// child sliver is laid out. +/// +/// Be mindful when positioning [SliverEnsureSemantics] in a [CustomScrollView] after slivers that build +/// their children lazily, like [SliverList]. Lazy slivers might underestimate the total scrollable size (scroll +/// extent) before the [SliverEnsureSemantics] widget. This inaccuracy can cause problems for assistive +/// technologies (e.g., screen readers), which rely on a correct scroll extent to navigate properly; they +/// might fail to scroll accurately to the content wrapped by [SliverEnsureSemantics]. +/// +/// To avoid this potential issue and ensure the scroll extent is calculated accurately up to this sliver, +/// it's recommended to use slivers that can determine their extent precisely beforehand. Instead of +/// [SliverList], consider using [SliverFixedExtentList], [SliverVariedExtentList], or +/// [SliverPrototypeExtentList]. If using [SliverGrid], ensure it employs a delegate such as +/// [SliverGridDelegateWithFixedCrossAxisCount] or [SliverGridDelegateWithMaxCrossAxisExtent]. +/// Using these alternatives guarantees that the scrollable area's size is known accurately, allowing +/// assistive technologies to function correctly with [SliverEnsureSemantics]. +/// +/// {@tool dartpad} +/// This example shows how to use [SliverEnsureSemantics] to keep certain headers and lists +/// available to assistive technologies while they are outside the current viewport and cache extent. +/// +/// ** See code in examples/api/lib/widgets/sliver/sliver_ensure_semantics.0.dart ** +/// {@end-tool} +// TODO(Renzo-Olivares): Investigate potential solutions for revealing off screen items, https://github.com/flutter/flutter/issues/166703. +class SliverEnsureSemantics extends SingleChildRenderObjectWidget { + /// Creates a sliver that ensures its sliver child is included in the semantics tree. + const SliverEnsureSemantics({super.key, required Widget sliver}) : super(child: sliver); + + @override + RenderObject createRenderObject(BuildContext context) => _RenderSliverEnsureSemantics(); +} + +/// Ensures its sliver child is included in the semantics tree. +class _RenderSliverEnsureSemantics extends RenderProxySliver { + @override + bool get ensureSemantics => true; +} diff --git a/packages/flutter/test/cupertino/date_picker_test.dart b/packages/flutter/test/cupertino/date_picker_test.dart index 883a664b2d..1c5f0dc67a 100644 --- a/packages/flutter/test/cupertino/date_picker_test.dart +++ b/packages/flutter/test/cupertino/date_picker_test.dart @@ -17,8 +17,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../impeller_test_helpers.dart'; - // TODO(yjbanov): on the web text rendered with perspective produces flaky goldens: https://github.com/flutter/flutter/issues/110785 final bool skipPerspectiveTextGoldens = isBrowser && isSkwasm; @@ -1611,7 +1609,7 @@ void main() { matchesGoldenFile('date_picker_test.datetime.drag.png'), ); } - }, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/143616 + }); testWidgets('DatePicker displays the date in correct order', (WidgetTester tester) async { await tester.pumpWidget( @@ -1761,7 +1759,7 @@ void main() { matchesGoldenFile('timer_picker_test.datetime.drag.png'), ); } - }, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/143616 + }); testWidgets('TimerPicker only changes hour label after scrolling stops', ( WidgetTester tester, diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index ec52249f1e..b3f607a109 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -4361,6 +4361,138 @@ void main() { expect(trailingOffset.dy - tileOffset.dy, minVerticalPadding); }); }); + + // Regression test for https://github.com/flutter/flutter/issues/165453 + testWidgets('ListTile isThreeLine', (WidgetTester tester) async { + const double height = 300; + const double avatarTop = 130.0; + const double placeholderTop = 138.0; + + Widget buildFrame({bool? themeDataIsThreeLine, bool? themeIsThreeLine, bool? isThreeLine}) { + return MaterialApp( + key: UniqueKey(), + theme: + themeDataIsThreeLine != null + ? ThemeData(listTileTheme: ListTileThemeData(isThreeLine: themeDataIsThreeLine)) + : null, + home: Material( + child: ListTileTheme( + data: + themeIsThreeLine != null ? ListTileThemeData(isThreeLine: themeIsThreeLine) : null, + child: ListView( + children: [ + ListTile( + isThreeLine: isThreeLine, + leading: const CircleAvatar(), + trailing: const SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: const Text('A'), + subtitle: const Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'), + ), + ListTile( + isThreeLine: isThreeLine, + leading: const CircleAvatar(), + trailing: const SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: const Text('A'), + subtitle: const Text('A'), + ), + ], + ), + ), + ), + ); + } + + void expectTwoLine() { + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, avatarTop, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, placeholderTop, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 72.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 16.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 24.0, 24.0, 24.0), + ); + } + + void expectThreeLine() { + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, 8.0, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 88.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 8.0, 24.0, 24.0), + ); + } + + await tester.pumpWidget(buildFrame()); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(themeDataIsThreeLine: true)); + expectThreeLine(); + + await tester.pumpWidget(buildFrame(themeDataIsThreeLine: false, themeIsThreeLine: true)); + expectThreeLine(); + + await tester.pumpWidget(buildFrame(themeDataIsThreeLine: true, themeIsThreeLine: false)); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(isThreeLine: true)); + expectThreeLine(); + + await tester.pumpWidget(buildFrame(themeIsThreeLine: true, isThreeLine: false)); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(themeDataIsThreeLine: true, isThreeLine: false)); + expectTwoLine(); + + await tester.pumpWidget( + buildFrame(themeDataIsThreeLine: true, themeIsThreeLine: true, isThreeLine: false), + ); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(themeIsThreeLine: false, isThreeLine: true)); + expectThreeLine(); + + await tester.pumpWidget(buildFrame(themeDataIsThreeLine: false, isThreeLine: true)); + expectThreeLine(); + + await tester.pumpWidget( + buildFrame(themeDataIsThreeLine: false, themeIsThreeLine: false, isThreeLine: true), + ); + expectThreeLine(); + }); } RenderParagraph _getTextRenderObject(WidgetTester tester, String text) { diff --git a/packages/flutter/test/material/list_tile_theme_test.dart b/packages/flutter/test/material/list_tile_theme_test.dart index 3c12053b7a..c9c71c7983 100644 --- a/packages/flutter/test/material/list_tile_theme_test.dart +++ b/packages/flutter/test/material/list_tile_theme_test.dart @@ -77,6 +77,7 @@ void main() { expect(themeData.mouseCursor, null); expect(themeData.visualDensity, null); expect(themeData.titleAlignment, null); + expect(themeData.isThreeLine, null); }); testWidgets('Default ListTileThemeData debugFillProperties', (WidgetTester tester) async { @@ -115,6 +116,7 @@ void main() { mouseCursor: MaterialStateMouseCursor.clickable, visualDensity: VisualDensity.comfortable, titleAlignment: ListTileTitleAlignment.top, + isThreeLine: true, ).debugFillProperties(builder); final List description = @@ -146,6 +148,7 @@ void main() { 'mouseCursor: WidgetStateMouseCursor(clickable)', 'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)', 'titleAlignment: ListTileTitleAlignment.top', + 'isThreeLine: true', ]), ); }); @@ -937,6 +940,7 @@ void main() { minTileHeight: 30, enableFeedback: true, titleAlignment: ListTileTitleAlignment.bottom, + isThreeLine: true, ); final ListTileThemeData copy = original.copyWith( @@ -958,6 +962,7 @@ void main() { minTileHeight: 80, enableFeedback: false, titleAlignment: ListTileTitleAlignment.top, + isThreeLine: false, ); expect(copy.dense, false); @@ -978,6 +983,7 @@ void main() { expect(copy.minTileHeight, 80); expect(copy.enableFeedback, false); expect(copy.titleAlignment, ListTileTitleAlignment.top); + expect(copy.isThreeLine, false); }); testWidgets('ListTileTheme.titleAlignment is overridden by ListTile.titleAlignment', ( @@ -1040,6 +1046,7 @@ void main() { titleAlignment: ListTileTitleAlignment.bottom, mouseCursor: MaterialStateMouseCursor.textable, visualDensity: VisualDensity.comfortable, + isThreeLine: true, ), ), home: Material( @@ -1067,6 +1074,7 @@ void main() { titleAlignment: ListTileTitleAlignment.top, mouseCursor: MaterialStateMouseCursor.clickable, visualDensity: VisualDensity.compact, + isThreeLine: false, child: const ListTile(), ); }, @@ -1098,6 +1106,217 @@ void main() { expect(theme.titleAlignment, ListTileTitleAlignment.top); expect(theme.mouseCursor, MaterialStateMouseCursor.clickable); expect(theme.visualDensity, VisualDensity.compact); + expect(theme.isThreeLine, false); + }); + + // Regression test for https://github.com/flutter/flutter/issues/165453 + testWidgets('ListTileThemeData isThreeLine', (WidgetTester tester) async { + const double height = 300; + const double avatarTop = 130.0; + const double placeholderTop = 138.0; + + Widget buildFrame({bool? isThreeLine}) { + return MaterialApp( + key: UniqueKey(), + theme: + isThreeLine != null + ? ThemeData(listTileTheme: ListTileThemeData(isThreeLine: isThreeLine)) + : null, + home: Material( + child: ListView( + children: const [ + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'), + ), + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A'), + ), + ], + ), + ), + ); + } + + void expectTwoLine() { + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, avatarTop, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, placeholderTop, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 72.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 16.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 24.0, 24.0, 24.0), + ); + } + + void expectThreeLine() { + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, 8.0, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 88.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 8.0, 24.0, 24.0), + ); + } + + await tester.pumpWidget(buildFrame()); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(isThreeLine: false)); + expectTwoLine(); + + await tester.pumpWidget(buildFrame(isThreeLine: true)); + expectThreeLine(); + }); + + // Regression test for https://github.com/flutter/flutter/issues/165453 + testWidgets('ListTileTheme isThreeLine', (WidgetTester tester) async { + const double height = 300; + const double avatarTop = 130.0; + const double placeholderTop = 138.0; + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(listTileTheme: const ListTileThemeData(isThreeLine: true)), + home: Material( + child: ListTileTheme( + data: const ListTileThemeData(isThreeLine: false), + child: ListView( + children: const [ + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'), + ), + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A'), + ), + ], + ), + ), + ), + ), + ); + + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, avatarTop, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, placeholderTop, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 72.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 16.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 24.0, 24.0, 24.0), + ); + + // THREE-LINE + await tester.pumpWidget( + MaterialApp( + key: UniqueKey(), + home: Material( + child: ListTileTheme( + data: const ListTileThemeData(isThreeLine: true), + child: ListView( + children: const [ + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM'), + ), + ListTile( + leading: CircleAvatar(), + trailing: SizedBox(height: 24.0, width: 24.0, child: Placeholder()), + title: Text('A'), + subtitle: Text('A'), + ), + ], + ), + ), + ), + ), + ); + + expect( + tester.getRect(find.byType(ListTile).at(0)), + const Rect.fromLTWH(0.0, 0.0, 800.0, height), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(0)), + const Rect.fromLTWH(16.0, 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(0)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, 8.0, 24.0, 24.0), + ); + expect( + tester.getRect(find.byType(ListTile).at(1)), + const Rect.fromLTWH(0.0, height, 800.0, 88.0), + ); + expect( + tester.getRect(find.byType(CircleAvatar).at(1)), + const Rect.fromLTWH(16.0, height + 8.0, 40.0, 40.0), + ); + expect( + tester.getRect(find.byType(Placeholder).at(1)), + const Rect.fromLTWH(800.0 - 24.0 - 24.0, height + 8.0, 24.0, 24.0), + ); }); } diff --git a/packages/flutter/test/widgets/color_filter_test.dart b/packages/flutter/test/widgets/color_filter_test.dart index 2f39a48e84..5b42a31a63 100644 --- a/packages/flutter/test/widgets/color_filter_test.dart +++ b/packages/flutter/test/widgets/color_filter_test.dart @@ -12,8 +12,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../impeller_test_helpers.dart'; - void main() { testWidgets('Color filter - red', (WidgetTester tester) async { await tester.pumpWidget( @@ -56,7 +54,7 @@ void main() { ), ); await expectLater(find.byType(ColorFiltered), matchesGoldenFile('color_filter_sepia.png')); - }, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/143616 + }); testWidgets('Color filter - reuses its layer', (WidgetTester tester) async { Future pumpWithColor(Color color) async { diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart index 362370a0b3..8fc6daa2dc 100644 --- a/packages/flutter/test/widgets/selectable_text_test.dart +++ b/packages/flutter/test/widgets/selectable_text_test.dart @@ -20,7 +20,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; -import '../impeller_test_helpers.dart'; import '../widgets/clipboard_utils.dart'; import '../widgets/editable_text_utils.dart' show textOffsetToPosition; import '../widgets/semantics_tester.dart'; @@ -5267,7 +5266,7 @@ void main() { find.byType(MaterialApp), matchesGoldenFile('selectable_text_golden.TextSelectionStyle.1.png'), ); - }, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/143616 + }); testWidgets('text selection style 2', (WidgetTester tester) async { await tester.pumpWidget( @@ -5304,7 +5303,7 @@ void main() { find.byType(MaterialApp), matchesGoldenFile('selectable_text_golden.TextSelectionStyle.2.png'), ); - }, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/143616 + }); testWidgets('keeps alive when has focus', (WidgetTester tester) async { await tester.pumpWidget( diff --git a/packages/flutter/test/widgets/shader_mask_test.dart b/packages/flutter/test/widgets/shader_mask_test.dart index e1e134baa9..fb6edab04e 100644 --- a/packages/flutter/test/widgets/shader_mask_test.dart +++ b/packages/flutter/test/widgets/shader_mask_test.dart @@ -10,8 +10,6 @@ library; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../impeller_test_helpers.dart'; - Shader createShader(Rect bounds) { return const LinearGradient( begin: Alignment.topCenter, @@ -102,5 +100,5 @@ void main() { find.byType(RepaintBoundary), matchesGoldenFile('shader_mask.bounds.matches_top_left.png'), ); - }, skip: impellerEnabled); // https://github.com/flutter/flutter/issues/144555 + }); } diff --git a/packages/flutter/test/widgets/sliver_tree_test.dart b/packages/flutter/test/widgets/sliver_tree_test.dart index d5ff22b297..d5511fe91d 100644 --- a/packages/flutter/test/widgets/sliver_tree_test.dart +++ b/packages/flutter/test/widgets/sliver_tree_test.dart @@ -2,6 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// This file is run as part of a reduced test set in CI on Mac and Windows +// machines. +@Tags(['reduced-test-set']) +library; + import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -861,4 +866,43 @@ void main() { }); }, ); + + testWidgets('TreeSliver and PinnedHeaderSliver can render correctly when used together.', ( + WidgetTester tester, + ) async { + const ValueKey key = ValueKey('sliver_tree_pined_header'); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: RepaintBoundary( + key: key, + child: SizedBox( + height: 20, + width: 20, + child: CustomScrollView( + slivers: [ + const PinnedHeaderSliver(child: SizedBox(height: 10)), + TreeSliver( + tree: >[TreeSliverNode(Object())], + treeRowExtentBuilder: (_, _) => 10, + treeNodeBuilder: ( + BuildContext context, + TreeSliverNode node, + AnimationStyle animationStyle, + ) { + return const ColoredBox(color: Colors.red); + }, + ), + ], + ), + ), + ), + ), + ), + ); + await expectLater(find.byKey(key), matchesGoldenFile('sliver_tree.pined_header.0.png')); + expect(tester.getTopLeft(find.byType(ColoredBox)), const Offset(0, 10)); + }); } diff --git a/packages/flutter/test/widgets/slivers_test.dart b/packages/flutter/test/widgets/slivers_test.dart index 66399e23bf..de4b6d8bb5 100644 --- a/packages/flutter/test/widgets/slivers_test.dart +++ b/packages/flutter/test/widgets/slivers_test.dart @@ -726,7 +726,7 @@ void main() { }, ); - Widget boilerPlate(Widget sliver) { + Widget boilerPlate(List slivers) { return Localizations( locale: const Locale('en', 'us'), delegates: const >[ @@ -735,10 +735,7 @@ void main() { ], child: Directionality( textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: CustomScrollView(slivers: [sliver]), - ), + child: MediaQuery(data: const MediaQueryData(), child: CustomScrollView(slivers: slivers)), ), ); } @@ -747,7 +744,7 @@ void main() { testWidgets('offstage true', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( - boilerPlate(const SliverOffstage(sliver: SliverToBoxAdapter(child: Text('a')))), + boilerPlate([const SliverOffstage(sliver: SliverToBoxAdapter(child: Text('a')))]), ); expect(semantics.nodesWith(label: 'a'), hasLength(0)); @@ -762,9 +759,9 @@ void main() { testWidgets('offstage false', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( - boilerPlate( + boilerPlate([ const SliverOffstage(offstage: false, sliver: SliverToBoxAdapter(child: Text('a'))), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(1)); @@ -783,12 +780,12 @@ void main() { // Opacity 1.0: Semantics and painting await tester.pumpWidget( - boilerPlate( + boilerPlate([ const SliverOpacity( sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)), opacity: 1.0, ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(1)); @@ -796,12 +793,12 @@ void main() { // Opacity 0.0: Nothing await tester.pumpWidget( - boilerPlate( + boilerPlate([ const SliverOpacity( sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)), opacity: 0.0, ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(0)); @@ -809,13 +806,13 @@ void main() { // Opacity 0.0 with semantics: Just semantics await tester.pumpWidget( - boilerPlate( + boilerPlate([ const SliverOpacity( sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)), opacity: 0.0, alwaysIncludeSemantics: true, ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(1)); @@ -823,12 +820,12 @@ void main() { // Opacity 0.0 without semantics: Nothing await tester.pumpWidget( - boilerPlate( + boilerPlate([ const SliverOpacity( sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)), opacity: 0.0, ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(0)); @@ -836,12 +833,12 @@ void main() { // Opacity 0.1: Semantics and painting await tester.pumpWidget( - boilerPlate( + boilerPlate([ const SliverOpacity( sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)), opacity: 0.1, ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(1)); @@ -849,12 +846,12 @@ void main() { // Opacity 0.1 without semantics: Still has semantics and painting await tester.pumpWidget( - boilerPlate( + boilerPlate([ const SliverOpacity( sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)), opacity: 0.1, ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(1)); @@ -862,13 +859,13 @@ void main() { // Opacity 0.1 with semantics: Semantics and painting await tester.pumpWidget( - boilerPlate( + boilerPlate([ const SliverOpacity( sliver: SliverToBoxAdapter(child: Text('a', textDirection: TextDirection.rtl)), opacity: 0.1, alwaysIncludeSemantics: true, ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(1)); @@ -883,7 +880,7 @@ void main() { final SemanticsTester semantics = SemanticsTester(tester); final List events = []; await tester.pumpWidget( - boilerPlate( + boilerPlate([ SliverIgnorePointer( ignoringSemantics: false, sliver: SliverToBoxAdapter( @@ -895,7 +892,7 @@ void main() { ), ), ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(1)); await tester.tap(find.byType(GestureDetector), warnIfMissed: false); @@ -907,7 +904,7 @@ void main() { final SemanticsTester semantics = SemanticsTester(tester); final List events = []; await tester.pumpWidget( - boilerPlate( + boilerPlate([ SliverIgnorePointer( ignoring: false, ignoringSemantics: true, @@ -920,7 +917,7 @@ void main() { ), ), ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(0)); await tester.tap(find.byType(GestureDetector)); @@ -931,13 +928,13 @@ void main() { testWidgets('ignoring only block semantics actions', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( - boilerPlate( + boilerPlate([ SliverIgnorePointer( sliver: SliverToBoxAdapter( child: GestureDetector(child: const Text('a'), onTap: () {}), ), ), - ), + ]), ); expect(semantics, includesNodeWith(label: 'a', actions: [])); semantics.dispose(); @@ -947,7 +944,7 @@ void main() { final SemanticsTester semantics = SemanticsTester(tester); final List events = []; await tester.pumpWidget( - boilerPlate( + boilerPlate([ SliverIgnorePointer( ignoringSemantics: true, sliver: SliverToBoxAdapter( @@ -959,7 +956,7 @@ void main() { ), ), ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(0)); await tester.tap(find.byType(GestureDetector), warnIfMissed: false); @@ -971,7 +968,7 @@ void main() { final SemanticsTester semantics = SemanticsTester(tester); final List events = []; await tester.pumpWidget( - boilerPlate( + boilerPlate([ SliverIgnorePointer( ignoring: false, ignoringSemantics: false, @@ -984,7 +981,7 @@ void main() { ), ), ), - ), + ]), ); expect(semantics.nodesWith(label: 'a'), hasLength(1)); await tester.tap(find.byType(GestureDetector)); @@ -993,6 +990,40 @@ void main() { }); }); + group('SliverEnsureSemantics - ', () { + testWidgets('ensure semantics', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + await tester.pumpWidget( + boilerPlate([ + const SliverEnsureSemantics(sliver: SliverToBoxAdapter(child: Text('a'))), + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text('Lorem Ipsum $index'), + ), + ); + }, + childCount: 50, + semanticIndexOffset: 1, + ), + ), + const SliverEnsureSemantics(sliver: SliverToBoxAdapter(child: Text('b'))), + ]), + ); + + // Even though 'b' is outside of the Viewport and cacheExtent, since it is + // wrapped with a `SliverEnsureSemantics` it will still be included in the + // semantics tree. + expect(semantics.nodesWith(label: 'b'), hasLength(1)); + expect(find.text('b'), findsNothing); + expect(find.byType(SliverEnsureSemantics, skipOffstage: false), findsNWidgets(2)); + semantics.dispose(); + }); + }); + testWidgets('SliverList handles 0 scrollOffsetCorrection', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/62198 await tester.pumpWidget( diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 8165fe3c5e..4cebf976da 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -258,8 +258,6 @@ List generateCommands({required bool verboseHelp, required bool platform: globals.platform, shutdownHooks: globals.shutdownHooks, os: globals.os, - processManager: globals.processManager, - artifacts: globals.artifacts!, ), UpgradeCommand(verboseHelp: verboseHelp), SymbolizeCommand(stdio: globals.stdio, fileSystem: globals.fs), diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart index 8c94b70cf6..abc7e66bbd 100644 --- a/packages/flutter_tools/lib/src/commands/widget_preview.dart +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -5,9 +5,7 @@ import 'package:args/args.dart'; import 'package:meta/meta.dart'; import 'package:package_config/package_config.dart'; -import 'package:process/process.dart'; -import '../artifacts.dart'; import '../base/common.dart'; import '../base/deferred_component.dart'; import '../base/file_system.dart'; @@ -26,8 +24,6 @@ import '../linux/build_linux.dart'; import '../macos/build_macos.dart'; import '../project.dart'; import '../runner/flutter_command.dart'; -import '../runner/flutter_command_runner.dart'; -import '../widget_preview/dtd_services.dart'; import '../widget_preview/preview_code_generator.dart'; import '../widget_preview/preview_detector.dart'; import '../widget_preview/preview_manifest.dart'; @@ -45,8 +41,6 @@ class WidgetPreviewCommand extends FlutterCommand { required Platform platform, required ShutdownHooks shutdownHooks, required OperatingSystemUtils os, - required ProcessManager processManager, - required Artifacts artifacts, }) { addSubcommand( WidgetPreviewStartCommand( @@ -58,8 +52,6 @@ class WidgetPreviewCommand extends FlutterCommand { platform: platform, shutdownHooks: shutdownHooks, os: os, - processManager: processManager, - artifacts: artifacts, ), ); addSubcommand( @@ -126,8 +118,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C required this.platform, required this.shutdownHooks, required this.os, - required this.processManager, - required this.artifacts, }) { addPubOptions(); argParser @@ -162,9 +152,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C static const String kHeadlessWeb = 'headless-web'; static const String kWidgetPreviewScaffoldOutputDir = 'scaffold-output-dir'; - /// Environment variable used to pass the DTD URI to the widget preview scaffold. - static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI'; - @override Future> get requiredArtifacts async => const { // Ensure the Flutter Web SDK is installed. @@ -198,10 +185,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C final OperatingSystemUtils os; - final ProcessManager processManager; - - final Artifacts artifacts; - late final FlutterProject rootProject = getRootProject(); late final PreviewDetector _previewDetector = PreviewDetector( @@ -220,12 +203,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C cache: cache, ); - late final WidgetPreviewDtdServices _dtdService = WidgetPreviewDtdServices( - logger: logger, - shutdownHooks: shutdownHooks, - dtdLauncher: DtdLauncher(logger: logger, artifacts: artifacts, processManager: processManager), - ); - /// The currently running instance of the widget preview scaffold. AppInstance? _widgetPreviewApp; @@ -307,7 +284,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C shutdownHooks.addShutdownHook(() async { await _widgetPreviewApp?.stop(); }); - await configureDtd(); _widgetPreviewApp = await runPreviewEnvironment( widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject, ); @@ -333,31 +309,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C _populatePreviewPubspec(rootProject: rootProject); } - /// Configures the Dart Tooling Daemon connection. - /// - /// If --dtd-uri is provided, the existing DTD instance will be used. If the tool fails to - /// connect to this URI, it will start its own DTD instance. - /// - /// 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); - Uri? existingDtdUri; - try { - if (existingDtdUriStr != null) { - existingDtdUri = Uri.parse(existingDtdUriStr); - } - } on FormatException { - logger.printWarning('Failed to parse value of --dtd-uri: $existingDtdUriStr.'); - } - if (existingDtdUri == null) { - logger.printTrace('Launching a fresh DTD instance...'); - await _dtdService.launchAndConnect(); - } else { - logger.printTrace('Connecting to existing DTD instance at: $existingDtdUri...'); - await _dtdService.connect(dtdWsUri: existingDtdUri); - } - } - /// Builds the application binary for the widget preview scaffold the first /// time the widget preview command is run. /// @@ -506,12 +457,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C BuildMode.debug, null, treeShakeIcons: false, - // Provide the DTD connection information directly to the preview scaffold. - // This could, in theory, be provided via a follow up call to a service extension - // registered by the preview scaffold, but there's some uncertainty around how service - // extensions will work with Flutter web embedded in VSCode without a Chrome debugger - // connection. - dartDefines: ['$kWidgetPreviewDtdUriEnvVar=${_dtdService.dtdUri}'], extraFrontEndOptions: isWeb ? ['--dartdevc-canary', '--dartdevc-module-format=ddc'] : null, packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path, @@ -654,7 +599,6 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C if (offline) '--offline', '--directory', widgetPreviewScaffoldProject.directory.path, - 'dtd', 'flutter_lints', 'stack_trace', ], diff --git a/packages/flutter_tools/lib/src/compute_dev_dependencies.dart b/packages/flutter_tools/lib/src/compute_dev_dependencies.dart index 007f30d8cc..d679d4ad54 100644 --- a/packages/flutter_tools/lib/src/compute_dev_dependencies.dart +++ b/packages/flutter_tools/lib/src/compute_dev_dependencies.dart @@ -18,7 +18,13 @@ Future> computeExclusiveDevDependencies( required Logger logger, required FlutterProject project, }) async { - final Map jsonResult = await pub.deps(project); + final Map? jsonResult = await pub.deps(project); + + // Avoid crashing if dart pub deps is not ready. + // See https://github.com/flutter/flutter/issues/166648. + if (jsonResult == null) { + return {}; + } Never fail([String? reason]) { logger.printTrace(const JsonEncoder.withIndent(' ').convert(jsonResult)); diff --git a/packages/flutter_tools/lib/src/custom_devices/custom_device.dart b/packages/flutter_tools/lib/src/custom_devices/custom_device.dart index 66aa535189..f362f7afac 100644 --- a/packages/flutter_tools/lib/src/custom_devices/custom_device.dart +++ b/packages/flutter_tools/lib/src/custom_devices/custom_device.dart @@ -8,6 +8,7 @@ import 'package:meta/meta.dart'; import 'package:process/process.dart'; import '../application_package.dart'; +import '../artifacts.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; @@ -21,6 +22,7 @@ import '../convert.dart'; import '../device.dart'; import '../device_port_forwarder.dart'; import '../features.dart'; +import '../globals.dart' as globals; import '../project.dart'; import '../protocol_discovery.dart'; import '../vmservice.dart'; @@ -341,6 +343,7 @@ class CustomDeviceAppSession { Map platformArgs = const {}, bool prebuiltApplication = false, String? userIdentifier, + Map additionalReplacementValues = const {}, }) async { final bool traceStartup = platformArgs['trace-startup'] as bool? ?? false; final String? packageName = _appPackage.name; @@ -352,7 +355,7 @@ class CustomDeviceAppSession { 'remotePath': '/tmp/', 'appName': packageName, 'engineOptions': _getEngineOptionsForCmdline(debuggingOptions, traceStartup, route), - }); + }, additionalReplacementValues: additionalReplacementValues); final Process process = await _processUtils.start(interpolated); assert(_process == null); @@ -700,6 +703,16 @@ class CustomDevice extends Device { String? userIdentifier, BundleBuilder? bundleBuilder, }) async { + final TargetPlatform platform = await targetPlatform; + final Artifacts artifacts = globals.artifacts!; + + final Map additionalReplacementValues = { + 'buildMode': debuggingOptions.buildInfo.modeName, + 'icuDataPath': artifacts.getArtifactPath(Artifact.icuData, platform: platform), + 'engineRevision': + artifacts.usesLocalArtifacts ? 'local' : globals.flutterVersion.engineRevision, + }; + if (!prebuiltApplication) { final String assetBundleDir = getAssetBuildDirectory(); @@ -707,7 +720,7 @@ class CustomDevice extends Device { // this just builds the asset bundle, it's the same as `flutter build bundle` await bundleBuilder.build( - platform: await targetPlatform, + platform: platform, buildInfo: debuggingOptions.buildInfo, mainPath: mainPath, depfilePath: defaultDepfilePath, @@ -720,7 +733,11 @@ class CustomDevice extends Device { if (packageName == null) { throwToolExit('Could not start app, name for $package is unknown.'); } - await _tryPostBuild(appName: packageName, localPath: assetBundleDir); + await _tryPostBuild( + appName: packageName, + localPath: assetBundleDir, + additionalReplacementValues: additionalReplacementValues, + ); } } @@ -736,6 +753,7 @@ class CustomDevice extends Device { platformArgs: platformArgs, prebuiltApplication: prebuiltApplication, userIdentifier: userIdentifier, + additionalReplacementValues: additionalReplacementValues, ); } diff --git a/packages/flutter_tools/lib/src/dart/pub.dart b/packages/flutter_tools/lib/src/dart/pub.dart index 5d01157463..640c151e6f 100644 --- a/packages/flutter_tools/lib/src/dart/pub.dart +++ b/packages/flutter_tools/lib/src/dart/pub.dart @@ -158,7 +158,9 @@ abstract class Pub { /// While it is guaranteed that, if successful, that the result are a valid /// JSON object, the exact contents returned are _not_ validated, and are left /// as a responsibility of the caller. - Future> deps(FlutterProject project); + /// + /// If `null` is returned, it should be assumed deps could not be determined. + Future?> deps(FlutterProject project); /// Runs pub in 'batch' mode. /// @@ -354,13 +356,22 @@ class _DefaultPub implements Pub { } @override - Future> deps(FlutterProject project) async { + Future?> deps(FlutterProject project) async { final List pubCommand = [..._pubCommand, 'deps', '--json']; + final RunResult runResult; - final RunResult runResult = await _processUtils.run( - pubCommand, - workingDirectory: project.directory.path, - ); + // Don't treat this command as terminal if it fails. + // See https://github.com/flutter/flutter/issues/166648 + try { + runResult = await _processUtils.run( + pubCommand, + workingDirectory: project.directory.path, + throwOnError: true, + ); + } on io.ProcessException catch (e) { + _logger.printWarning('${pubCommand.join(' ')} ${e.message}'); + return null; + } Never fail([String? reason]) { final String stdout = runResult.stdout; @@ -374,11 +385,6 @@ class _DefaultPub implements Pub { ); } - // Guard against dart pub deps crashing. - if (runResult.exitCode != 0) { - fail(); - } - // Guard against dart pub deps having explicitly invalid output. try { final Object? result = json.decode(runResult.stdout); 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 d8f342d9b7..158550c7cd 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 String kMachineFlag = 'machine'; static const String kPackagesOption = 'packages'; static const String kPrefixedErrorsFlag = 'prefixed-errors'; - static const String kDtdUrl = 'dtd-url'; static const String kPrintDtd = 'print-dtd'; static const String kQuietFlag = 'quiet'; static const String 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/lib/src/widget_preview/dtd_services.dart b/packages/flutter_tools/lib/src/widget_preview/dtd_services.dart deleted file mode 100644 index 1a28fa6c08..0000000000 --- a/packages/flutter_tools/lib/src/widget_preview/dtd_services.dart +++ /dev/null @@ -1,101 +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:async'; - -import 'package:dtd/dtd.dart'; -import 'package:process/process.dart'; - -import '../artifacts.dart'; -import '../base/common.dart'; -import '../base/io.dart'; -import '../base/logger.dart'; -import '../base/process.dart'; -import '../convert.dart'; - -/// Provides services, streams, and RPC invocations to interact with the Widget Preview Scaffold. -class WidgetPreviewDtdServices { - WidgetPreviewDtdServices({ - required this.logger, - required this.shutdownHooks, - required this.dtdLauncher, - }) { - shutdownHooks.addShutdownHook(() async { - await _dtd?.close(); - await dtdLauncher.dispose(); - }); - } - - final Logger logger; - final ShutdownHooks shutdownHooks; - final DtdLauncher dtdLauncher; - - DartToolingDaemon? _dtd; - - /// The [Uri] pointing to the currently connected DTD instance. - /// - /// Returns `null` if there is no DTD connection. - Uri? get dtdUri => _dtdUri; - Uri? _dtdUri; - - /// Starts DTD in a child process before invoking [connect] with a [Uri] pointing to the new - /// DTD instance. - Future launchAndConnect() async { - // Connect to the new DTD instance. - await connect(dtdWsUri: await dtdLauncher.launch()); - } - - /// Connects to an existing DTD instance and registers any relevant services. - Future connect({required Uri dtdWsUri}) async { - _dtdUri = dtdWsUri; - _dtd = await DartToolingDaemon.connect(dtdWsUri); - // TODO(bkonyi): register services. - logger.printTrace('Connected to DTD and registered services.'); - } -} - -/// Manages the lifecycle of a Dart Tooling Daemon (DTD) instance. -class DtdLauncher { - DtdLauncher({required this.logger, required this.artifacts, required this.processManager}); - - /// Starts a new DTD instance and returns the web socket URI it's available on. - Future launch() async { - if (_dtdProcess != null) { - throw StateError('Attempted to launch DTD twice.'); - } - - // Start DTD. - _dtdProcess = await processManager.start([ - artifacts.getArtifactPath(Artifact.engineDartBinary), - 'tooling-daemon', - '--machine', - ]); - - // Wait for the DTD connection information. - final Completer dtdUri = Completer(); - late final StreamSubscription sub; - sub = _dtdProcess!.stdout.transform(const Utf8Decoder()).listen((String data) async { - await sub.cancel(); - final Map jsonData = json.decode(data) as Map; - if (jsonData case {'tooling_daemon_details': {'uri': final String dtdUriString}}) { - dtdUri.complete(Uri.parse(dtdUriString)); - } else { - throwToolExit('Unable to start the Dart Tooling Daemon.'); - } - }); - return dtdUri.future; - } - - /// Kills the spawned DTD instance. - Future dispose() async { - _dtdProcess?.kill(); - _dtdProcess = null; - } - - final Logger logger; - final Artifacts artifacts; - final ProcessManager processManager; - - Process? _dtdProcess; -} diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index 65fc5cf372..090aa0e8ea 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: ffi: 2.1.4 file: 7.0.1 flutter_template_images: 5.0.0 - html: 0.15.5 + html: 0.15.5+1 http: 1.3.0 intl: 0.20.2 meta: 1.16.0 @@ -122,4 +122,4 @@ dartdoc: # Exclude this package from the hosted API docs. nodoc: true -# PUBSPEC CHECKSUM: fe37 +# PUBSPEC CHECKSUM: c093 diff --git a/packages/flutter_tools/static/custom-devices.schema.json b/packages/flutter_tools/static/custom-devices.schema.json index da0a78400d..9c5b894b82 100644 --- a/packages/flutter_tools/static/custom-devices.schema.json +++ b/packages/flutter_tools/static/custom-devices.schema.json @@ -59,7 +59,7 @@ "required": false }, "postBuild": { - "description": "The command to be invoked after the build process is done, to do any additional packaging for example.", + "description": "The command to be invoked after the build process is done, to do any additional packaging for example. The following variables are available via string interpolation:\n- ${appName}\n- ${localPath}\n- ${buildMode}\n- ${icuDataPath}\n- ${engineRevision}", "type": ["array", "null"], "items": { "type": "string" @@ -91,7 +91,7 @@ ] }, "runDebug": { - "description": "The command to be invoked to run the app in debug mode. The name of the app to be started is available via the ${appName} string interpolation. Make sure the flutter cmdline output is available via this commands stdout/stderr since the SDK needs the \"VM Service is now listening on ...\" message to function. If the forwardPort command is not specified, the VM Service URL will be connected to as-is, without any port forwarding. In that case you need to make sure it is reachable from your host device, possibly via the \"--vm-service-host=\" engine flag.", + "description": "The command to be invoked to run the app in debug mode. Make sure the flutter cmdline output is available via this commands stdout/stderr since the SDK needs the \"VM Service is now listening on ...\" message to function. If the forwardPort command is not specified, the VM Service URL will be connected to as-is, without any port forwarding. In that case you need to make sure it is reachable from your host device, possibly via the \"--vm-service-host=\" engine flag. The following variables are available via string interpolation:\n- ${appName}\n- ${engineOptions}\n- ${buildMode}\n- ${icuDataPath}\n- ${engineRevision}", "type": "array", "items": { "type": "string" diff --git a/packages/flutter_tools/templates/template_manifest.json b/packages/flutter_tools/templates/template_manifest.json index 454040e21a..04f74048da 100644 --- a/packages/flutter_tools/templates/template_manifest.json +++ b/packages/flutter_tools/templates/template_manifest.json @@ -356,7 +356,6 @@ "templates/widget_preview_scaffold/lib/src/widget_preview.dart.tmpl", "templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl", "templates/widget_preview_scaffold/lib/src/controls.dart.tmpl", - "templates/widget_preview_scaffold/lib/src/dtd_services.dart.tmpl", "templates/widget_preview_scaffold/lib/src/generated_preview.dart.tmpl", "templates/widget_preview_scaffold/lib/src/utils.dart.tmpl", "templates/widget_preview_scaffold/pubspec.yaml.tmpl", diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd_services.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd_services.dart.tmpl deleted file mode 100644 index eaff8e7fb0..0000000000 --- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/dtd_services.dart.tmpl +++ /dev/null @@ -1,33 +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:async'; - -import 'package:dtd/dtd.dart'; - -/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling. -class WidgetPreviewScaffoldDtdServices { - /// Environment variable for the DTD URI. - static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI'; - - /// Connects to the Dart Tooling Daemon (DTD) specified by the Flutter tool. - /// - /// If the connection is successful, the Widget Preview Scaffold will register services and - /// subscribe to various streams to interact directly with other tooling (e.g., IDEs). - Future connect() async { - final Uri dtdWsUri = Uri.parse( - const String.fromEnvironment(kWidgetPreviewDtdUriEnvVar), - ); - _dtd = await DartToolingDaemon.connect(dtdWsUri); - unawaited( - _dtd.postEvent( - 'WidgetPreviewScaffold', - 'Connected', - const {}, - ), - ); - } - - late final DartToolingDaemon _dtd; -} diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl index 568743d90e..8449dd0ec5 100644 --- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl +++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/src/widget_preview_rendering.dart.tmpl @@ -13,7 +13,6 @@ import 'package:flutter/services.dart'; import 'package:stack_trace/stack_trace.dart'; import 'controls.dart'; -import 'dtd_services.dart'; import 'generated_preview.dart'; import 'utils.dart'; import 'widget_preview.dart'; @@ -411,8 +410,6 @@ class PreviewAssetBundle extends PlatformAssetBundle { /// the preview scaffold project which prevents us from being able to use hot /// restart to iterate on this file. Future mainImpl() async { - // TODO(bkonyi): store somewhere. - await WidgetPreviewScaffoldDtdServices().connect(); runApp(_WidgetPreviewScaffold()); } diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/pubspec.yaml.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/pubspec.yaml.tmpl index c74199d7ab..ca4a8ecee5 100644 --- a/packages/flutter_tools/templates/widget_preview_scaffold/pubspec.yaml.tmpl +++ b/packages/flutter_tools/templates/widget_preview_scaffold/pubspec.yaml.tmpl @@ -12,7 +12,6 @@ dependencies: flutter_test: sdk: flutter # These will be replaced with proper constraints after the template is hydrated. - dtd: any flutter_lints: any stack_trace: any diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/widget_preview_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/widget_preview_test.dart index b624e5917b..cbfaf3b439 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/widget_preview_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview/widget_preview_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:file/memory.dart'; -import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; @@ -48,8 +47,6 @@ void main() { platform: platform, processManager: processManager, ), - processManager: FakeProcessManager.any(), - artifacts: Artifacts.test(fileSystem: fileSystem), ); rootProject = FakeFlutterProject( projectRoot: 'some_project', diff --git a/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart b/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart index 7a90ec2772..d5e4cc81d8 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart @@ -6,7 +6,6 @@ import 'dart:io' as io show IOOverrides; import 'package:args/command_runner.dart'; import 'package:file_testing/file_testing.dart'; -import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/bot_detector.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -81,8 +80,6 @@ void main() { logger: logger, platform: platform, ), - artifacts: Artifacts.test(), - processManager: FakeProcessManager.any(), ), ); await runner.run(['widget-preview', ...arguments]); diff --git a/packages/flutter_tools/test/general.shard/compute_dev_dependencies_test.dart b/packages/flutter_tools/test/general.shard/compute_dev_dependencies_test.dart index 4f59e91566..1e3a584426 100644 --- a/packages/flutter_tools/test/general.shard/compute_dev_dependencies_test.dart +++ b/packages/flutter_tools/test/general.shard/compute_dev_dependencies_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' as io; + import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/logger.dart'; @@ -356,6 +358,17 @@ void main() { ); }); + test('a pub error is treated as no data available instead of terminal', () async { + final ProcessManager processes = _dartPubDepsCrashes(project: project); + final Set dependencies = await computeExclusiveDevDependencies( + pub(processes), + project: project, + logger: logger, + ); + + expect(dependencies, isEmpty, reason: 'pub deps crashed, but was not terminal'); + }); + test('throws and logs on invalid JSON', () async { final ProcessManager processes = _dartPubDepsReturns(''' { @@ -420,3 +433,18 @@ ProcessManager _dartPubDepsReturns(String dartPubDepsOutput, {required FlutterPr ), ]); } + +ProcessManager _dartPubDepsCrashes({required FlutterProject project}) { + return FakeProcessManager.list([ + FakeCommand( + command: const [_dartBin, 'pub', '--suppress-analytics', 'deps', '--json'], + workingDirectory: project.directory.path, + exception: const io.ProcessException('pub', [ + 'pub', + '--suppress-analytics', + 'deps', + '--json', + ]), + ), + ]); +} diff --git a/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart b/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart index bf0f9f2dda..3250d11bd5 100644 --- a/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart +++ b/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; +import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_system/build_system.dart'; @@ -530,6 +531,100 @@ void main() { }, ); + testUsingContext( + 'custom device command string interpolation end-to-end test', + () async { + final Completer runDebugCompleter = Completer(); + + final CustomDeviceConfig config = testConfig.copyWith( + platform: TargetPlatform.linux_arm64, + postBuildCommand: const [ + 'testpostbuild', + r'--buildMode=${buildMode}', + r'--icuDataPath=${icuDataPath}', + r'--engineRevision=${engineRevision}', + ], + runDebugCommand: const [ + 'testrundebug', + r'--buildMode=${buildMode}', + r'--icuDataPath=${icuDataPath}', + r'--engineRevision=${engineRevision}', + ], + ); + + final List commandArgumentsPattern = [ + RegExp(r'--buildMode=.*'), + RegExp(r'--icuDataPath=.*'), + RegExp(r'--engineRevision=.*'), + ]; + + final String expectedIcuDataPath = globals.artifacts!.getArtifactPath( + Artifact.icuData, + platform: config.platform, + ); + final String expectedEngineRevision = globals.flutterVersion.engineRevision; + + final List expectedCommandArguments = [ + '--buildMode=debug', + '--icuDataPath=$expectedIcuDataPath', + '--engineRevision=$expectedEngineRevision', + ]; + + final List expectedRunDebugCommand = [ + 'testrundebug', + ...expectedCommandArguments, + ]; + final List expectedPostBuildCommand = [ + 'testpostbuild', + ...expectedCommandArguments, + ]; + + final FakeProcessManager processManager = FakeProcessManager.list([ + FakeCommand( + command: ['testpostbuild', ...commandArgumentsPattern], + onRun: (List command) => expect(command, expectedPostBuildCommand), + ), + FakeCommand(command: config.uninstallCommand), + FakeCommand(command: config.installCommand), + FakeCommand( + command: ['testrundebug', ...commandArgumentsPattern], + completer: runDebugCompleter, + onRun: (List command) => expect(command, expectedRunDebugCommand), + stdout: 'The Dart VM service is listening on http://127.0.0.1:12345/abcd/\n', + ), + FakeCommand( + command: config.forwardPortCommand!, + stdout: testConfigForwardPortSuccessOutput, + ), + ]); + + // CustomDevice.startApp doesn't care whether we pass a prebuilt app or + // buildable app as long as we pass prebuiltApplication as false + final PrebuiltLinuxApp app = PrebuiltLinuxApp(executable: 'testexecutable'); + + // finally start actually testing things + final CustomDevice device = CustomDevice( + config: config, + logger: BufferLogger.test(), + processManager: processManager, + ); + + await device.startApp( + app, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + bundleBuilder: FakeBundleBuilder(), + ); + expect(runDebugCompleter.isCompleted, false); + + expect(await device.stopApp(app), true); + expect(runDebugCompleter.isCompleted, true); + }, + overrides: { + FileSystem: () => MemoryFileSystem.test(), + ProcessManager: () => FakeProcessManager.any(), + }, + ); + testWithoutContext('CustomDevice screenshotting', () async { bool screenshotCommandWasExecuted = false; diff --git a/packages/flutter_tools/test/general.shard/dart/pub_deps_test.dart b/packages/flutter_tools/test/general.shard/dart/pub_deps_test.dart index d8306d0bdd..5b1f546af8 100644 --- a/packages/flutter_tools/test/general.shard/dart/pub_deps_test.dart +++ b/packages/flutter_tools/test/general.shard/dart/pub_deps_test.dart @@ -43,7 +43,7 @@ void main() { ); }); - testWithoutContext('fails on non-zero exit code', () async { + testWithoutContext('returns null on non-zero exit code', () async { final BufferLogger logger = BufferLogger.test(); final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final ProcessManager processManager = _dartPubDepsFails( @@ -62,14 +62,8 @@ void main() { ); await expectLater( - () => pub.deps(FlutterProject.fromDirectoryTest(fileSystem.currentDirectory)), - throwsA( - isA().having( - (StateError e) => e.message, - 'message', - contains('dart pub --suppress-analytics deps --json failed'), - ), - ), + pub.deps(FlutterProject.fromDirectoryTest(fileSystem.currentDirectory)), + completion(isNull), ); }); 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 f080f6b051..14cfc1c6ae 100644 --- a/packages/flutter_tools/test/integration.shard/widget_preview_test.dart +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test.dart @@ -5,18 +5,12 @@ import 'dart:async'; import 'dart:convert'; -import 'package:dtd/dtd.dart'; import 'package:file/file.dart'; 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'; import '../src/common.dart'; -import '../src/context.dart'; import 'test_data/basic_project.dart'; import 'test_utils.dart'; @@ -49,13 +43,10 @@ const List subsequentLaunchMessagesWeb = [ void main() { late Directory tempDir; Process? process; - Logger? logger; - DtdLauncher? dtdLauncher; final BasicProject project = BasicProject(); const ProcessManager processManager = LocalProcessManager(); setUp(() async { - logger = BufferLogger.test(); tempDir = createResolvedTempDirectorySync('widget_preview_test.'); await project.setUpIn(tempDir); }); @@ -63,15 +54,12 @@ void main() { tearDown(() async { process?.kill(); process = null; - await dtdLauncher?.dispose(); - dtdLauncher = null; tryToDelete(tempDir); }); Future runWidgetPreview({ required List expectedMessages, bool useWeb = false, - Uri? dtdUri, }) async { expect(expectedMessages, isNotEmpty); int i = 0; @@ -84,7 +72,6 @@ void main() { '--${WidgetPreviewStartCommand.kHeadlessWeb}' else '--${WidgetPreviewStartCommand.kUseFlutterDesktop}', - if (dtdUri != null) '--${FlutterGlobalOptions.kDtdUrl}=$dtdUri', ], workingDirectory: tempDir.path); final Completer completer = Completer(); @@ -116,6 +103,8 @@ void main() { }), ); await completer.future; + process!.kill(); + process = null; } group('flutter widget-preview start', () { @@ -143,38 +132,5 @@ void main() { // We shouldn't regenerate the scaffold after the initial run. await runWidgetPreview(expectedMessages: subsequentLaunchMessagesWeb, useWeb: true); }); - - testUsingContext('can connect to an existing DTD instance', () async { - dtdLauncher = DtdLauncher( - logger: logger!, - artifacts: globals.artifacts!, - processManager: globals.processManager, - ); - - // Start a DTD instance. - final Uri dtdUri = await dtdLauncher!.launch(); - - // Connect to it and listen to the WidgetPreviewScaffold stream. - // - // The preview scaffold will send a 'Connected' event on this stream once it has initialized - // and is ready. - final DartToolingDaemon dtdConnection = await DartToolingDaemon.connect(dtdUri); - const String kWidgetPreviewScaffoldStream = 'WidgetPreviewScaffold'; - final Completer completer = Completer(); - dtdConnection.onEvent(kWidgetPreviewScaffoldStream).listen((DTDEvent event) { - expect(event.stream, kWidgetPreviewScaffoldStream); - expect(event.kind, 'Connected'); - completer.complete(); - }); - await dtdConnection.streamListen(kWidgetPreviewScaffoldStream); - - // Start the widget preview and wait for the 'Connected' event. - await runWidgetPreview( - expectedMessages: firstLaunchMessagesWeb, - useWeb: true, - dtdUri: dtdUri, - ); - await completer.future; - }); }); } diff --git a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/dtd_services.dart b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/dtd_services.dart deleted file mode 100644 index eaff8e7fb0..0000000000 --- a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/dtd_services.dart +++ /dev/null @@ -1,33 +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:async'; - -import 'package:dtd/dtd.dart'; - -/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling. -class WidgetPreviewScaffoldDtdServices { - /// Environment variable for the DTD URI. - static const String kWidgetPreviewDtdUriEnvVar = 'WIDGET_PREVIEW_DTD_URI'; - - /// Connects to the Dart Tooling Daemon (DTD) specified by the Flutter tool. - /// - /// If the connection is successful, the Widget Preview Scaffold will register services and - /// subscribe to various streams to interact directly with other tooling (e.g., IDEs). - Future connect() async { - final Uri dtdWsUri = Uri.parse( - const String.fromEnvironment(kWidgetPreviewDtdUriEnvVar), - ); - _dtd = await DartToolingDaemon.connect(dtdWsUri); - unawaited( - _dtd.postEvent( - 'WidgetPreviewScaffold', - 'Connected', - const {}, - ), - ); - } - - late final DartToolingDaemon _dtd; -} diff --git a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview_rendering.dart b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview_rendering.dart index 568743d90e..8449dd0ec5 100644 --- a/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview_rendering.dart +++ b/packages/flutter_tools/test/widget_preview_scaffold.shard/widget_preview_scaffold/lib/src/widget_preview_rendering.dart @@ -13,7 +13,6 @@ import 'package:flutter/services.dart'; import 'package:stack_trace/stack_trace.dart'; import 'controls.dart'; -import 'dtd_services.dart'; import 'generated_preview.dart'; import 'utils.dart'; import 'widget_preview.dart'; @@ -411,8 +410,6 @@ class PreviewAssetBundle extends PlatformAssetBundle { /// the preview scaffold project which prevents us from being able to use hot /// restart to iterate on this file. Future mainImpl() async { - // TODO(bkonyi): store somewhere. - await WidgetPreviewScaffoldDtdServices().connect(); runApp(_WidgetPreviewScaffold()); } 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 411eb4a65c..598273ede0 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 @@ -12,7 +12,6 @@ dependencies: flutter_test: sdk: flutter # These will be replaced with proper constraints after the template is hydrated. - dtd: 2.5.0 flutter_lints: 5.0.0 stack_trace: 1.12.1 @@ -21,13 +20,7 @@ dependencies: characters: 1.4.0 # 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" collection: 1.19.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - convert: 3.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" fake_async: 1.3.3 # 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" - http: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - http_parser: 4.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - json_rpc_2: 3.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" leak_tracker: 10.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" leak_tracker_flutter_testing: 3.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" leak_tracker_testing: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" @@ -41,15 +34,10 @@ dependencies: string_scanner: 1.4.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.4 # 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" - unified_analytics: 7.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" vector_math: 2.1.4 # 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" - web: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket: 0.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" - web_socket_channel: 3.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" flutter: uses-material-design: true -# PUBSPEC CHECKSUM: 4b12 +# PUBSPEC CHECKSUM: 367e