diff --git a/BUILD.gn b/BUILD.gn index 704c0f2c8628d..409c5f912a570 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -195,6 +195,7 @@ group("unittests") { if (is_mac) { public_deps += [ "//flutter/impeller/golden_tests:impeller_golden_tests", + "//flutter/shell/platform/darwin/common:availability_version_check_unittests", "//flutter/shell/platform/darwin/common:framework_common_unittests", "//flutter/third_party/spring_animation:spring_animation_unittests", ] diff --git a/DEPS b/DEPS index f9814f12b8a9b..0386902e6ee43 100644 --- a/DEPS +++ b/DEPS @@ -18,7 +18,7 @@ vars = { 'llvm_git': 'https://llvm.googlesource.com', # OCMock is for testing only so there is no google clone 'ocmock_git': 'https://github.com/erikdoe/ocmock.git', - 'skia_revision': '33502f9b0c7d845adceb4907d1c7dc1f22b3fd4c', + 'skia_revision': '795ed944ff5bde5916d193824589d3bacfa61a7d', # WARNING: DO NOT EDIT canvaskit_cipd_instance MANUALLY # See `lib/web_ui/README.md` for how to roll CanvasKit to a new version. @@ -57,7 +57,7 @@ vars = { # Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. - 'dart_revision': '077d768accfa3419b8b57cf779cca8228b417faa', + 'dart_revision': '3b128c5454834a1aaef37d9bb12595e7c217ab61', # WARNING: DO NOT EDIT MANUALLY # The lines between blank lines above and below are generated by a script. See create_updated_flutter_deps.py @@ -262,7 +262,7 @@ allowed_hosts = [ ] deps = { - 'src': 'https://github.com/flutter/buildroot.git' + '@' + '9780f253156165c515962f5f0c56235d34617689', + 'src': 'https://github.com/flutter/buildroot.git' + '@' + 'f27d99b4428dea312646130d60c33a2d38fa9dc6', # Fuchsia compatibility # diff --git a/ci/builders/mac_ios_engine.json b/ci/builders/mac_ios_engine.json index 600f495929b0c..3dfc5c4d559b4 100644 --- a/ci/builders/mac_ios_engine.json +++ b/ci/builders/mac_ios_engine.json @@ -3,8 +3,8 @@ { "drone_dimensions": [ "device_type=none", - "mac_model=Macmini8,1", - "os=Mac-12" + "os=Mac-12", + "cpu=x86" ], "gn": [ "--ios", @@ -26,7 +26,8 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-12" + "os=Mac-12", + "cpu=x86" ], "gn": [ "--ios", @@ -49,7 +50,8 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-12" + "os=Mac-12", + "cpu=x86" ], "gn": [ "--ios", @@ -72,7 +74,8 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-12" + "os=Mac-12", + "cpu=x86" ], "gn": [ "--ios", @@ -96,7 +99,8 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-12" + "os=Mac-12", + "cpu=x86" ], "gn": [ "--ios", @@ -120,8 +124,8 @@ { "drone_dimensions": [ "device_type=none", - "mac_model=Macmini8,1", - "os=Mac-12" + "os=Mac-12", + "cpu=x86" ], "gn": [ "--ios", @@ -144,7 +148,8 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-12" + "os=Mac-12", + "cpu=x86" ], "gn": [ "--ios", @@ -168,7 +173,8 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-12" + "os=Mac-12", + "cpu=x86" ], "gn": [ "--ios", @@ -192,7 +198,8 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-12" + "os=Mac-12", + "cpu=x86" ], "gn": [ "--ios", @@ -217,7 +224,8 @@ { "drone_dimensions": [ "device_type=none", - "os=Mac-12" + "os=Mac-12", + "cpu=x86" ], "gn": [ "--ios", diff --git a/ci/licenses_golden/excluded_files b/ci/licenses_golden/excluded_files index feed60b197fdf..412f6d957fbdb 100644 --- a/ci/licenses_golden/excluded_files +++ b/ci/licenses_golden/excluded_files @@ -280,6 +280,7 @@ ../../../flutter/shell/platform/common/text_input_model_unittests.cc ../../../flutter/shell/platform/common/text_range_unittests.cc ../../../flutter/shell/platform/darwin/Doxyfile +../../../flutter/shell/platform/darwin/common/availability_version_check_unittests.cc ../../../flutter/shell/platform/darwin/common/framework/Source/flutter_codecs_unittest.mm ../../../flutter/shell/platform/darwin/common/framework/Source/flutter_standard_codec_unittest.mm ../../../flutter/shell/platform/darwin/macos/README.md diff --git a/ci/licenses_golden/licenses_dart b/ci/licenses_golden/licenses_dart index f463b1c0dbf13..60ffd6d8ca5fb 100644 --- a/ci/licenses_golden/licenses_dart +++ b/ci/licenses_golden/licenses_dart @@ -1,4 +1,4 @@ -Signature: fdaa9367efe1c4a57d9abc0eb91db374 +Signature: ca990fb99a9b23dd546b46d46ece34dd ==================================================================================================== LIBRARY: dart diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 53a9299268fe4..f5fa01c758d78 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2597,6 +2597,7 @@ ORIGIN: ../../../flutter/shell/platform/common/text_input_model.cc + ../../../fl ORIGIN: ../../../flutter/shell/platform/common/text_input_model.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/common/text_range.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/availability_version_check.cc + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/common/availability_version_check.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/buffer_conversions.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/buffer_conversions.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/common/command_line.h + ../../../flutter/LICENSE @@ -2697,6 +2698,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextI ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelay.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelay.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelayTest.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUmbrellaImport.m + ../../../flutter/LICENSE @@ -2718,6 +2720,8 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap_I ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest_mrc.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController_FlutterScreenAndSceneIfLoadedTest.mm + ../../../flutter/LICENSE @@ -2728,6 +2732,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibilit ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection_test.mm + ../../../flutter/LICENSE @@ -5364,6 +5369,7 @@ FILE: ../../../flutter/shell/platform/common/text_input_model.cc FILE: ../../../flutter/shell/platform/common/text_input_model.h FILE: ../../../flutter/shell/platform/common/text_range.h FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.cc +FILE: ../../../flutter/shell/platform/darwin/common/availability_version_check.h FILE: ../../../flutter/shell/platform/darwin/common/buffer_conversions.h FILE: ../../../flutter/shell/platform/darwin/common/buffer_conversions.mm FILE: ../../../flutter/shell/platform/darwin/common/command_line.h @@ -5465,6 +5471,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInp FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelay.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelay.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelayTest.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUIPressProxy.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterUmbrellaImport.m @@ -5486,6 +5493,8 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/KeyCodeMap_Int FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest_mrc.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/UIViewController_FlutterScreenAndSceneIfLoadedTest.mm @@ -5496,6 +5505,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/connection_collection_test.mm diff --git a/ci/licenses_golden/licenses_skia b/ci/licenses_golden/licenses_skia index 64b87cc1a193f..720013e312849 100644 --- a/ci/licenses_golden/licenses_skia +++ b/ci/licenses_golden/licenses_skia @@ -1,4 +1,4 @@ -Signature: eca53560b3fc7923df8e91b89c6a742d +Signature: d1349790807333b45e728aa0781329ea ==================================================================================================== LIBRARY: etc1 diff --git a/flow/layers/layer_tree.h b/flow/layers/layer_tree.h index 5f573da2099a0..686abbace2e0e 100644 --- a/flow/layers/layer_tree.h +++ b/flow/layers/layer_tree.h @@ -70,6 +70,8 @@ class LayerTree { /// When `Paint` is called, if leaf layer tracing is enabled, additional /// metadata around raterization of leaf layers is collected. /// + /// This is not supported in the Impeller backend. + /// /// See: `LayerSnapshotStore` void enable_leaf_layer_tracing(bool enable) { enable_leaf_layer_tracing_ = enable; diff --git a/impeller/aiks/aiks_unittests.cc b/impeller/aiks/aiks_unittests.cc index 4950aae8496fc..42b9fd6ab0122 100644 --- a/impeller/aiks/aiks_unittests.cc +++ b/impeller/aiks/aiks_unittests.cc @@ -1580,6 +1580,26 @@ TEST_P(AiksTest, DrawPaintWithAdvancedBlendOverFilter) { ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } +TEST_P(AiksTest, DrawAdvancedBlendPartlyOffscreen) { + std::vector colors = {Color{0.9568, 0.2627, 0.2118, 1.0}, + Color{0.1294, 0.5882, 0.9529, 1.0}}; + std::vector stops = {0.0, 1.0}; + + Paint paint = { + .color_source = ColorSource::MakeLinearGradient( + {0, 0}, {100, 100}, std::move(colors), std::move(stops), + Entity::TileMode::kRepeat, Matrix::MakeScale(Vector3(0.3, 0.3, 0.3))), + .blend_mode = BlendMode::kLighten, + }; + + Canvas canvas; + canvas.DrawPaint({.color = Color::Blue()}); + canvas.Scale(Vector2(2, 2)); + canvas.ClipRect(Rect::MakeLTRB(0, 0, 200, 200)); + canvas.DrawCircle({100, 100}, 100, paint); + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + #define BLEND_MODE_TUPLE(blend_mode) {#blend_mode, BlendMode::k##blend_mode}, struct BlendModeSelection { @@ -2086,9 +2106,12 @@ TEST_P(AiksTest, CanRenderClippedLayers) { canvas.DrawRect(Rect::MakeSize(Size{400, 400}), {.color = Color::White()}); // Fill the layer with green, but do so with a color blend that can't be // collapsed into the parent pass. + // TODO(jonahwilliams): this blend mode was changed from color burn to + // hardlight to work around https://github.com/flutter/flutter/issues/136554 + // . canvas.DrawRect( Rect::MakeSize(Size{400, 400}), - {.color = Color::Green(), .blend_mode = BlendMode::kColorBurn}); + {.color = Color::Green(), .blend_mode = BlendMode::kHardLight}); } ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); @@ -2393,19 +2416,18 @@ TEST_P(AiksTest, ClearColorOptimizationDoesNotApplyForBackdropFilters) { Picture picture = canvas.EndRecordingAsPicture(); std::optional actual_color; + bool found_subpass = false; picture.pass->IterateAllElements([&](EntityPass::Element& element) -> bool { if (auto subpass = std::get_if>(&element)) { actual_color = subpass->get()->GetClearColor(); + found_subpass = true; } // Fail if the first element isn't a subpass. return true; }); - ASSERT_TRUE(actual_color.has_value()); - if (!actual_color) { - return; - } - ASSERT_EQ(actual_color.value(), Color::BlackTransparent()); + EXPECT_TRUE(found_subpass); + EXPECT_FALSE(actual_color.has_value()); } TEST_P(AiksTest, CollapsedDrawPaintInSubpass) { @@ -3562,7 +3584,85 @@ TEST_P(AiksTest, ClearBlend) { clear.blend_mode = BlendMode::kClear; canvas.DrawCircle(Point::MakeXY(300.0, 300.0), 200.0, clear); +} + +TEST_P(AiksTest, MatrixImageFilterMagnify) { + Canvas canvas; + canvas.Scale(GetContentScale()); + auto image = std::make_shared(CreateTextureForFixture("airplane.jpg")); + canvas.Translate({600, -200}); + canvas.SaveLayer({ + .image_filter = std::make_shared( + Matrix{ + 2, 0, 0, 0, // + 0, 2, 0, 0, // + 0, 0, 2, 0, // + 0, 0, 0, 1 // + }, + SamplerDescriptor{}), + }); + canvas.DrawImage(image, {0, 0}, Paint{.color = Color(1.0, 1.0, 1.0, 0.5)}); + canvas.Restore(); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +// This should be solid red, if you see a little red box this is broken. +TEST_P(AiksTest, ClearColorOptimizationWhenSubpassIsBiggerThanParentPass) { + Canvas canvas; + canvas.Scale(GetContentScale()); + canvas.DrawRect(Rect::MakeLTRB(200, 200, 300, 300), {.color = Color::Red()}); + canvas.SaveLayer({ + .image_filter = std::make_shared( + Matrix::MakeScale({2, 2, 1}), SamplerDescriptor{}), + }); + // Draw a rectangle that would fully cover the parent pass size, but not + // the subpass that it is rendered in. + canvas.DrawRect(Rect::MakeLTRB(0, 0, 400, 400), {.color = Color::Green()}); + // Draw a bigger rectangle to force the subpass to be bigger. + canvas.DrawRect(Rect::MakeLTRB(0, 0, 800, 800), {.color = Color::Red()}); + canvas.Restore(); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, MaskBlurWithZeroSigmaIsSkipped) { + Canvas canvas; + + Paint paint = { + .color = Color::White(), + .mask_blur_descriptor = + Paint::MaskBlurDescriptor{ + .style = FilterContents::BlurStyle::kNormal, + .sigma = Sigma(0), + }, + }; + + canvas.DrawCircle({300, 300}, 200, paint); + canvas.DrawRect(Rect::MakeLTRB(100, 300, 500, 600), paint); + + ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); +} + +TEST_P(AiksTest, SubpassWithClearColorOptimization) { + Canvas canvas; + + // Use a non-srcOver blend mode to ensure that we don't detect this as an + // opacity peephole optimization. + canvas.SaveLayer( + {.color = Color::Blue().WithAlpha(0.5), .blend_mode = BlendMode::kSource}, + Rect::MakeLTRB(0, 0, 200, 200)); + canvas.DrawPaint( + {.color = Color::BlackTransparent(), .blend_mode = BlendMode::kSource}); + canvas.Restore(); + + canvas.SaveLayer( + {.color = Color::Blue(), .blend_mode = BlendMode::kDestinationOver}); + canvas.Restore(); + // This playground should appear blank on CI since we are only drawing + // transparent black. If the clear color optimization is broken, the texture + // will be filled with NaNs and may produce a magenta texture on macOS or iOS. ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture())); } diff --git a/impeller/aiks/canvas.cc b/impeller/aiks/canvas.cc index ae4c9d512037f..4c4a0ab3f263c 100644 --- a/impeller/aiks/canvas.cc +++ b/impeller/aiks/canvas.cc @@ -18,6 +18,7 @@ #include "impeller/entity/contents/texture_contents.h" #include "impeller/entity/contents/vertices_contents.h" #include "impeller/entity/geometry/geometry.h" +#include "impeller/geometry/constants.h" #include "impeller/geometry/path_builder.h" namespace impeller { @@ -205,6 +206,11 @@ bool Canvas::AttemptDrawBlurredRRect(const Rect& rect, return false; } + if (std::fabs(new_paint.mask_blur_descriptor->sigma.sigma) <= + kEhCloseEnough) { + return true; + } + // For symmetrically mask blurred solid RRects, absorb the mask blur and use // a faster SDF approximation. @@ -534,6 +540,14 @@ void Canvas::SaveLayer(const Paint& paint, const std::shared_ptr& backdrop_filter) { Save(true, paint.blend_mode, backdrop_filter); + // The DisplayList bounds/rtree doesn't account for filters applied to parent + // layers, and so sub-DisplayLists are getting culled as if no filters are + // applied. + // See also: https://github.com/flutter/flutter/issues/139294 + if (paint.image_filter) { + xformation_stack_.back().cull_rect = std::nullopt; + } + auto& new_layer_pass = GetCurrentPass(); new_layer_pass.SetBoundsLimit(bounds); diff --git a/impeller/aiks/canvas_unittests.cc b/impeller/aiks/canvas_unittests.cc index ed83f0500a254..728316cb3c61d 100644 --- a/impeller/aiks/canvas_unittests.cc +++ b/impeller/aiks/canvas_unittests.cc @@ -4,6 +4,7 @@ #include "flutter/testing/testing.h" #include "impeller/aiks/canvas.h" +#include "impeller/aiks/image_filter.h" #include "impeller/geometry/path_builder.h" // TODO(zanderso): https://github.com/flutter/flutter/issues/127701 @@ -336,6 +337,23 @@ TEST(AiksCanvasTest, PathClipDiffAgainstFullyCoveredCullRect) { ASSERT_EQ(canvas.GetCurrentLocalCullingBounds().value(), result_cull); } +TEST(AiksCanvasTest, DisableLocalBoundsRectForFilteredSaveLayers) { + Rect initial_cull = Rect::MakeXYWH(0, 0, 10, 10); + + Canvas canvas(initial_cull); + ASSERT_TRUE(canvas.GetCurrentLocalCullingBounds().has_value()); + + canvas.Save(); + canvas.SaveLayer( + Paint{.image_filter = ImageFilter::MakeBlur( + Sigma(10), Sigma(10), FilterContents::BlurStyle::kNormal, + Entity::TileMode::kDecal)}); + ASSERT_FALSE(canvas.GetCurrentLocalCullingBounds().has_value()); + + canvas.Restore(); + ASSERT_TRUE(canvas.GetCurrentLocalCullingBounds().has_value()); +} + } // namespace testing } // namespace impeller diff --git a/impeller/aiks/paint_pass_delegate.cc b/impeller/aiks/paint_pass_delegate.cc index f9a081a0b55cf..604e4dc92831c 100644 --- a/impeller/aiks/paint_pass_delegate.cc +++ b/impeller/aiks/paint_pass_delegate.cc @@ -10,7 +10,6 @@ #include "impeller/entity/contents/texture_contents.h" #include "impeller/entity/entity_pass.h" #include "impeller/geometry/color.h" -#include "impeller/geometry/path_builder.h" namespace impeller { diff --git a/impeller/entity/contents/clip_contents.cc b/impeller/entity/contents/clip_contents.cc index 21c2eb1aa2469..77bbccc82f0c7 100644 --- a/impeller/entity/contents/clip_contents.cc +++ b/impeller/entity/contents/clip_contents.cc @@ -66,7 +66,7 @@ Contents::StencilCoverage ClipContents::GetStencilCoverage( bool ClipContents::ShouldRender( const Entity& entity, - const std::optional& stencil_coverage) const { + const std::optional stencil_coverage) const { return true; } @@ -163,7 +163,7 @@ Contents::StencilCoverage ClipRestoreContents::GetStencilCoverage( bool ClipRestoreContents::ShouldRender( const Entity& entity, - const std::optional& stencil_coverage) const { + const std::optional stencil_coverage) const { return true; } diff --git a/impeller/entity/contents/clip_contents.h b/impeller/entity/contents/clip_contents.h index 3b0faac98bc60..bc7b89ac055a5 100644 --- a/impeller/entity/contents/clip_contents.h +++ b/impeller/entity/contents/clip_contents.h @@ -35,7 +35,7 @@ class ClipContents final : public Contents { // |Contents| bool ShouldRender(const Entity& entity, - const std::optional& stencil_coverage) const override; + const std::optional stencil_coverage) const override; // |Contents| bool Render(const ContentContext& renderer, @@ -76,7 +76,7 @@ class ClipRestoreContents final : public Contents { // |Contents| bool ShouldRender(const Entity& entity, - const std::optional& stencil_coverage) const override; + const std::optional stencil_coverage) const override; // |Contents| bool Render(const ContentContext& renderer, diff --git a/impeller/entity/contents/contents.cc b/impeller/entity/contents/contents.cc index a181e22302b21..4671d23b4c0d5 100644 --- a/impeller/entity/contents/contents.cc +++ b/impeller/entity/contents/contents.cc @@ -133,11 +133,10 @@ bool Contents::ApplyColorFilter( } bool Contents::ShouldRender(const Entity& entity, - const std::optional& stencil_coverage) const { + const std::optional stencil_coverage) const { if (!stencil_coverage.has_value()) { return false; } - auto coverage = GetCoverage(entity); if (!coverage.has_value()) { return false; diff --git a/impeller/entity/contents/contents.h b/impeller/entity/contents/contents.h index b9dec5db2d12e..11b5ad00570de 100644 --- a/impeller/entity/contents/contents.h +++ b/impeller/entity/contents/contents.h @@ -113,7 +113,7 @@ class Contents { const std::string& label = "Snapshot") const; virtual bool ShouldRender(const Entity& entity, - const std::optional& stencil_coverage) const; + const std::optional stencil_coverage) const; //---------------------------------------------------------------------------- /// @brief Return the color source's intrinsic size, if available. diff --git a/impeller/entity/contents/framebuffer_blend_contents.cc b/impeller/entity/contents/framebuffer_blend_contents.cc index 3ced8b52d9d76..c571c0a349a24 100644 --- a/impeller/entity/contents/framebuffer_blend_contents.cc +++ b/impeller/entity/contents/framebuffer_blend_contents.cc @@ -48,6 +48,7 @@ bool FramebufferBlendContents::Render(const ContentContext& renderer, std::nullopt, // sampler_descriptor true, // msaa_enabled "FramebufferBlendContents Snapshot"); // label + if (!src_snapshot.has_value()) { return true; } @@ -56,21 +57,16 @@ bool FramebufferBlendContents::Render(const ContentContext& renderer, return true; } Rect src_coverage = coverage.value(); - auto maybe_src_uvs = src_snapshot->GetCoverageUVs(src_coverage); - if (!maybe_src_uvs.has_value()) { - return true; - } - std::array src_uvs = maybe_src_uvs.value(); auto size = src_coverage.size; VertexBufferBuilder vtx_builder; vtx_builder.AddVertices({ - {Point(0, 0), src_uvs[0]}, - {Point(size.width, 0), src_uvs[1]}, - {Point(size.width, size.height), src_uvs[3]}, - {Point(0, 0), src_uvs[0]}, - {Point(size.width, size.height), src_uvs[3]}, - {Point(0, size.height), src_uvs[2]}, + {Point(0, 0), Point(0, 0)}, + {Point(size.width, 0), Point(1, 0)}, + {Point(size.width, size.height), Point(1, 1)}, + {Point(0, 0), Point(0, 0)}, + {Point(size.width, size.height), Point(1, 1)}, + {Point(0, size.height), Point(0, 1)}, }); auto vtx_buffer = vtx_builder.CreateVertexBuffer(host_buffer); @@ -147,8 +143,7 @@ bool FramebufferBlendContents::Render(const ContentContext& renderer, FS::BindTextureSamplerSrc(cmd, src_snapshot->texture, src_sampler); frame_info.mvp = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) * - entity.GetTransformation() * - Matrix::MakeTranslation(src_coverage.origin); + src_snapshot->transform; frame_info.src_y_coord_scale = src_snapshot->texture->GetYCoordScale(); VS::BindFrameInfo(cmd, host_buffer.EmplaceUniform(frame_info)); diff --git a/impeller/entity/contents/solid_rrect_blur_contents.cc b/impeller/entity/contents/solid_rrect_blur_contents.cc index ffb190cf7a271..e0078f7093e43 100644 --- a/impeller/entity/contents/solid_rrect_blur_contents.cc +++ b/impeller/entity/contents/solid_rrect_blur_contents.cc @@ -8,6 +8,7 @@ #include "impeller/entity/contents/content_context.h" #include "impeller/entity/entity.h" #include "impeller/geometry/color.h" +#include "impeller/geometry/constants.h" #include "impeller/geometry/path.h" #include "impeller/geometry/path_builder.h" #include "impeller/renderer/render_pass.h" @@ -55,7 +56,8 @@ std::optional SolidRRectBlurContents::GetCoverage( bool SolidRRectBlurContents::Render(const ContentContext& renderer, const Entity& entity, RenderPass& pass) const { - if (!rect_.has_value()) { + // Early return if sigma is close to zero to avoid rendering NaNs. + if (!rect_.has_value() || std::fabs(sigma_.sigma) <= kEhCloseEnough) { return true; } diff --git a/impeller/entity/entity.cc b/impeller/entity/entity.cc index b7ebc2af966c9..ae18016932911 100644 --- a/impeller/entity/entity.cc +++ b/impeller/entity/entity.cc @@ -71,7 +71,11 @@ Contents::StencilCoverage Entity::GetStencilCoverage( } bool Entity::ShouldRender(const std::optional& stencil_coverage) const { +#ifdef IMPELLER_CONTENT_CULLING return contents_->ShouldRender(*this, stencil_coverage); +#else + return true; +#endif // IMPELLER_CONTENT_CULLING } void Entity::SetContents(std::shared_ptr contents) { diff --git a/impeller/entity/entity_pass.cc b/impeller/entity/entity_pass.cc index 0d91112369ae7..495856a338bb5 100644 --- a/impeller/entity/entity_pass.cc +++ b/impeller/entity/entity_pass.cc @@ -202,8 +202,8 @@ std::optional EntityPass::GetSubpassCoverage( // has deviated too much from the parent pass to safely intersect with the // pass coverage limit. coverage_limit = - (image_filter && image_filter->IsTranslationOnly() ? std::nullopt - : coverage_limit); + (image_filter && !image_filter->IsTranslationOnly() ? std::nullopt + : coverage_limit); auto entities_coverage = subpass.GetElementsCoverage(coverage_limit); // The entities don't cover anything. There is nothing to do. @@ -368,7 +368,7 @@ bool EntityPass::Render(ContentContext& renderer, if (!supports_onscreen_backdrop_reads && reads_from_onscreen_backdrop) { auto offscreen_target = CreateRenderTarget( renderer, root_render_target.GetRenderTargetSize(), true, - GetClearColor(render_target.GetRenderTargetSize())); + GetClearColorOrDefault(render_target.GetRenderTargetSize())); if (!OnRender(renderer, // renderer capture, // capture @@ -475,7 +475,8 @@ bool EntityPass::Render(ContentContext& renderer, } // Set up the clear color of the root pass. - color0.clear_color = GetClearColor(render_target.GetRenderTargetSize()); + color0.clear_color = + GetClearColorOrDefault(render_target.GetRenderTargetSize()); root_render_target.SetColorAttachment(color0, 0); EntityPassTarget pass_target( @@ -628,10 +629,10 @@ EntityPass::EntityResult EntityPass::GetEntityForElement( } auto subpass_target = CreateRenderTarget( - renderer, // renderer - subpass_size, // size - subpass->GetTotalPassReads(renderer) > 0, // readable - subpass->GetClearColor(subpass_size)); // clear_color + renderer, // renderer + subpass_size, // size + subpass->GetTotalPassReads(renderer) > 0, // readable + subpass->GetClearColorOrDefault(subpass_size)); // clear_color if (!subpass_target.IsValid()) { VALIDATION_LOG << "Subpass render target is invalid."; @@ -720,9 +721,9 @@ bool EntityPass::OnRender( VALIDATION_LOG << SPrintF("Pass context invalid (Depth=%d)", pass_depth); return false; } + auto clear_color_size = pass_target.GetRenderTarget().GetRenderTargetSize(); - if (!collapsed_parent_pass && - !GetClearColor(root_pass_size).IsTransparent()) { + if (!collapsed_parent_pass && GetClearColor(clear_color_size).has_value()) { // Force the pass context to create at least one new pass if the clear color // is present. pass_context.GetRenderPass(pass_depth); @@ -892,7 +893,7 @@ bool EntityPass::OnRender( // Skip elements that are incorporated into the clear color. if (is_collapsing_clear_colors) { auto [entity_color, _] = - ElementAsBackgroundColor(element, root_pass_size); + ElementAsBackgroundColor(element, clear_color_size); if (entity_color.has_value()) { continue; } @@ -1139,21 +1140,29 @@ void EntityPass::SetBlendMode(BlendMode blend_mode) { flood_clip_ = Entity::IsBlendModeDestructive(blend_mode); } -Color EntityPass::GetClearColor(ISize target_size) const { - Color result = Color::BlackTransparent(); +Color EntityPass::GetClearColorOrDefault(ISize size) const { + return GetClearColor(size).value_or(Color::BlackTransparent()); +} + +std::optional EntityPass::GetClearColor(ISize target_size) const { if (backdrop_filter_proc_) { - return result; + return std::nullopt; } + std::optional result = std::nullopt; for (const Element& element : elements_) { auto [entity_color, blend_mode] = ElementAsBackgroundColor(element, target_size); if (!entity_color.has_value()) { break; } - result = result.Blend(entity_color.value(), blend_mode); + result = result.value_or(Color::BlackTransparent()) + .Blend(entity_color.value(), blend_mode); } - return result.Premultiply(); + if (result.has_value()) { + return result->Premultiply(); + } + return result; } void EntityPass::SetBackdropFilter(BackdropFilterProc proc) { diff --git a/impeller/entity/entity_pass.h b/impeller/entity/entity_pass.h index d09649abfd9d0..c03a837047eeb 100644 --- a/impeller/entity/entity_pass.h +++ b/impeller/entity/entity_pass.h @@ -135,7 +135,13 @@ class EntityPass { void SetBlendMode(BlendMode blend_mode); - Color GetClearColor(ISize size = ISize::Infinite()) const; + /// @brief Return the premultiplied clear color of the pass entities, if any. + std::optional GetClearColor(ISize size = ISize::Infinite()) const; + + /// @brief Return the premultiplied clear color of the pass entities. + /// + /// If the entity pass has no clear color, this will return transparent black. + Color GetClearColorOrDefault(ISize size = ISize::Infinite()) const; void SetBackdropFilter(BackdropFilterProc proc); diff --git a/impeller/entity/entity_unittests.cc b/impeller/entity/entity_unittests.cc index c76ece221dbd8..2b28fa3ede2e7 100644 --- a/impeller/entity/entity_unittests.cc +++ b/impeller/entity/entity_unittests.cc @@ -1607,6 +1607,20 @@ TEST_P(EntityTest, SolidFillShouldRenderIsCorrect) { } } +TEST_P(EntityTest, DoesNotCullEntitiesByDefault) { + auto fill = std::make_shared(); + fill->SetColor(Color::CornflowerBlue()); + fill->SetGeometry( + Geometry::MakeRect(Rect::MakeLTRB(-1000, -1000, -900, -900))); + + Entity entity; + entity.SetContents(fill); + + // Even though the entity is offscreen, this should still render because we do + // not compute the coverage intersection by default. + EXPECT_TRUE(entity.ShouldRender(Rect::MakeLTRB(0, 0, 100, 100))); +} + TEST_P(EntityTest, ClipContentsShouldRenderIsCorrect) { // For clip ops, `ShouldRender` should always return true. @@ -2417,6 +2431,14 @@ TEST_P(EntityTest, PointFieldGeometryDivisions) { ASSERT_EQ(PointFieldGeometry::ComputeCircleDivisions(20000.0, true), 140u); } +TEST_P(EntityTest, PointFieldGeometryCoverage) { + std::vector points = {{10, 20}, {100, 200}}; + auto geometry = Geometry::MakePointField(points, 5.0, false); + ASSERT_EQ(*geometry->GetCoverage(Matrix()), Rect::MakeLTRB(5, 15, 105, 205)); + ASSERT_EQ(*geometry->GetCoverage(Matrix::MakeTranslation({30, 0, 0})), + Rect::MakeLTRB(35, 15, 135, 205)); +} + TEST_P(EntityTest, ColorFilterContentsWithLargeGeometry) { Entity entity; entity.SetTransformation(Matrix::MakeScale(GetContentScale())); diff --git a/impeller/entity/geometry/point_field_geometry.cc b/impeller/entity/geometry/point_field_geometry.cc index 81fe3885d916b..9821bad91ad7b 100644 --- a/impeller/entity/geometry/point_field_geometry.cc +++ b/impeller/entity/geometry/point_field_geometry.cc @@ -287,8 +287,9 @@ std::optional PointFieldGeometry::GetCoverage( right = std::max(right, it->x); bottom = std::max(bottom, it->y); } - return Rect::MakeLTRB(left - radius_, top - radius_, right + radius_, - bottom + radius_); + auto coverage = Rect::MakeLTRB(left - radius_, top - radius_, + right + radius_, bottom + radius_); + return coverage.TransformBounds(transform); } return std::nullopt; } diff --git a/impeller/tessellator/tessellator.cc b/impeller/tessellator/tessellator.cc index f7c80fe747ea0..c8cfdbeff819f 100644 --- a/impeller/tessellator/tessellator.cc +++ b/impeller/tessellator/tessellator.cc @@ -78,140 +78,81 @@ Tessellator::Result Tessellator::Tessellate( constexpr int kVertexSize = 2; constexpr int kPolygonSize = 3; - // If we have a larger polyline and the fill type is non-zero, we can split - // the tessellation up per contour. Since in general the complexity is at - // least nlog(n), this speeds up the processes substantially. - if (polyline.contours.size() > kMultiContourThreshold && - fill_type == FillType::kNonZero) { - std::vector points; - std::vector data; + //---------------------------------------------------------------------------- + /// Feed contour information to the tessellator. + /// + static_assert(sizeof(Point) == 2 * sizeof(float)); + for (size_t contour_i = 0; contour_i < polyline.contours.size(); + contour_i++) { + size_t start_point_index, end_point_index; + std::tie(start_point_index, end_point_index) = + polyline.GetContourPointBounds(contour_i); + + ::tessAddContour(tessellator, // the C tessellator + kVertexSize, // + polyline.points.data() + start_point_index, // + sizeof(Point), // + end_point_index - start_point_index // + ); + } - //---------------------------------------------------------------------------- - /// Feed contour information to the tessellator. - /// - size_t total = 0u; - static_assert(sizeof(Point) == 2 * sizeof(float)); - for (size_t contour_i = 0; contour_i < polyline.contours.size(); - contour_i++) { - size_t start_point_index, end_point_index; - std::tie(start_point_index, end_point_index) = - polyline.GetContourPointBounds(contour_i); - - ::tessAddContour(tessellator, // the C tessellator - kVertexSize, // - polyline.points.data() + start_point_index, // - sizeof(Point), // - end_point_index - start_point_index // - ); - - //---------------------------------------------------------------------------- - /// Let's tessellate. - /// - auto result = ::tessTesselate(tessellator, // tessellator - ToTessWindingRule(fill_type), // winding - TESS_POLYGONS, // element type - kPolygonSize, // polygon size - kVertexSize, // vertex size - nullptr // normal (null is automatic) - ); - - if (result != 1) { - return Result::kTessellationError; - } - - int vertex_item_count = tessGetVertexCount(tessellator) * kVertexSize; - auto vertices = tessGetVertices(tessellator); - for (int i = 0; i < vertex_item_count; i += 2) { - points.emplace_back(vertices[i], vertices[i + 1]); - } - - int element_item_count = tessGetElementCount(tessellator) * kPolygonSize; - auto elements = tessGetElements(tessellator); - total += element_item_count; - for (int i = 0; i < element_item_count; i++) { - data.emplace_back(points[elements[i]].x); - data.emplace_back(points[elements[i]].y); - } - points.clear(); + //---------------------------------------------------------------------------- + /// Let's tessellate. + /// + auto result = ::tessTesselate(tessellator, // tessellator + ToTessWindingRule(fill_type), // winding + TESS_POLYGONS, // element type + kPolygonSize, // polygon size + kVertexSize, // vertex size + nullptr // normal (null is automatic) + ); + + if (result != 1) { + return Result::kTessellationError; + } + + int element_item_count = tessGetElementCount(tessellator) * kPolygonSize; + + // We default to using a 16bit index buffer, but in cases where we generate + // more tessellated data than this can contain we need to fall back to + // dropping the index buffer entirely. Instead code could instead switch to + // a uint32 index buffer, but this is done for simplicity with the other + // fast path above. + if (element_item_count < USHRT_MAX) { + int vertex_item_count = tessGetVertexCount(tessellator); + auto vertices = tessGetVertices(tessellator); + auto elements = tessGetElements(tessellator); + + // libtess uses an int index internally due to usage of -1 as a sentinel + // value. + std::vector indices(element_item_count); + for (int i = 0; i < element_item_count; i++) { + indices[i] = static_cast(elements[i]); } - if (!callback(data.data(), total, nullptr, 0u)) { + if (!callback(vertices, vertex_item_count, indices.data(), + element_item_count)) { return Result::kInputError; } } else { - //---------------------------------------------------------------------------- - /// Feed contour information to the tessellator. - /// - static_assert(sizeof(Point) == 2 * sizeof(float)); - for (size_t contour_i = 0; contour_i < polyline.contours.size(); - contour_i++) { - size_t start_point_index, end_point_index; - std::tie(start_point_index, end_point_index) = - polyline.GetContourPointBounds(contour_i); - - ::tessAddContour(tessellator, // the C tessellator - kVertexSize, // - polyline.points.data() + start_point_index, // - sizeof(Point), // - end_point_index - start_point_index // - ); - } - - //---------------------------------------------------------------------------- - /// Let's tessellate. - /// - auto result = ::tessTesselate(tessellator, // tessellator - ToTessWindingRule(fill_type), // winding - TESS_POLYGONS, // element type - kPolygonSize, // polygon size - kVertexSize, // vertex size - nullptr // normal (null is automatic) - ); + std::vector points; + std::vector data; - if (result != 1) { - return Result::kTessellationError; + int vertex_item_count = tessGetVertexCount(tessellator) * kVertexSize; + auto vertices = tessGetVertices(tessellator); + points.reserve(vertex_item_count); + for (int i = 0; i < vertex_item_count; i += 2) { + points.emplace_back(vertices[i], vertices[i + 1]); } int element_item_count = tessGetElementCount(tessellator) * kPolygonSize; - - // We default to using a 16bit index buffer, but in cases where we generate - // more tessellated data than this can contain we need to fall back to - // dropping the index buffer entirely. Instead code could instead switch to - // a uint32 index buffer, but this is done for simplicity with the other - // fast path above. - if (element_item_count < USHRT_MAX) { - int vertex_item_count = tessGetVertexCount(tessellator); - auto vertices = tessGetVertices(tessellator); - auto elements = tessGetElements(tessellator); - - // libtess uses an int index internally due to usage of -1 as a sentinel - // value. - std::vector indices(element_item_count); - for (int i = 0; i < element_item_count; i++) { - indices[i] = static_cast(elements[i]); - } - if (!callback(vertices, vertex_item_count, indices.data(), - element_item_count)) { - return Result::kInputError; - } - } else { - std::vector points; - std::vector data; - - int vertex_item_count = tessGetVertexCount(tessellator) * kVertexSize; - auto vertices = tessGetVertices(tessellator); - for (int i = 0; i < vertex_item_count; i += 2) { - points.emplace_back(vertices[i], vertices[i + 1]); - } - - int element_item_count = tessGetElementCount(tessellator) * kPolygonSize; - auto elements = tessGetElements(tessellator); - for (int i = 0; i < element_item_count; i++) { - data.emplace_back(points[elements[i]].x); - data.emplace_back(points[elements[i]].y); - } - if (!callback(data.data(), element_item_count, nullptr, 0u)) { - return Result::kInputError; - } + auto elements = tessGetElements(tessellator); + data.reserve(element_item_count); + for (int i = 0; i < element_item_count; i++) { + data.emplace_back(points[elements[i]].x); + data.emplace_back(points[elements[i]].y); + } + if (!callback(data.data(), element_item_count, nullptr, 0u)) { + return Result::kInputError; } } diff --git a/impeller/tessellator/tessellator.h b/impeller/tessellator/tessellator.h index b6d444c5a81ba..7cd8690da9fb4 100644 --- a/impeller/tessellator/tessellator.h +++ b/impeller/tessellator/tessellator.h @@ -44,10 +44,6 @@ class Tessellator { ~Tessellator(); - /// @brief An arbitrary value to determine when a multi-contour non-zero fill - /// path should be split into multiple tessellations. - static constexpr size_t kMultiContourThreshold = 30u; - /// @brief A callback that returns the results of the tessellation. /// /// The index buffer may not be populated, in which case [indices] will diff --git a/impeller/tessellator/tessellator_unittests.cc b/impeller/tessellator/tessellator_unittests.cc index 8fca675a9a9a6..f3af64c7ff4cb 100644 --- a/impeller/tessellator/tessellator_unittests.cc +++ b/impeller/tessellator/tessellator_unittests.cc @@ -88,27 +88,6 @@ TEST(TessellatorTest, TessellatorBuilderReturnsCorrectResultStatus) { ASSERT_EQ(result, Tessellator::Result::kInputError); } - // More than 30 contours, non-zero fill mode. - { - Tessellator t; - PathBuilder builder = {}; - for (auto i = 0u; i < Tessellator::kMultiContourThreshold + 1; i++) { - builder.AddCircle(Point(i, i), 4); - } - auto polyline = builder.TakePath().CreatePolyline(1.0f); - bool no_indices = false; - Tessellator::Result result = t.Tessellate( - FillType::kNonZero, polyline, - [&no_indices](const float* vertices, size_t vertices_count, - const uint16_t* indices, size_t indices_count) { - no_indices = indices == nullptr; - return true; - }); - - ASSERT_TRUE(no_indices); - ASSERT_EQ(result, Tessellator::Result::kSuccess); - } - // More than uint16 points, odd fill mode. { Tessellator t; diff --git a/shell/common/shell.cc b/shell/common/shell.cc index 82db9dea4ff64..5164d1a1db176 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -1968,6 +1968,13 @@ bool Shell::OnServiceProtocolRenderFrameWithRasterStats( rapidjson::Document* response) { FML_DCHECK(task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread()); + // Impeller does not support this protocol method. + if (io_manager_->GetImpellerContext()) { + const char* error = "Raster status not supported on Impeller backend."; + ServiceProtocolFailureError(response, error); + return false; + } + // TODO(dkwingsmt): This method only handles view #0, including the snapshot // and the frame size. We need to adapt this method to multi-view. // https://github.com/flutter/flutter/issues/131892 diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index 2371fa9113aa7..11d911d2bc356 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -2713,6 +2713,47 @@ TEST_F(ShellTest, OnServiceProtocolRenderFrameWithRasterStatsWorks) { DestroyShell(std::move(shell)); } +#if defined(FML_OS_MACOSX) +TEST_F(ShellTest, OnServiceProtocolRenderFrameWithRasterStatsDisableImpeller) { + auto settings = CreateSettingsForFixture(); + settings.enable_impeller = true; + std::unique_ptr shell = CreateShell({ + .settings = settings, + .platform_view_create_callback = ShellTestPlatformViewBuilder({ + .rendering_backend = + ShellTestPlatformView::BackendType::kMetalBackend, + }), + }); + + // Create the surface needed by rasterizer + PlatformViewNotifyCreated(shell.get()); + + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("scene_with_red_box"); + + RunEngine(shell.get(), std::move(configuration)); + PumpOneFrame(shell.get()); + + ServiceProtocol::Handler::ServiceProtocolMap empty_params; + rapidjson::Document document; + OnServiceProtocol( + shell.get(), ServiceProtocolEnum::kRenderFrameWithRasterStats, + shell->GetTaskRunners().GetRasterTaskRunner(), empty_params, &document); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + document.Accept(writer); + std::string actual_json = buffer.GetString(); + std::string expected_json = + "{\"code\":-32000,\"message\":\"Raster status not supported on Impeller " + "backend.\"}"; + + ASSERT_EQ(actual_json, expected_json); + + PlatformViewNotifyDestroyed(shell.get()); + DestroyShell(std::move(shell)); +} +#endif // FML_OS_MACOSX + // TODO(https://github.com/flutter/flutter/issues/100273): Disabled due to // flakiness. // TODO(https://github.com/flutter/flutter/issues/100299): Fix it when diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 06e4c9de02985..d9f2774afb903 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -507,13 +507,9 @@ action("android_jar") { "--native_lib", rebase_path("$validation_layer_out_dir/libVkLayer_khronos_validation.so"), ] - if (current_cpu == "arm64") { - args += [ - "--native_lib", - rebase_path("$android_libcpp_root/libs/arm64-v8a/libc++_shared.so", - root_build_dir), - ] - } else { + if (current_cpu != "arm64") { + # This may not be necessarily required anymore. It was kept to maintain + # old behavior. assert(false, "Validation layers not supported for arch.") } } diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 0e7ad70aee29d..b5559a3a9e23a 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -147,7 +147,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega // Whether software rendering is used. private boolean usesSoftwareRendering = false; - private static boolean enableHardwareBufferRenderingTarget = true; + private static boolean enableHardwareBufferRenderingTarget = false; private final PlatformViewsChannel.PlatformViewsHandler channelHandler = new PlatformViewsChannel.PlatformViewsHandler() { diff --git a/shell/platform/darwin/common/BUILD.gn b/shell/platform/darwin/common/BUILD.gn index 0d29be6c703eb..786deba37e03f 100644 --- a/shell/platform/darwin/common/BUILD.gn +++ b/shell/platform/darwin/common/BUILD.gn @@ -50,6 +50,25 @@ source_set("availability_version_check") { public_configs = [ "//flutter:config" ] } +test_fixtures("availability_version_check_fixtures") { + fixtures = [] +} + +executable("availability_version_check_unittests") { + testonly = true + + sources = [ "availability_version_check_unittests.cc" ] + + deps = [ + ":availability_version_check", + ":availability_version_check_fixtures", + "//flutter/fml", + "//flutter/testing", + ] + + public_configs = [ "//flutter:config" ] +} + # Shared framework headers end up in the same folder as platform-specific # framework headers when consumed by clients, so the include paths assume they # are next to each other. diff --git a/shell/platform/darwin/common/availability_version_check.cc b/shell/platform/darwin/common/availability_version_check.cc index 67514cbf5561f..1564bec4335f0 100644 --- a/shell/platform/darwin/common/availability_version_check.cc +++ b/shell/platform/darwin/common/availability_version_check.cc @@ -2,20 +2,141 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "flutter/shell/platform/darwin/common/availability_version_check.h" + +#include +#include +#include + +#include #include #include -#include +#include "flutter/fml/build_config.h" +#include "flutter/fml/file.h" #include "flutter/fml/logging.h" +#include "flutter/fml/mapping.h" +#include "flutter/fml/platform/darwin/cf_utils.h" + +// The implementation of _availability_version_check defined in this file is +// based on the code in the clang-rt library at: +// +// https://github.com/llvm/llvm-project/blob/e315bf25a843582de39257e1345408a10dc08224/compiler-rt/lib/builtins/os_version_check.c +// +// Flutter provides its own implementation due to an issue introduced in recent +// versions of Clang following Clang 18 in which the clang-rt library declares +// weak linkage against the _availability_version_check symbol. This declaration +// causes apps to be rejected from the App Store. When Flutter statically links +// the implementation below, the weak linkage is satisfied at Engine build time, +// the symbol is no longer exposed from the Engine dylib, and apps will then +// not be rejected from the App Store. +// +// The implementation of _availability_version_check can delegate to the +// dynamically looked-up symbol on recent iOS versions, but the lookup will fail +// on iOS 11 and 12. When the lookup fails, the current OS version must be +// retrieved from a plist file at a well-known path. The logic for this below is +// copied from the clang-rt implementation and adapted for the Engine. -// See context in https://github.com/flutter/flutter/issues/132130 and +// See more context in https://github.com/flutter/flutter/issues/132130 and // https://github.com/flutter/engine/pull/44711. // TODO(zanderso): Remove this after Clang 18 rolls into Xcode. -// https://github.com/flutter/flutter/issues/133203 +// https://github.com/flutter/flutter/issues/133203. + +#define CF_PROPERTY_LIST_IMMUTABLE 0 + +namespace flutter { + +// This function parses the platform's version information out of a plist file +// at a well-known path. It parses the plist file using CoreFoundation functions +// to match the implementation in the clang-rt library. +std::optional ProductVersionFromSystemVersionPList() { + std::string plist_path = "/System/Library/CoreServices/SystemVersion.plist"; +#if FML_OS_IOS_SIMULATOR + char* plist_path_prefix = getenv("IPHONE_SIMULATOR_ROOT"); + if (!plist_path_prefix) { + FML_DLOG(ERROR) << "Failed to getenv IPHONE_SIMULATOR_ROOT"; + return std::nullopt; + } + plist_path = std::string(plist_path_prefix) + plist_path; +#endif // FML_OS_IOS_SIMULATOR + + auto plist_mapping = fml::FileMapping::CreateReadOnly(plist_path); + + // Get the file buffer into CF's format. We pass in a null allocator here * + // because we free PListBuf ourselves + auto file_contents = fml::CFRef(CFDataCreateWithBytesNoCopy( + nullptr, plist_mapping->GetMapping(), + static_cast(plist_mapping->GetSize()), kCFAllocatorNull)); + if (!file_contents) { + FML_DLOG(ERROR) << "Failed to CFDataCreateWithBytesNoCopyFunc"; + return std::nullopt; + } + + auto plist = fml::CFRef( + reinterpret_cast(CFPropertyListCreateWithData( + nullptr, file_contents, CF_PROPERTY_LIST_IMMUTABLE, nullptr, + nullptr))); + if (!plist) { + FML_DLOG(ERROR) << "Failed to CFPropertyListCreateWithDataFunc or " + "CFPropertyListCreateFromXMLDataFunc"; + return std::nullopt; + } + + auto product_version = + fml::CFRef(CFStringCreateWithCStringNoCopy( + nullptr, "ProductVersion", kCFStringEncodingASCII, kCFAllocatorNull)); + if (!product_version) { + FML_DLOG(ERROR) << "Failed to CFStringCreateWithCStringNoCopyFunc"; + return std::nullopt; + } + CFTypeRef opaque_value = CFDictionaryGetValue(plist, product_version); + if (!opaque_value || CFGetTypeID(opaque_value) != CFStringGetTypeID()) { + FML_DLOG(ERROR) << "Failed to CFDictionaryGetValueFunc"; + return std::nullopt; + } + + char version_str[32]; + if (!CFStringGetCString(reinterpret_cast(opaque_value), + version_str, sizeof(version_str), + kCFStringEncodingUTF8)) { + FML_DLOG(ERROR) << "Failed to CFStringGetCStringFunc"; + return std::nullopt; + } + + int32_t major = 0; + int32_t minor = 0; + int32_t subminor = 0; + int matches = sscanf(version_str, "%d.%d.%d", &major, &minor, &subminor); + // A major version number is sufficient. The minor and subminor numbers might + // not be present. + if (matches < 1) { + FML_DLOG(ERROR) << "Failed to match product version string: " + << version_str; + return std::nullopt; + } + + return ProductVersion{major, minor, subminor}; +} + +bool IsEncodedVersionLessThanOrSame(uint32_t encoded_lhs, ProductVersion rhs) { + // Parse the values out of encoded_lhs, then compare against rhs. + const int32_t major = (encoded_lhs >> 16) & 0xffff; + const int32_t minor = (encoded_lhs >> 8) & 0xff; + const int32_t subminor = encoded_lhs & 0xff; + auto lhs = ProductVersion{major, minor, subminor}; + + return lhs <= rhs; +} + +} // namespace flutter namespace { +// The host's OS version when the dynamic lookup of _availability_version_check +// has failed. +static flutter::ProductVersion g_version; + typedef uint32_t dyld_platform_t; typedef struct { @@ -36,13 +157,41 @@ void InitializeAvailabilityCheck(void* unused) { } AvailabilityVersionCheck = reinterpret_cast( dlsym(RTLD_DEFAULT, "_availability_version_check")); - FML_CHECK(AvailabilityVersionCheck); + if (AvailabilityVersionCheck) { + return; + } + + // If _availability_version_check can't be dynamically loaded, then version + // information must be parsed out of a system plist file. + auto product_version = flutter::ProductVersionFromSystemVersionPList(); + if (product_version.has_value()) { + g_version = product_version.value(); + } else { + // If reading version info out of the system plist file fails, then + // fall back to the minimum version that Flutter supports. +#if FML_OS_IOS || FML_OS_IOS_SIMULATOR + g_version = std::make_tuple(11, 0, 0); +#elif FML_OS_MACOSX + g_version = std::make_tuple(10, 14, 0); +#endif // FML_OS_MACOSX + } } extern "C" bool _availability_version_check(uint32_t count, dyld_build_version_t versions[]) { dispatch_once_f(&DispatchOnceCounter, NULL, InitializeAvailabilityCheck); - return AvailabilityVersionCheck(count, versions); + if (AvailabilityVersionCheck) { + return AvailabilityVersionCheck(count, versions); + } + + if (count == 0) { + return true; + } + + // This function is called in only one place in the clang-rt implementation + // where there is only one element in the array. + return flutter::IsEncodedVersionLessThanOrSame(versions[0].version, + g_version); } } // namespace diff --git a/shell/platform/darwin/common/availability_version_check.h b/shell/platform/darwin/common/availability_version_check.h new file mode 100644 index 0000000000000..8724a72d1990a --- /dev/null +++ b/shell/platform/darwin/common/availability_version_check.h @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +namespace flutter { + +using ProductVersion = + std::tuple; + +std::optional ProductVersionFromSystemVersionPList(); + +bool IsEncodedVersionLessThanOrSame(uint32_t encoded_lhs, ProductVersion rhs); + +} // namespace flutter diff --git a/shell/platform/darwin/common/availability_version_check_unittests.cc b/shell/platform/darwin/common/availability_version_check_unittests.cc new file mode 100644 index 0000000000000..7753d1cfa522d --- /dev/null +++ b/shell/platform/darwin/common/availability_version_check_unittests.cc @@ -0,0 +1,35 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "flutter/shell/platform/darwin/common/availability_version_check.h" + +#include "gtest/gtest.h" + +TEST(AvailabilityVersionCheck, CanDecodeSystemPlist) { + auto maybe_product_version = flutter::ProductVersionFromSystemVersionPList(); + ASSERT_TRUE(maybe_product_version.has_value()); + if (maybe_product_version.has_value()) { + auto product_version = maybe_product_version.value(); + ASSERT_GT(product_version, std::make_tuple(0, 0, 0)); + } +} + +static inline uint32_t ConstructVersion(uint32_t major, + uint32_t minor, + uint32_t subminor) { + return ((major & 0xffff) << 16) | ((minor & 0xff) << 8) | (subminor & 0xff); +} + +TEST(AvailabilityVersionCheck, CanParseAndCompareVersions) { + auto rhs_version = std::make_tuple(17, 2, 0); + uint32_t encoded_lower_version = ConstructVersion(12, 3, 7); + ASSERT_TRUE(flutter::IsEncodedVersionLessThanOrSame(encoded_lower_version, + rhs_version)); + + uint32_t encoded_higher_version = ConstructVersion(42, 0, 1); + ASSERT_FALSE(flutter::IsEncodedVersionLessThanOrSame(encoded_higher_version, + rhs_version)); +} diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index fe180c2af422f..5a65050fe0e11 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -238,8 +238,11 @@ source_set("ios_test_flutter_mrc") { "framework/Source/FlutterEngineTest_mrc.mm", "framework/Source/FlutterPlatformPluginTest.mm", "framework/Source/FlutterPlatformViewsTest.mm", + "framework/Source/FlutterTouchInterceptingView_Test.h", "framework/Source/FlutterViewControllerTest_mrc.mm", "framework/Source/FlutterViewTest.mm", + "framework/Source/SemanticsObjectTestMocks.h", + "framework/Source/SemanticsObjectTest_mrc.mm", "framework/Source/VsyncWaiterIosTest.mm", "framework/Source/accessibility_bridge_test.mm", "platform_message_handler_ios_test.mm", @@ -311,6 +314,7 @@ shared_library("ios_test_flutter") { "framework/Source/FlutterViewControllerTest.mm", "framework/Source/SemanticsObjectTest.mm", "framework/Source/UIViewController_FlutterScreenAndSceneIfLoadedTest.mm", + "framework/Source/availability_version_check_test.mm", "framework/Source/connection_collection_test.mm", ] deps = [ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index 722571bda0a03..8ad0e2351426d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -10,6 +10,8 @@ #import #include "flutter/fml/logging.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h" @@ -41,7 +43,7 @@ using namespace flutter; static void SetStatusBarHiddenForSharedApplication(BOOL hidden) { -#if APPLICATION_EXTENSION_API_ONLY +#if not APPLICATION_EXTENSION_API_ONLY [UIApplication sharedApplication].statusBarHidden = hidden; #else FML_LOG(WARNING) << "Application based status bar styling is not available in app extension."; @@ -49,7 +51,7 @@ static void SetStatusBarHiddenForSharedApplication(BOOL hidden) { } static void SetStatusBarStyleForSharedApplication(UIStatusBarStyle style) { -#if APPLICATION_EXTENSION_API_ONLY +#if not APPLICATION_EXTENSION_API_ONLY // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9 // in favor of delegating to the view controller. [[UIApplication sharedApplication] setStatusBarStyle:style]; @@ -154,10 +156,38 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - (void)showShareViewController:(NSString*)content { UIViewController* engineViewController = [_engine.get() viewController]; + NSArray* itemsToShare = @[ content ?: [NSNull null] ]; UIActivityViewController* activityViewController = [[[UIActivityViewController alloc] initWithActivityItems:itemsToShare applicationActivities:nil] autorelease]; + + if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { + // On iPad, the share screen is presented in a popover view, and requires a + // sourceView and sourceRect + FlutterTextInputPlugin* _textInputPlugin = [_engine.get() textInputPlugin]; + UITextRange* range = _textInputPlugin.textInputView.selectedTextRange; + + // firstRectForRange cannot be used here as it's current implementation does + // not always return the full rect of the range. + CGRect firstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + caretRectForPosition:(FlutterTextPosition*)range.start]; + CGRect transformedFirstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + localRectFromFrameworkTransform:firstRect]; + CGRect lastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + caretRectForPosition:(FlutterTextPosition*)range.end]; + CGRect transformedLastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView + localRectFromFrameworkTransform:lastRect]; + + activityViewController.popoverPresentationController.sourceView = engineViewController.view; + // In case of RTL Language, get the minimum x coordinate + activityViewController.popoverPresentationController.sourceRect = + CGRectMake(fmin(transformedFirstRect.origin.x, transformedLastRect.origin.x), + transformedFirstRect.origin.y, + abs(transformedLastRect.origin.x - transformedFirstRect.origin.x), + transformedFirstRect.size.height); + } + [engineViewController presentViewController:activityViewController animated:YES completion:nil]; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index 73cc460b351f3..5b0ef4123f707 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -161,6 +161,43 @@ - (void)testShareScreenInvoked { [self waitForExpectationsWithTimeout:1 handler:nil]; } +- (void)testShareScreenInvokedOnIPad { + FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease]; + [engine runWithEntrypoint:nil]; + std::unique_ptr> _weakFactory = + std::make_unique>(engine); + + XCTestExpectation* presentExpectation = + [self expectationWithDescription:@"Share view controller presented on iPad"]; + + FlutterViewController* engineViewController = + [[[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil] autorelease]; + FlutterViewController* mockEngineViewController = OCMPartialMock(engineViewController); + OCMStub([mockEngineViewController + presentViewController:[OCMArg isKindOfClass:[UIActivityViewController class]] + animated:YES + completion:nil]); + + id mockTraitCollection = OCMClassMock([UITraitCollection class]); + OCMStub([mockTraitCollection userInterfaceIdiom]).andReturn(UIUserInterfaceIdiomPad); + + FlutterPlatformPlugin* plugin = + [[[FlutterPlatformPlugin alloc] initWithEngine:_weakFactory->GetWeakPtr()] autorelease]; + FlutterPlatformPlugin* mockPlugin = OCMPartialMock(plugin); + + FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"Share.invoke" + arguments:@"Test"]; + FlutterResult result = ^(id result) { + OCMVerify([mockEngineViewController + presentViewController:[OCMArg isKindOfClass:[UIActivityViewController class]] + animated:YES + completion:nil]); + [presentExpectation fulfill]; + }; + [mockPlugin handleMethodCall:methodCall result:result]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + - (void)testClipboardHasCorrectStrings { [UIPasteboard generalPasteboard].string = nil; FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease]; @@ -368,4 +405,93 @@ - (void)testViewControllerBasedStatusBarHiddenUpdate { [bundleMock stopMocking]; } +- (void)testStatusBarHiddenUpdate { + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"]) + .andReturn(@NO); + id mockApplication = OCMClassMock([UIApplication class]); + OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); + + // Enabling system UI overlays to update status bar. + FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil] autorelease]; + std::unique_ptr> _weakFactory = + std::make_unique>(engine); + + // Update to hidden. + FlutterPlatformPlugin* plugin = [engine platformPlugin]; + + XCTestExpectation* enableSystemUIOverlaysCalled = + [self expectationWithDescription:@"setEnabledSystemUIOverlays"]; + FlutterResult resultSet = ^(id result) { + [enableSystemUIOverlaysCalled fulfill]; + }; + FlutterMethodCall* methodCallSet = + [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setEnabledSystemUIOverlays" + arguments:@[ @"SystemUiOverlay.bottom" ]]; + [plugin handleMethodCall:methodCallSet result:resultSet]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +#if not APPLICATION_EXTENSION_API_ONLY + OCMVerify([mockApplication setStatusBarHidden:YES]); +#endif + + // Update to shown. + XCTestExpectation* enableSystemUIOverlaysCalled2 = + [self expectationWithDescription:@"setEnabledSystemUIOverlays"]; + FlutterResult resultSet2 = ^(id result) { + [enableSystemUIOverlaysCalled2 fulfill]; + }; + FlutterMethodCall* methodCallSet2 = + [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setEnabledSystemUIOverlays" + arguments:@[ @"SystemUiOverlay.top" ]]; + [plugin handleMethodCall:methodCallSet2 result:resultSet2]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +#if not APPLICATION_EXTENSION_API_ONLY + OCMVerify([mockApplication setStatusBarHidden:NO]); +#endif + + [flutterViewController deregisterNotifications]; + [mockApplication stopMocking]; + [bundleMock stopMocking]; +} + +- (void)testStatusBarStyle { + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"]) + .andReturn(@NO); + id mockApplication = OCMClassMock([UIApplication class]); + OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); + + FlutterEngine* engine = [[[FlutterEngine alloc] initWithName:@"test" project:nil] autorelease]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil] autorelease]; + std::unique_ptr> _weakFactory = + std::make_unique>(engine); + XCTAssertFalse(flutterViewController.prefersStatusBarHidden); + + FlutterPlatformPlugin* plugin = [engine platformPlugin]; + + XCTestExpectation* enableSystemUIModeCalled = + [self expectationWithDescription:@"setSystemUIOverlayStyle"]; + FlutterResult resultSet = ^(id result) { + [enableSystemUIModeCalled fulfill]; + }; + FlutterMethodCall* methodCallSet = + [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setSystemUIOverlayStyle" + arguments:@{@"statusBarBrightness" : @"Brightness.dark"}]; + [plugin handleMethodCall:methodCallSet result:resultSet]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + +#if not APPLICATION_EXTENSION_API_ONLY + OCMVerify([mockApplication setStatusBarStyle:UIStatusBarStyleLightContent]); +#endif + + [flutterViewController deregisterNotifications]; + [mockApplication stopMocking]; + [bundleMock stopMocking]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index bf7e607878d1b..6c3bcd9ccdaab 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -406,10 +406,15 @@ static bool ClipRRectContainsPlatformViewBoundingRect(const SkRRect& clip_rrect, } UIView* FlutterPlatformViewsController::GetPlatformViewByID(int64_t view_id) { + return [GetFlutterTouchInterceptingViewByID(view_id) embeddedView]; +} + +FlutterTouchInterceptingView* FlutterPlatformViewsController::GetFlutterTouchInterceptingViewByID( + int64_t view_id) { if (views_.empty()) { return nil; } - return [touch_interceptors_[view_id].get() embeddedView]; + return touch_interceptors_[view_id].get(); } long FlutterPlatformViewsController::FindFirstResponderPlatformViewId() { @@ -957,6 +962,10 @@ @implementation FlutterTouchInterceptingView { fml::scoped_nsobject _delayingRecognizer; FlutterPlatformViewGestureRecognizersBlockingPolicy _blockingPolicy; UIView* _embeddedView; + // The used as the accessiblityContainer. + // The `accessiblityContainer` is used in UIKit to determine the parent of this accessibility + // node. + NSObject* _flutterAccessibilityContainer; } - (instancetype)initWithEmbeddedView:(UIView*)embeddedView platformViewsController: @@ -1035,6 +1044,14 @@ - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { } +- (void)setFlutterAccessibilityContainer:(NSObject*)flutterAccessibilityContainer { + _flutterAccessibilityContainer = flutterAccessibilityContainer; +} + +- (id)accessibilityContainer { + return _flutterAccessibilityContainer; +} + @end @implementation DelayingGestureRecognizer { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm index 1e6dad56089aa..c55111136df9b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm @@ -11,6 +11,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" @@ -3099,4 +3100,12 @@ - (void)testOnlyPlatformViewsAreRemovedWhenReset { XCTAssertEqual(mockFlutterView.subviews.firstObject, someView); } +- (void)testFlutterTouchInterceptingViewLinksToAccessibilityContainer { + FlutterTouchInterceptingView* touchInteceptorView = + [[[FlutterTouchInterceptingView alloc] init] autorelease]; + NSObject* container = [[[NSObject alloc] init] autorelease]; + [touchInteceptorView setFlutterAccessibilityContainer:container]; + XCTAssertEqualObjects([touchInteceptorView accessibilityContainer], container); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 43cb43cf4e33b..e18569868115f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -12,6 +12,7 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" #import "flutter/shell/platform/darwin/ios/ios_context.h" @class FlutterTouchInterceptingView; @@ -230,10 +231,17 @@ class FlutterPlatformViewsController { // Returns the `FlutterPlatformView`'s `view` object associated with the view_id. // // If the `FlutterPlatformViewsController` does not contain any `FlutterPlatformView` object or - // a `FlutterPlatformView` object asscociated with the view_id cannot be found, the method + // a `FlutterPlatformView` object associated with the view_id cannot be found, the method // returns nil. UIView* GetPlatformViewByID(int64_t view_id); + // Returns the `FlutterTouchInterceptingView` with the view_id. + // + // If the `FlutterPlatformViewsController` does not contain any `FlutterPlatformView` object or + // a `FlutterPlatformView` object associated with the view_id cannot be found, the method + // returns nil. + FlutterTouchInterceptingView* GetFlutterTouchInterceptingViewByID(int64_t view_id); + PostPrerollResult PostPrerollAction( const fml::RefPtr& raster_thread_merger); @@ -424,6 +432,9 @@ class FlutterPlatformViewsController { // Get embedded view - (UIView*)embeddedView; + +// Sets flutterAccessibilityContainer as this view's accessibilityContainer. +- (void)setFlutterAccessibilityContainer:(NSObject*)flutterAccessibilityContainer; @end @interface UIView (FirstResponder) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h index 96eff563e0850..888fdaa898954 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h @@ -164,6 +164,10 @@ FLUTTER_DARWIN_EXPORT - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; - (instancetype)initWithOwner:(FlutterTextInputPlugin*)textInputPlugin NS_DESIGNATED_INITIALIZER; +// TODO(louisehsu): These are being exposed to support Share in FlutterPlatformPlugin +// Consider moving that feature into FlutterTextInputPlugin to avoid exposing extra methods +- (CGRect)localRectFromFrameworkTransform:(CGRect)incomingRect; +- (CGRect)caretRectForPosition:(UITextPosition*)position; @end @interface UIView (FindFirstResponder) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index f02ab85da7ece..09946dc0e50e1 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -602,7 +602,7 @@ - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position case UITextGranularityLine: // The default UITextInputStringTokenizer does not handle line granularity // correctly. We need to implement our own line tokenizer. - result = [self lineEnclosingPosition:position]; + result = [self lineEnclosingPosition:position inDirection:direction]; break; case UITextGranularityCharacter: case UITextGranularityWord: @@ -618,7 +618,21 @@ - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position return result; } -- (UITextRange*)lineEnclosingPosition:(UITextPosition*)position { +- (UITextRange*)lineEnclosingPosition:(UITextPosition*)position + inDirection:(UITextDirection)direction { + // TODO(hellohuanlin): remove iOS 17 check. The same logic should apply to older iOS version. + if (@available(iOS 17.0, *)) { + // According to the API doc if the text position is at a text-unit boundary, it is considered + // enclosed only if the next position in the given direction is entirely enclosed. Link: + // https://developer.apple.com/documentation/uikit/uitextinputtokenizer/1614464-rangeenclosingposition?language=objc + FlutterTextPosition* flutterPosition = (FlutterTextPosition*)position; + if (flutterPosition.index > _textInputView.text.length || + (flutterPosition.index == _textInputView.text.length && + direction == UITextStorageDirectionForward)) { + return nil; + } + } + // Gets the first line break position after the input position. NSString* textAfter = [_textInputView textInRange:[_textInputView textRangeFromPosition:position diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index f18357acf0ffc..8a6cc6b53a305 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -2660,6 +2660,54 @@ - (void)testFlutterTokenizerCanParseLines { XCTAssertEqual(range.range.length, 20u); } +- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin]; + [inputView insertText:@"0123456789\n012345"]; + id tokenizer = [inputView tokenizer]; + + FlutterTextRange* range = + (FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument] + withGranularity:UITextGranularityLine + inDirection:UITextStorageDirectionBackward]; + XCTAssertEqual(range.range.location, 11u); + XCTAssertEqual(range.range.length, 6u); +} + +- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin]; + [inputView insertText:@"0123456789\n012345"]; + id tokenizer = [inputView tokenizer]; + + FlutterTextRange* range = + (FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument] + withGranularity:UITextGranularityLine + inDirection:UITextStorageDirectionForward]; + if (@available(iOS 17.0, *)) { + XCTAssertNil(range); + } else { + XCTAssertEqual(range.range.location, 11u); + XCTAssertEqual(range.range.length, 6u); + } +} + +- (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin]; + [inputView insertText:@"0123456789\n012345"]; + id tokenizer = [inputView tokenizer]; + + FlutterTextPosition* position = [FlutterTextPosition positionWithIndex:100]; + FlutterTextRange* range = + (FlutterTextRange*)[tokenizer rangeEnclosingPosition:position + withGranularity:UITextGranularityLine + inDirection:UITextStorageDirectionForward]; + if (@available(iOS 17.0, *)) { + XCTAssertNil(range); + } else { + XCTAssertEqual(range.range.location, 0u); + XCTAssertEqual(range.range.length, 0u); + } +} + - (void)testFlutterTextInputPluginRetainsFlutterTextInputView { FlutterViewController* flutterViewController = [[FlutterViewController alloc] init]; FlutterTextInputPlugin* myInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:engine]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h b/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h new file mode 100644 index 0000000000000..52f4f465c0f16 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" + +#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTER_TOUCH_INTERCEPTING_VIEW_TEST_H_ +#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTER_TOUCH_INTERCEPTING_VIEW_TEST_H_ + +@interface FlutterTouchInterceptingView (Tests) +- (id)accessibilityContainer; +@end + +#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTER_TOUCH_INTERCEPTING_VIEW_TESTS_H_ diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.h b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h index f7c611dec2ced..8a431193d1a8d 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.h +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.h @@ -18,6 +18,7 @@ constexpr float kScrollExtentMaxForInf = 1000; @class FlutterCustomAccessibilityAction; @class FlutterPlatformViewSemanticsContainer; +@class FlutterTouchInterceptingView; /** * A node in the iOS semantics tree. This object is a wrapper over a native accessibiliy @@ -171,7 +172,8 @@ constexpr float kScrollExtentMaxForInf = 1000; - (instancetype)initWithBridge:(fml::WeakPtr)bridge uid:(int32_t)uid - platformView:(UIView*)platformView NS_DESIGNATED_INITIALIZER; + platformView:(FlutterTouchInterceptingView*)platformView + NS_DESIGNATED_INITIALIZER; @end diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm index 5889a32463892..8f278fe31a721 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm @@ -157,9 +157,7 @@ @interface FlutterScrollableSemanticsObject () @property(nonatomic, retain) FlutterSemanticsScrollView* scrollView; @end -@implementation FlutterScrollableSemanticsObject { - fml::scoped_nsobject _container; -} +@implementation FlutterScrollableSemanticsObject - (instancetype)initWithBridge:(fml::WeakPtr)bridge uid:(int32_t)uid { @@ -865,9 +863,10 @@ @implementation FlutterPlatformViewSemanticsContainer - (instancetype)initWithBridge:(fml::WeakPtr)bridge uid:(int32_t)uid - platformView:(nonnull UIView*)platformView { + platformView:(nonnull FlutterTouchInterceptingView*)platformView { if (self = [super initWithBridge:bridge uid:uid]) { _platformView = [platformView retain]; + [platformView setFlutterAccessibilityContainer:self]; } return self; } @@ -882,12 +881,6 @@ - (id)nativeAccessibility { return _platformView; } -#pragma mark - UIAccessibilityContainer overrides - -- (NSArray*)accessibilityElements { - return @[ _platformView ]; -} - @end @implementation SemanticsObjectContainer { diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm index 2a3f7e799e40f..3d7f2cdf0163e 100644 --- a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest.mm @@ -7,97 +7,13 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h" FLUTTER_ASSERT_ARC -const CGRect kScreenSize = CGRectMake(0, 0, 600, 800); - -namespace flutter { -namespace { - -class SemanticsActionObservation { - public: - SemanticsActionObservation(int32_t observed_id, SemanticsAction observed_action) - : id(observed_id), action(observed_action) {} - - int32_t id; - SemanticsAction action; -}; - -class MockAccessibilityBridge : public AccessibilityBridgeIos { - public: - MockAccessibilityBridge() : observations({}) { - view_ = [[UIView alloc] initWithFrame:kScreenSize]; - window_ = [[UIWindow alloc] initWithFrame:kScreenSize]; - [window_ addSubview:view_]; - } - bool isVoiceOverRunning() const override { return isVoiceOverRunningValue; } - UIView* view() const override { return view_; } - UIView* textInputView() override { return nil; } - void DispatchSemanticsAction(int32_t id, SemanticsAction action) override { - SemanticsActionObservation observation(id, action); - observations.push_back(observation); - } - void DispatchSemanticsAction(int32_t id, - SemanticsAction action, - fml::MallocMapping args) override { - SemanticsActionObservation observation(id, action); - observations.push_back(observation); - } - void AccessibilityObjectDidBecomeFocused(int32_t id) override {} - void AccessibilityObjectDidLoseFocus(int32_t id) override {} - std::shared_ptr GetPlatformViewsController() const override { - return nil; - } - std::vector observations; - bool isVoiceOverRunningValue; - - private: - UIView* view_; - UIWindow* window_; -}; - -class MockAccessibilityBridgeNoWindow : public AccessibilityBridgeIos { - public: - MockAccessibilityBridgeNoWindow() : observations({}) { - view_ = [[UIView alloc] initWithFrame:kScreenSize]; - } - bool isVoiceOverRunning() const override { return isVoiceOverRunningValue; } - UIView* view() const override { return view_; } - UIView* textInputView() override { return nil; } - void DispatchSemanticsAction(int32_t id, SemanticsAction action) override { - SemanticsActionObservation observation(id, action); - observations.push_back(observation); - } - void DispatchSemanticsAction(int32_t id, - SemanticsAction action, - fml::MallocMapping args) override { - SemanticsActionObservation observation(id, action); - observations.push_back(observation); - } - void AccessibilityObjectDidBecomeFocused(int32_t id) override {} - void AccessibilityObjectDidLoseFocus(int32_t id) override {} - std::shared_ptr GetPlatformViewsController() const override { - return nil; - } - std::vector observations; - bool isVoiceOverRunningValue; - - private: - UIView* view_; -}; -} // namespace -} // namespace flutter - @interface SemanticsObjectTest : XCTestCase @end -@interface SemanticsObject (Tests) -- (BOOL)accessibilityScrollToVisible; -- (BOOL)accessibilityScrollToVisibleWithChild:(id)child; -- (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event; -@end - @implementation SemanticsObjectTest - (void)testCreate { @@ -203,54 +119,6 @@ - (void)testAccessibilityHitTestNoFocusableItem { XCTAssertNil(hitTestResult); } -- (void)testAccessibilityHitTestSearchCanReturnPlatformView { - fml::WeakPtrFactory factory( - new flutter::MockAccessibilityBridge()); - fml::WeakPtr bridge = factory.GetWeakPtr(); - SemanticsObject* object0 = [[SemanticsObject alloc] initWithBridge:bridge uid:0]; - SemanticsObject* object1 = [[SemanticsObject alloc] initWithBridge:bridge uid:1]; - SemanticsObject* object3 = [[SemanticsObject alloc] initWithBridge:bridge uid:3]; - UIView* platformView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; - FlutterPlatformViewSemanticsContainer* platformViewSemanticsContainer = - [[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:bridge - uid:1 - platformView:platformView]; - - object0.children = @[ object1 ]; - object0.childrenInHitTestOrder = @[ object1 ]; - object1.children = @[ platformViewSemanticsContainer, object3 ]; - object1.childrenInHitTestOrder = @[ platformViewSemanticsContainer, object3 ]; - - flutter::SemanticsNode node0; - node0.id = 0; - node0.rect = SkRect::MakeXYWH(0, 0, 200, 200); - node0.label = "0"; - [object0 setSemanticsNode:&node0]; - - flutter::SemanticsNode node1; - node1.id = 1; - node1.rect = SkRect::MakeXYWH(0, 0, 200, 200); - node1.label = "1"; - [object1 setSemanticsNode:&node1]; - - flutter::SemanticsNode node2; - node2.id = 2; - node2.rect = SkRect::MakeXYWH(0, 0, 100, 100); - node2.label = "2"; - [platformViewSemanticsContainer setSemanticsNode:&node2]; - - flutter::SemanticsNode node3; - node3.id = 3; - node3.rect = SkRect::MakeXYWH(0, 0, 200, 200); - node3.label = "3"; - [object3 setSemanticsNode:&node3]; - - CGPoint point = CGPointMake(10, 10); - id hitTestResult = [object0 _accessibilityHitTest:point withEvent:nil]; - - XCTAssertEqual(hitTestResult, platformView); -} - - (void)testAccessibilityScrollToVisible { fml::WeakPtrFactory factory( new flutter::MockAccessibilityBridge()); @@ -897,27 +765,6 @@ - (void)testShouldDispatchShowOnScreenActionForHidden { XCTAssertTrue(bridge->observations[0].action == flutter::SemanticsAction::kShowOnScreen); } -- (void)testFlutterPlatformViewSemanticsContainer { - fml::WeakPtrFactory factory( - new flutter::MockAccessibilityBridge()); - fml::WeakPtr bridge = factory.GetWeakPtr(); - __weak UIView* weakPlatformView; - @autoreleasepool { - UIView* platformView = [[UIView alloc] init]; - - FlutterPlatformViewSemanticsContainer* container = - [[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:bridge - uid:1 - platformView:platformView]; - XCTAssertEqualObjects(container.accessibilityElements, @[ platformView ]); - weakPlatformView = platformView; - XCTAssertNotNil(weakPlatformView); - } - // Check if there's no more strong references to `platformView` after container and platformView - // are released. - XCTAssertNil(weakPlatformView); -} - - (void)testFlutterSwitchSemanticsObjectMatchesUISwitch { fml::WeakPtrFactory factory( new flutter::MockAccessibilityBridge()); diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h new file mode 100644 index 0000000000000..0f0c0303d4a98 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h @@ -0,0 +1,95 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_SEMANTICS_OBJECT_TEST_MOCKS_H_ +#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_SEMANTICS_OBJECT_TEST_MOCKS_H_ + +#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" + +const CGRect kScreenSize = CGRectMake(0, 0, 600, 800); + +namespace flutter { +namespace { + +class SemanticsActionObservation { + public: + SemanticsActionObservation(int32_t observed_id, SemanticsAction observed_action) + : id(observed_id), action(observed_action) {} + + int32_t id; + SemanticsAction action; +}; + +class MockAccessibilityBridge : public AccessibilityBridgeIos { + public: + MockAccessibilityBridge() : observations({}) { + view_ = [[UIView alloc] initWithFrame:kScreenSize]; + window_ = [[UIWindow alloc] initWithFrame:kScreenSize]; + [window_ addSubview:view_]; + } + bool isVoiceOverRunning() const override { return isVoiceOverRunningValue; } + UIView* view() const override { return view_; } + UIView* textInputView() override { return nil; } + void DispatchSemanticsAction(int32_t id, SemanticsAction action) override { + SemanticsActionObservation observation(id, action); + observations.push_back(observation); + } + void DispatchSemanticsAction(int32_t id, + SemanticsAction action, + fml::MallocMapping args) override { + SemanticsActionObservation observation(id, action); + observations.push_back(observation); + } + void AccessibilityObjectDidBecomeFocused(int32_t id) override {} + void AccessibilityObjectDidLoseFocus(int32_t id) override {} + std::shared_ptr GetPlatformViewsController() const override { + return nil; + } + std::vector observations; + bool isVoiceOverRunningValue; + + private: + UIView* view_; + UIWindow* window_; +}; + +class MockAccessibilityBridgeNoWindow : public AccessibilityBridgeIos { + public: + MockAccessibilityBridgeNoWindow() : observations({}) { + view_ = [[UIView alloc] initWithFrame:kScreenSize]; + } + bool isVoiceOverRunning() const override { return isVoiceOverRunningValue; } + UIView* view() const override { return view_; } + UIView* textInputView() override { return nil; } + void DispatchSemanticsAction(int32_t id, SemanticsAction action) override { + SemanticsActionObservation observation(id, action); + observations.push_back(observation); + } + void DispatchSemanticsAction(int32_t id, + SemanticsAction action, + fml::MallocMapping args) override { + SemanticsActionObservation observation(id, action); + observations.push_back(observation); + } + void AccessibilityObjectDidBecomeFocused(int32_t id) override {} + void AccessibilityObjectDidLoseFocus(int32_t id) override {} + std::shared_ptr GetPlatformViewsController() const override { + return nil; + } + std::vector observations; + bool isVoiceOverRunningValue; + + private: + UIView* view_; +}; +} // namespace +} // namespace flutter + +@interface SemanticsObject (Tests) +- (BOOL)accessibilityScrollToVisible; +- (BOOL)accessibilityScrollToVisibleWithChild:(id)child; +- (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event; +@end + +#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_SEMANTICS_OBJECT_TEST_MOCKS_H_ diff --git a/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest_mrc.mm b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest_mrc.mm new file mode 100644 index 0000000000000..0567e37c0e30e --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/SemanticsObjectTest_mrc.mm @@ -0,0 +1,89 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTouchInterceptingView_Test.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObjectTestMocks.h" + +FLUTTER_ASSERT_NOT_ARC + +@interface SemanticsObjectTestMRC : XCTestCase +@end + +@implementation SemanticsObjectTestMRC + +- (void)testAccessibilityHitTestSearchCanReturnPlatformView { + fml::WeakPtrFactory factory( + new flutter::MockAccessibilityBridge()); + fml::WeakPtr bridge = factory.GetWeakPtr(); + SemanticsObject* object0 = [[[SemanticsObject alloc] initWithBridge:bridge uid:0] autorelease]; + SemanticsObject* object1 = [[[SemanticsObject alloc] initWithBridge:bridge uid:1] autorelease]; + SemanticsObject* object3 = [[[SemanticsObject alloc] initWithBridge:bridge uid:3] autorelease]; + FlutterTouchInterceptingView* platformView = + [[[FlutterTouchInterceptingView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)] autorelease]; + FlutterPlatformViewSemanticsContainer* platformViewSemanticsContainer = + [[[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:bridge + uid:1 + platformView:platformView] autorelease]; + + object0.children = @[ object1 ]; + object0.childrenInHitTestOrder = @[ object1 ]; + object1.children = @[ platformViewSemanticsContainer, object3 ]; + object1.childrenInHitTestOrder = @[ platformViewSemanticsContainer, object3 ]; + + flutter::SemanticsNode node0; + node0.id = 0; + node0.rect = SkRect::MakeXYWH(0, 0, 200, 200); + node0.label = "0"; + [object0 setSemanticsNode:&node0]; + + flutter::SemanticsNode node1; + node1.id = 1; + node1.rect = SkRect::MakeXYWH(0, 0, 200, 200); + node1.label = "1"; + [object1 setSemanticsNode:&node1]; + + flutter::SemanticsNode node2; + node2.id = 2; + node2.rect = SkRect::MakeXYWH(0, 0, 100, 100); + node2.label = "2"; + [platformViewSemanticsContainer setSemanticsNode:&node2]; + + flutter::SemanticsNode node3; + node3.id = 3; + node3.rect = SkRect::MakeXYWH(0, 0, 200, 200); + node3.label = "3"; + [object3 setSemanticsNode:&node3]; + + CGPoint point = CGPointMake(10, 10); + id hitTestResult = [object0 _accessibilityHitTest:point withEvent:nil]; + + XCTAssertEqual(hitTestResult, platformView); +} + +- (void)testFlutterPlatformViewSemanticsContainer { + fml::WeakPtrFactory factory( + new flutter::MockAccessibilityBridge()); + fml::WeakPtr bridge = factory.GetWeakPtr(); + FlutterTouchInterceptingView* platformView = + [[[FlutterTouchInterceptingView alloc] init] autorelease]; + @autoreleasepool { + FlutterPlatformViewSemanticsContainer* container = + [[[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:bridge + uid:1 + platformView:platformView] autorelease]; + XCTAssertEqualObjects(platformView.accessibilityContainer, container); + XCTAssertEqual(platformView.retainCount, 2u); + } + // Check if there's no more strong references to `platformView` after container and platformView + // are released. + XCTAssertEqual(platformView.retainCount, 1u); +} + +@end diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index bf19109e92d07..27b33ad4ca3d4 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -279,7 +279,7 @@ static void ReplaceSemanticsObject(SemanticsObject* oldObject, return [[[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:weak_ptr uid:node.id - platformView:weak_ptr->GetPlatformViewsController()->GetPlatformViewByID( + platformView:weak_ptr->GetPlatformViewsController()->GetFlutterTouchInterceptingViewByID( node.platformViewId)] autorelease]; } else { return [[[FlutterSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease]; diff --git a/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm b/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm new file mode 100644 index 0000000000000..c843893c216e8 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/availability_version_check_test.mm @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#import +#import + +#import "flutter/shell/platform/darwin/common/availability_version_check.h" + +@interface AvailabilityVersionCheckTest : XCTestCase +@end + +@implementation AvailabilityVersionCheckTest + +- (void)testSimple { + auto maybe_product_version = flutter::ProductVersionFromSystemVersionPList(); + XCTAssertTrue(maybe_product_version.has_value()); + if (maybe_product_version.has_value()) { + auto product_version = maybe_product_version.value(); + XCTAssertTrue(product_version > std::make_tuple(0, 0, 0)); + } +} + +@end diff --git a/testing/run_tests.py b/testing/run_tests.py index 9a0e24f4f1022..64a17f4efdc09 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -434,6 +434,7 @@ def make_test(name, flags=None, extra_env=None): unittests += [ # The accessibility library only supports Mac and Windows. make_test('accessibility_unittests'), + make_test('availability_version_check_unittests'), make_test('framework_common_unittests'), make_test('spring_animation_unittests'), ] diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index 7c15465dc9ea8..92679d2a94ae1 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -510,6 +510,13 @@ class MultiPlatformViewBackgroundForegroundScenario extends Scenario PlatformMessageResponseCallback? callback, ) { final String message = utf8.decode(data!.buffer.asUint8List()); + + // The expected first event should be 'AppLifecycleState.resumed', but + // occasionally it will receive 'AppLifecycleState.inactive' first. Skip + // any messages until 'AppLifecycleState.resumed' is received. + if (_lastLifecycleState.isEmpty && message != 'AppLifecycleState.resumed') { + return; + } if (_lastLifecycleState == 'AppLifecycleState.inactive' && message == 'AppLifecycleState.resumed') { _nextFrame = _secondFrame;