[fuchsia][a11y] Set explicit hit regions in flatland embedder (#37338)

diff --git a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc
index f31aecb..2746db7 100644
--- a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc
+++ b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc
@@ -93,8 +93,9 @@
   zx_handle_t handle = static_cast<zx_handle_t>(view_id);
   FML_CHECK(frame_layers_.count(handle) == 0);
 
-  frame_layers_.emplace(std::make_pair(EmbedderLayerId{handle},
-                                       EmbedderLayer(frame_size_, *params)));
+  frame_layers_.emplace(std::make_pair(
+      EmbedderLayerId{handle},
+      EmbedderLayer(frame_size_, *params, flutter::RTreeFactory())));
   frame_composition_order_.push_back(handle);
 }
 
@@ -125,8 +126,9 @@
   frame_dpr_ = device_pixel_ratio;
 
   // Create the root layer.
-  frame_layers_.emplace(
-      std::make_pair(kRootLayerId, EmbedderLayer(frame_size, std::nullopt)));
+  frame_layers_.emplace(std::make_pair(
+      kRootLayerId,
+      EmbedderLayer(frame_size, std::nullopt, flutter::RTreeFactory())));
   frame_composition_order_.push_back(kRootLayerId);
 }
 
@@ -193,6 +195,19 @@
     }
   }
 
+  // Finish recording SkPictures.
+  {
+    TRACE_EVENT0("flutter", "FinishRecordingPictures");
+
+    for (const auto& surface_index : frame_surface_indices) {
+      const auto& layer = frame_layers_.find(surface_index.first);
+      FML_CHECK(layer != frame_layers_.end());
+      layer->second.picture =
+          layer->second.recorder->finishRecordingAsPicture();
+      FML_CHECK(layer->second.picture != nullptr);
+    }
+  }
+
   // Submit layers and platform views to Scenic in composition order.
   {
     TRACE_EVENT0("flutter", "SubmitLayers");
@@ -334,30 +349,43 @@
                 ? fuchsia::ui::composition::BlendMode::SRC
                 : fuchsia::ui::composition::BlendMode::SRC_OVER);
 
+        // Set hit regions for this layer; these hit regions correspond to the
+        // portions of the layer on which skia drew content.
+        {
+          FML_CHECK(layer->second.rtree);
+          std::list<SkRect> intersection_rects =
+              layer->second.rtree->searchNonOverlappingDrawnRects(
+                  SkRect::Make(layer->second.surface_size));
+
+          std::vector<fuchsia::ui::composition::HitRegion> hit_regions;
+          for (const SkRect& rect : intersection_rects) {
+            hit_regions.emplace_back();
+            auto& new_hit_region = hit_regions.back();
+            new_hit_region.region.x = rect.x();
+            new_hit_region.region.y = rect.y();
+            new_hit_region.region.width = rect.width();
+            new_hit_region.region.height = rect.height();
+            new_hit_region.hit_test =
+                fuchsia::ui::composition::HitTestInteraction::DEFAULT;
+          }
+
+          flatland_->flatland()->SetHitRegions(
+              flatland_layers_[flatland_layer_index].transform_id,
+              std::move(hit_regions));
+        }
+
         // Attach the FlatlandLayer to the main scene graph.
         flatland_->flatland()->AddChild(
             root_transform_id_,
             flatland_layers_[flatland_layer_index].transform_id);
         child_transforms_.emplace_back(
             flatland_layers_[flatland_layer_index].transform_id);
-
-        // Attach full-screen hit testing shield. Note that since the hit-region
-        // may be transformed (translated, rotated), we do not want to set
-        // width/height to FLT_MAX. This will cause a numeric overflow.
-        flatland_->flatland()->SetHitRegions(
-            flatland_layers_[flatland_layer_index].transform_id,
-            {{{0, 0, kMaxHitRegionSize, kMaxHitRegionSize},
-              fuchsia::ui::composition::HitTestInteraction::
-                  SEMANTICALLY_INVISIBLE}});
       }
 
       // Reset for the next pass:
       flatland_layer_index++;
     }
 
-    // TODO(fxbug.dev/104956): Setting per-layer overlay hit region for Flatland
-    // external view embedder should match with what is being done in GFX
-    // external view embedder.
     // Set up the input interceptor at the top of the
     // scene, if applicable.  It will capture all input, and any unwanted input
     // will be reinjected into embedded views.
@@ -396,13 +424,10 @@
 
       const auto& layer = frame_layers_.find(surface_index.first);
       FML_CHECK(layer != frame_layers_.end());
-      sk_sp<SkPicture> picture =
-          layer->second.recorder->finishRecordingAsPicture();
-      FML_CHECK(picture != nullptr);
 
       canvas->setMatrix(SkMatrix::I());
       canvas->clear(SK_ColorTRANSPARENT);
-      canvas->drawPicture(picture);
+      canvas->drawPicture(layer->second.picture);
       canvas->flush();
     }
   }
diff --git a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h
index 0600b59..c6d6707 100644
--- a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h
+++ b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h
@@ -17,6 +17,7 @@
 #include <vector>
 
 #include "flutter/flow/embedded_views.h"
+#include "flutter/flow/rtree.h"
 #include "flutter/fml/logging.h"
 #include "flutter/fml/macros.h"
 #include "flutter/shell/common/canvas_spy.h"
@@ -144,18 +145,29 @@
 
   struct EmbedderLayer {
     EmbedderLayer(const SkISize& frame_size,
-                  std::optional<flutter::EmbeddedViewParams> view_params)
-        : embedded_view_params(std::move(view_params)),
+                  std::optional<flutter::EmbeddedViewParams> view_params,
+                  flutter::RTreeFactory rtree_factory)
+        : rtree(rtree_factory.getInstance()),
+          embedded_view_params(std::move(view_params)),
           recorder(std::make_unique<SkPictureRecorder>()),
           canvas_spy(std::make_unique<flutter::CanvasSpy>(
-              recorder->beginRecording(frame_size.width(),
-                                       frame_size.height()))),
-          surface_size(frame_size) {}
+              recorder->beginRecording(SkRect::Make(frame_size),
+                                       &rtree_factory))),
+          surface_size(frame_size),
+          picture(nullptr) {}
+
+    // Records paint operations applied to this layer's `SkCanvas`.
+    // These records are used to determine which portions of this layer
+    // contain content. The embedder propagates this information to scenic, so
+    // that scenic can accurately decide which portions of this layer may
+    // interact with input.
+    sk_sp<flutter::RTree> rtree;
 
     std::optional<flutter::EmbeddedViewParams> embedded_view_params;
     std::unique_ptr<SkPictureRecorder> recorder;
     std::unique_ptr<flutter::CanvasSpy> canvas_spy;
     SkISize surface_size;
+    sk_sp<SkPicture> picture;
   };
   using EmbedderLayerId = std::optional<uint32_t>;
   constexpr static EmbedderLayerId kRootLayerId = EmbedderLayerId{};
diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc
index a55d89a..4c78208 100644
--- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc
+++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc
@@ -850,7 +850,7 @@
 
   auto& transform = found_transform->second;
   FML_CHECK(transform);
-  transform->num_hit_regions = regions.size();
+  transform->hit_regions = std::move(regions);
 }
 
 void FakeFlatland::Clear() {
diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.cc b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.cc
index a265241..777344a 100644
--- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.cc
+++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.cc
@@ -72,7 +72,7 @@
                            .children = CloneFakeTransformVector(
                                transform->children, transform_cache),
                            .content = CloneFakeContent(transform->content),
-                           .num_hit_regions = transform->num_hit_regions,
+                           .hit_regions = transform->hit_regions,
                        }));
   FML_CHECK(success);
 
@@ -136,7 +136,7 @@
   return id == other.id && translation == other.translation &&
          *clip_bounds == *other.clip_bounds &&
          orientation == other.orientation && children == other.children &&
-         content == other.content && num_hit_regions == other.num_hit_regions;
+         content == other.content && hit_regions == other.hit_regions;
 }
 
 bool FakeGraph::operator==(const FakeGraph& other) const {
diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h
index cd2170c..1b0fc40 100644
--- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h
+++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland_types.h
@@ -15,6 +15,7 @@
 #include <lib/fidl/cpp/interface_request.h>
 #include <zircon/types.h>
 
+#include <algorithm>
 #include <cstdint>
 #include <optional>
 #include <unordered_map>
@@ -98,6 +99,31 @@
   return size_equal;
 }
 
+inline bool operator==(const fuchsia::ui::composition::HitRegion& a,
+                       const fuchsia::ui::composition::HitRegion& b) {
+  return a.region == b.region && a.hit_test == b.hit_test;
+}
+
+inline bool operator!=(const fuchsia::ui::composition::HitRegion& a,
+                       const fuchsia::ui::composition::HitRegion& b) {
+  return !(a == b);
+}
+
+inline bool operator==(
+    const std::vector<fuchsia::ui::composition::HitRegion>& a,
+    const std::vector<fuchsia::ui::composition::HitRegion>& b) {
+  if (a.size() != b.size())
+    return false;
+
+  for (size_t i = 0; i < a.size(); ++i) {
+    if (a[i] != b[i]) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
 namespace flutter_runner::testing {
 
 constexpr static fuchsia::ui::composition::TransformId kInvalidTransformId{0};
@@ -194,7 +220,7 @@
 
   std::vector<std::shared_ptr<FakeTransform>> children;
   std::shared_ptr<FakeContent> content;
-  size_t num_hit_regions;
+  std::vector<fuchsia::ui::composition::HitRegion> hit_regions;
 };
 
 struct FakeGraph {
diff --git a/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc b/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc
index fac4884..213efaa 100644
--- a/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc
+++ b/shell/platform/fuchsia/flutter/tests/flatland_external_view_embedder_unittests.cc
@@ -202,6 +202,15 @@
                inset));
 }
 
+Matcher<fuchsia::ui::composition::HitRegion> IsHitRegion(
+    const float x,
+    const float y,
+    const float width,
+    const float height,
+    const fuchsia::ui::composition::HitTestInteraction hit_test) {
+  return FieldsAre(FieldsAre(x, y, width, height), hit_test);
+}
+
 Matcher<FakeGraph> IsEmptyGraph() {
   return FieldsAre(IsEmpty(), IsEmpty(), Eq(nullptr), Eq(std::nullopt));
 }
@@ -224,7 +233,7 @@
           /*scale*/ scale, FakeTransform::kDefaultOrientation,
           /*clip_bounds*/ _, FakeTransform::kDefaultOpacity,
           /*children*/ ElementsAreArray(layer_matchers),
-          /*content*/ Eq(nullptr), /*num_hit_regions*/ _)),
+          /*content*/ Eq(nullptr), /*hit_regions*/ _)),
       Eq(FakeView{
           .view_token = viewport_token_koids.second,
           .view_ref = view_ref_koids.first,
@@ -240,7 +249,8 @@
 Matcher<std::shared_ptr<FakeTransform>> IsImageLayer(
     const fuchsia::math::SizeU& layer_size,
     fuchsia::ui::composition::BlendMode blend_mode,
-    size_t num_hit_regions) {
+    std::vector<Matcher<fuchsia::ui::composition::HitRegion>>
+        hit_region_matchers) {
   return Pointee(FieldsAre(
       /*id*/ _, FakeTransform::kDefaultTranslation,
       FakeTransform::kDefaultScale, FakeTransform::kDefaultOrientation,
@@ -252,7 +262,7 @@
           FakeImage::kDefaultSampleRegion, layer_size,
           FakeImage::kDefaultOpacity, blend_mode,
           /*buffer_import_token*/ _, /*vmo_index*/ 0))),
-      num_hit_regions));
+      /* hit_regions*/ ElementsAreArray(hit_region_matchers)));
 }
 
 Matcher<std::shared_ptr<FakeTransform>> IsViewportLayer(
@@ -271,7 +281,7 @@
           /* id */ _, IsViewportProperties(view_logical_size, view_inset),
           /* viewport_token */ GetKoids(view_token).second,
           /* child_view_watcher */ _))),
-      /*num_hit_regions*/ 0));
+      /*hit_regions*/ _));
 }
 
 fuchsia::ui::composition::OnNextFrameBeginValues WithPresentCredits(
@@ -478,11 +488,21 @@
 
   // Pump the message loop. The scene updates should propagate to flatland.
   loop().RunUntilIdle();
+
   EXPECT_THAT(
       fake_flatland().graph(),
-      IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, view_ref,
-                     /*layers*/
-                     {IsImageLayer(frame_size, kFirstLayerBlendMode, 1)}));
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref,
+          /*layers*/
+          {IsImageLayer(
+              frame_size, kFirstLayerBlendMode,
+              {IsHitRegion(
+                  /* x */ 128.f,
+                  /* y */ 256.f,
+                  /* width */ 16.f,
+                  /* height */ 16.f,
+                  /* hit_test */
+                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
 }
 
 TEST_F(FlatlandExternalViewEmbedderTest, SceneWithOneView) {
@@ -598,10 +618,26 @@
       fake_flatland().graph(),
       IsFlutterGraph(
           parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/
-          {IsImageLayer(frame_size, kFirstLayerBlendMode, 1),
+          {IsImageLayer(
+               frame_size, kFirstLayerBlendMode,
+               {IsHitRegion(
+                   /* x */ 128.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
            IsViewportLayer(child_view_token, child_view_size, child_view_inset,
                            {0, 0}, kScale, kOpacityFloat),
-           IsImageLayer(frame_size, kUpperLayerBlendMode, 1)},
+           IsImageLayer(
+               frame_size, kUpperLayerBlendMode,
+               {IsHitRegion(
+                   /* x */ 384.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})},
           {kInvDPR, kInvDPR}));
 
   // Destroy the view.  The scene graph shouldn't change yet.
@@ -611,10 +647,26 @@
       fake_flatland().graph(),
       IsFlutterGraph(
           parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/
-          {IsImageLayer(frame_size, kFirstLayerBlendMode, 1),
+          {IsImageLayer(
+               frame_size, kFirstLayerBlendMode,
+               {IsHitRegion(
+                   /* x */ 128.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
            IsViewportLayer(child_view_token, child_view_size, child_view_inset,
                            {0, 0}, kScale, kOpacityFloat),
-           IsImageLayer(frame_size, kUpperLayerBlendMode, 1)},
+           IsImageLayer(
+               frame_size, kUpperLayerBlendMode,
+               {IsHitRegion(
+                   /* x */ 384.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})},
           {kInvDPR, kInvDPR}));
 
   // Draw another frame without the view.  The scene graph shouldn't change yet.
@@ -634,19 +686,43 @@
       fake_flatland().graph(),
       IsFlutterGraph(
           parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/
-          {IsImageLayer(frame_size, kFirstLayerBlendMode, 1),
+          {IsImageLayer(
+               frame_size, kFirstLayerBlendMode,
+               {IsHitRegion(
+                   /* x */ 128.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
            IsViewportLayer(child_view_token, child_view_size, child_view_inset,
                            {0, 0}, kScale, kOpacityFloat),
-           IsImageLayer(frame_size, kUpperLayerBlendMode, 1)},
+           IsImageLayer(
+               frame_size, kUpperLayerBlendMode,
+               {IsHitRegion(
+                   /* x */ 384.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})},
           {kInvDPR, kInvDPR}));
 
   // Pump the message loop.  The scene updates should propagate to flatland.
   loop().RunUntilIdle();
   EXPECT_THAT(
       fake_flatland().graph(),
-      IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
-                     view_ref, /*layers*/
-                     {IsImageLayer(frame_size, kFirstLayerBlendMode, 1)}));
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/
+          {IsImageLayer(
+              frame_size, kFirstLayerBlendMode,
+              {IsHitRegion(
+                  /* x */ 128.f,
+                  /* y */ 256.f,
+                  /* width */ 16.f,
+                  /* height */ 16.f,
+                  /* hit_test */
+                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
 }
 
 TEST_F(FlatlandExternalViewEmbedderTest, SceneWithOneView_NoOverlay) {
@@ -736,24 +812,40 @@
   loop().RunUntilIdle();
   EXPECT_THAT(
       fake_flatland().graph(),
-      IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
-                     view_ref, /*layers*/
-                     {IsImageLayer(frame_size, kFirstLayerBlendMode, 1),
-                      IsViewportLayer(child_view_token, child_view_size,
-                                      FakeViewport::kDefaultViewportInset,
-                                      {0, 0}, kScale, kOpacityFloat)}));
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/
+          {IsImageLayer(
+               frame_size, kFirstLayerBlendMode,
+               {IsHitRegion(
+                   /* x */ 128.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
+           IsViewportLayer(child_view_token, child_view_size,
+                           FakeViewport::kDefaultViewportInset, {0, 0}, kScale,
+                           kOpacityFloat)}));
 
   // Destroy the view.  The scene graph shouldn't change yet.
   external_view_embedder.DestroyView(
       child_view_id, [](fuchsia::ui::composition::ContentId) {});
   EXPECT_THAT(
       fake_flatland().graph(),
-      IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
-                     view_ref, /*layers*/
-                     {IsImageLayer(frame_size, kFirstLayerBlendMode, 1),
-                      IsViewportLayer(child_view_token, child_view_size,
-                                      FakeViewport::kDefaultViewportInset,
-                                      {0, 0}, kScale, kOpacityFloat)}));
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/
+          {IsImageLayer(
+               frame_size, kFirstLayerBlendMode,
+               {IsHitRegion(
+                   /* x */ 128.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
+           IsViewportLayer(child_view_token, child_view_size,
+                           FakeViewport::kDefaultViewportInset, {0, 0}, kScale,
+                           kOpacityFloat)}));
 
   // Draw another frame without the view.  The scene graph shouldn't change yet.
   DrawSimpleFrame(
@@ -771,20 +863,36 @@
 
   EXPECT_THAT(
       fake_flatland().graph(),
-      IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
-                     view_ref, /*layers*/
-                     {IsImageLayer(frame_size, kFirstLayerBlendMode, 1),
-                      IsViewportLayer(child_view_token, child_view_size,
-                                      FakeViewport::kDefaultViewportInset,
-                                      {0, 0}, kScale, kOpacityFloat)}));
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/
+          {IsImageLayer(
+               frame_size, kFirstLayerBlendMode,
+               {IsHitRegion(
+                   /* x */ 128.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)}),
+           IsViewportLayer(child_view_token, child_view_size,
+                           FakeViewport::kDefaultViewportInset, {0, 0}, kScale,
+                           kOpacityFloat)}));
 
   // Pump the message loop.  The scene updates should propagate to flatland.
   loop().RunUntilIdle();
   EXPECT_THAT(
       fake_flatland().graph(),
-      IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
-                     view_ref, /*layers*/
-                     {IsImageLayer(frame_size, kFirstLayerBlendMode, 1)}));
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/
+          {IsImageLayer(
+              frame_size, kFirstLayerBlendMode,
+              {IsHitRegion(
+                  /* x */ 128.f,
+                  /* y */ 256.f,
+                  /* width */ 16.f,
+                  /* height */ 16.f,
+                  /* hit_test */
+                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
 }
 
 TEST_F(FlatlandExternalViewEmbedderTest,
@@ -850,18 +958,36 @@
   loop().RunUntilIdle();
   EXPECT_THAT(
       fake_flatland().graph(),
-      IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, view_ref,
-                     /*layers*/
-                     {IsImageLayer(frame_size, kFirstLayerBlendMode, 1)}));
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref,
+          /*layers*/
+          {IsImageLayer(
+              frame_size, kFirstLayerBlendMode,
+              {IsHitRegion(
+                  /* x */ 128.f,
+                  /* y */ 256.f,
+                  /* width */ 16.f,
+                  /* height */ 16.f,
+                  /* hit_test */
+                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
 
   // Destroy the view.  The scene graph shouldn't change yet.
   external_view_embedder.DestroyView(
       child_view_id, [](fuchsia::ui::composition::ContentId) {});
   EXPECT_THAT(
       fake_flatland().graph(),
-      IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, view_ref,
-                     /*layers*/
-                     {IsImageLayer(frame_size, kFirstLayerBlendMode, 1)}));
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref,
+          /*layers*/
+          {IsImageLayer(
+              frame_size, kFirstLayerBlendMode,
+              {IsHitRegion(
+                  /* x */ 128.f,
+                  /* y */ 256.f,
+                  /* width */ 16.f,
+                  /* height */ 16.f,
+                  /* hit_test */
+                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
 
   // Draw another frame without the view and change the size. The scene graph
   // shouldn't change yet.
@@ -883,17 +1009,208 @@
       });
   EXPECT_THAT(
       fake_flatland().graph(),
-      IsFlutterGraph(parent_viewport_watcher, viewport_creation_token, view_ref,
-                     /*layers*/
-                     {IsImageLayer(frame_size, kFirstLayerBlendMode, 1)}));
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref,
+          /*layers*/
+          {IsImageLayer(
+              frame_size, kFirstLayerBlendMode,
+              {IsHitRegion(
+                  /* x */ 128.f,
+                  /* y */ 256.f,
+                  /* width */ 16.f,
+                  /* height */ 16.f,
+                  /* hit_test */
+                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
 
   // Pump the message loop.  The scene updates should propagate to flatland.
   loop().RunUntilIdle();
   EXPECT_THAT(
       fake_flatland().graph(),
-      IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
-                     view_ref, /*layers*/
-                     {IsImageLayer(new_frame_size, kFirstLayerBlendMode, 1)}));
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref, /*layers*/
+          {IsImageLayer(
+              new_frame_size, kFirstLayerBlendMode,
+              {IsHitRegion(
+                  /* x */ 64.f,
+                  /* y */ 128.f,
+                  /* width */ 8.f,
+                  /* height */ 8.f,
+                  /* hit_test */
+                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
+}
+
+// This test case exercises the scenario in which the view contains two disjoint
+// regions with painted content; we should generate two separate hit regions
+// matching the bounds of the painted regions in this case.
+TEST_F(FlatlandExternalViewEmbedderTest, SimpleScene_DisjointHitRegions) {
+  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
+  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
+  fuchsia::ui::views::ViewCreationToken view_creation_token;
+  fuchsia::ui::views::ViewRef view_ref;
+  auto view_creation_token_status = zx::channel::create(
+      0u, &viewport_creation_token.value, &view_creation_token.value);
+  ASSERT_EQ(view_creation_token_status, ZX_OK);
+  auto view_ref_pair = scenic::ViewRefPair::New();
+  view_ref_pair.view_ref.Clone(&view_ref);
+
+  // Create the `FlatlandExternalViewEmbedder` and pump the message loop until
+  // the initial scene graph is setup.
+  FlatlandExternalViewEmbedder external_view_embedder(
+      std::move(view_creation_token),
+      fuchsia::ui::views::ViewIdentityOnCreation{
+          .view_ref = std::move(view_ref_pair.view_ref),
+          .view_ref_control = std::move(view_ref_pair.control_ref),
+      },
+      fuchsia::ui::composition::ViewBoundProtocols{},
+      parent_viewport_watcher.NewRequest(), flatland_connection(),
+      fake_surface_producer());
+  flatland_connection()->Present();
+  loop().RunUntilIdle();
+  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
+  loop().RunUntilIdle();
+  EXPECT_THAT(fake_flatland().graph(),
+              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
+                             view_ref));
+
+  // Draw the scene.  The scene graph shouldn't change yet.
+  const SkISize frame_size_signed = SkISize::Make(512, 512);
+  const fuchsia::math::SizeU frame_size{
+      static_cast<uint32_t>(frame_size_signed.width()),
+      static_cast<uint32_t>(frame_size_signed.height())};
+  DrawSimpleFrame(
+      external_view_embedder, frame_size_signed, 1.f, [](SkCanvas* canvas) {
+        const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(),
+                                                canvas->imageInfo().height());
+
+        SkRect paint_region_1, paint_region_2;
+
+        paint_region_1 = SkRect::MakeXYWH(
+            canvas_size.width() / 4.f, canvas_size.height() / 2.f,
+            canvas_size.width() / 32.f, canvas_size.height() / 32.f);
+
+        SkPaint rect_paint;
+        rect_paint.setColor(SK_ColorGREEN);
+        canvas->drawRect(paint_region_1, rect_paint);
+
+        paint_region_2 = SkRect::MakeXYWH(
+            canvas_size.width() * 3.f / 4.f, canvas_size.height() / 2.f,
+            canvas_size.width() / 32.f, canvas_size.height() / 32.f);
+
+        rect_paint.setColor(SK_ColorRED);
+        canvas->drawRect(paint_region_2, rect_paint);
+      });
+  EXPECT_THAT(fake_flatland().graph(),
+              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
+                             view_ref));
+
+  // Pump the message loop. The scene updates should propagate to flatland.
+  loop().RunUntilIdle();
+
+  EXPECT_THAT(
+      fake_flatland().graph(),
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref,
+          /*layers*/
+          {IsImageLayer(
+              frame_size, kFirstLayerBlendMode,
+              {IsHitRegion(
+                   /* x */ 128.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT),
+               IsHitRegion(
+                   /* x */ 384.f,
+                   /* y */ 256.f,
+                   /* width */ 16.f,
+                   /* height */ 16.f,
+                   /* hit_test */
+                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
+}
+
+// This test case exercises the scenario in which the view contains two
+// overlapping regions with painted content; we should generate one hit
+// region matching the union of the bounds of the two painted regions in
+// this case.
+TEST_F(FlatlandExternalViewEmbedderTest, SimpleScene_OverlappingHitRegions) {
+  fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
+  fuchsia::ui::views::ViewportCreationToken viewport_creation_token;
+  fuchsia::ui::views::ViewCreationToken view_creation_token;
+  fuchsia::ui::views::ViewRef view_ref;
+  auto view_creation_token_status = zx::channel::create(
+      0u, &viewport_creation_token.value, &view_creation_token.value);
+  ASSERT_EQ(view_creation_token_status, ZX_OK);
+  auto view_ref_pair = scenic::ViewRefPair::New();
+  view_ref_pair.view_ref.Clone(&view_ref);
+
+  // Create the `FlatlandExternalViewEmbedder` and pump the message loop until
+  // the initial scene graph is setup.
+  FlatlandExternalViewEmbedder external_view_embedder(
+      std::move(view_creation_token),
+      fuchsia::ui::views::ViewIdentityOnCreation{
+          .view_ref = std::move(view_ref_pair.view_ref),
+          .view_ref_control = std::move(view_ref_pair.control_ref),
+      },
+      fuchsia::ui::composition::ViewBoundProtocols{},
+      parent_viewport_watcher.NewRequest(), flatland_connection(),
+      fake_surface_producer());
+  flatland_connection()->Present();
+  loop().RunUntilIdle();
+  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
+  loop().RunUntilIdle();
+  EXPECT_THAT(fake_flatland().graph(),
+              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
+                             view_ref));
+
+  // Draw the scene.  The scene graph shouldn't change yet.
+  const SkISize frame_size_signed = SkISize::Make(512, 512);
+  const fuchsia::math::SizeU frame_size{
+      static_cast<uint32_t>(frame_size_signed.width()),
+      static_cast<uint32_t>(frame_size_signed.height())};
+  DrawSimpleFrame(
+      external_view_embedder, frame_size_signed, 1.f, [](SkCanvas* canvas) {
+        const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(),
+                                                canvas->imageInfo().height());
+
+        SkRect paint_region_1, paint_region_2;
+
+        paint_region_1 = SkRect::MakeXYWH(
+            canvas_size.width() / 4.f, canvas_size.height() / 2.f,
+            3.f * canvas_size.width() / 8.f, canvas_size.height() / 4.f);
+
+        SkPaint rect_paint;
+        rect_paint.setColor(SK_ColorGREEN);
+        canvas->drawRect(paint_region_1, rect_paint);
+
+        paint_region_2 = SkRect::MakeXYWH(
+            canvas_size.width() * 3.f / 8.f, canvas_size.height() / 2.f,
+            3.f * canvas_size.width() / 8.f, canvas_size.height() / 4.f);
+
+        rect_paint.setColor(SK_ColorRED);
+        canvas->drawRect(paint_region_2, rect_paint);
+      });
+  EXPECT_THAT(fake_flatland().graph(),
+              IsFlutterGraph(parent_viewport_watcher, viewport_creation_token,
+                             view_ref));
+
+  // Pump the message loop. The scene updates should propagate to flatland.
+  loop().RunUntilIdle();
+
+  EXPECT_THAT(
+      fake_flatland().graph(),
+      IsFlutterGraph(
+          parent_viewport_watcher, viewport_creation_token, view_ref,
+          /*layers*/
+          {IsImageLayer(
+              frame_size, kFirstLayerBlendMode,
+              {IsHitRegion(
+                  /* x */ 128.f,
+                  /* y */ 256.f,
+                  /* width */ 256.f,
+                  /* height */ 128.f,
+                  /* hit_test */
+                  fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
 }
 
 }  // namespace flutter_runner::testing