Set nested clip nodes (#37850)

diff --git a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc
index 2746db7..617f22b 100644
--- a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc
+++ b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "flatland_external_view_embedder.h"
+#include <algorithm>
 #include <cstdint>
 
 #include "flutter/fml/trace_event.h"
@@ -17,6 +18,24 @@
 // overflows on operations (like FLT_MAX would).
 constexpr float kMaxHitRegionSize = 1'000'000.f;
 
+void AttachClipTransformChild(
+    FlatlandConnection* flatland,
+    FlatlandExternalViewEmbedder::ClipTransform* parent_clip_transform,
+    const fuchsia::ui::composition::TransformId& child_transform_id) {
+  flatland->flatland()->AddChild(parent_clip_transform->transform_id,
+                                 child_transform_id);
+  parent_clip_transform->children.push_back(child_transform_id);
+}
+
+void DetachClipTransformChildren(
+    FlatlandConnection* flatland,
+    FlatlandExternalViewEmbedder::ClipTransform* clip_transform) {
+  for (auto& child : clip_transform->children) {
+    flatland->flatland()->RemoveChild(clip_transform->transform_id, child);
+  }
+  clip_transform->children.clear();
+}
+
 }  // namespace
 
 FlatlandExternalViewEmbedder::FlatlandExternalViewEmbedder(
@@ -263,7 +282,6 @@
               viewport.transform_id,
               {static_cast<int32_t>(view_mutators.transform.getTranslateX()),
                static_cast<int32_t>(view_mutators.transform.getTranslateY())});
-
           flatland_->flatland()->SetScale(
               viewport.transform_id, {view_mutators.transform.getScaleX(),
                                       view_mutators.transform.getScaleY()});
@@ -271,7 +289,54 @@
         }
 
         // TODO(fxbug.dev/94000): Set HitTestBehavior.
-        // TODO(fxbug.dev/94000): Set ClipRegions.
+
+        // Set clip regions.
+        if (view_mutators.clips != viewport.mutators.clips) {
+          // Expand the clip_transforms array to fit any new transforms.
+          while (viewport.clip_transforms.size() < view_mutators.clips.size()) {
+            ClipTransform clip_transform;
+            clip_transform.transform_id = flatland_->NextTransformId();
+            flatland_->flatland()->CreateTransform(clip_transform.transform_id);
+            viewport.clip_transforms.emplace_back(std::move(clip_transform));
+          }
+          FML_CHECK(viewport.clip_transforms.size() >=
+                    view_mutators.clips.size());
+
+          // Adjust and re-parent all clip transforms.
+          for (auto& clip_transform : viewport.clip_transforms) {
+            DetachClipTransformChildren(flatland_.get(), &clip_transform);
+          }
+
+          for (size_t c = 0; c < view_mutators.clips.size(); c++) {
+            const SkMatrix& clip_matrix = view_mutators.clips[c].transform;
+            const SkRect& clip_rect = view_mutators.clips[c].rect;
+
+            flatland_->flatland()->SetTranslation(
+                viewport.clip_transforms[c].transform_id,
+                {static_cast<int32_t>(clip_matrix.getTranslateX()),
+                 static_cast<int32_t>(clip_matrix.getTranslateY())});
+            flatland_->flatland()->SetScale(
+                viewport.clip_transforms[c].transform_id,
+                {clip_matrix.getScaleX(), clip_matrix.getScaleY()});
+            fuchsia::math::Rect rect = {
+                static_cast<int32_t>(clip_rect.x()),
+                static_cast<int32_t>(clip_rect.y()),
+                static_cast<int32_t>(clip_rect.width()),
+                static_cast<int32_t>(clip_rect.height())};
+            flatland_->flatland()->SetClipBoundary(
+                viewport.clip_transforms[c].transform_id,
+                std::make_unique<fuchsia::math::Rect>(std::move(rect)));
+
+            const auto child_transform_id =
+                c != (view_mutators.clips.size() - 1)
+                    ? viewport.clip_transforms[c + 1].transform_id
+                    : viewport.transform_id;
+            AttachClipTransformChild(flatland_.get(),
+                                     &(viewport.clip_transforms[c]),
+                                     child_transform_id);
+          }
+          viewport.mutators.clips = view_mutators.clips;
+        }
 
         // Set opacity.
         if (view_mutators.opacity != viewport.mutators.opacity) {
@@ -299,9 +364,13 @@
         }
 
         // Attach the FlatlandView to the main scene graph.
+        const auto main_child_transform =
+            viewport.mutators.clips.empty()
+                ? viewport.transform_id
+                : viewport.clip_transforms[0].transform_id;
         flatland_->flatland()->AddChild(root_transform_id_,
-                                        viewport.transform_id);
-        child_transforms_.emplace_back(viewport.transform_id);
+                                        main_child_transform);
+        child_transforms_.emplace_back(main_child_transform);
       }
 
       // Acquire the surface associated with the layer.
@@ -485,19 +554,29 @@
 
   auto viewport_id = flatland_view->second.viewport_id;
   auto transform_id = flatland_view->second.transform_id;
+  auto& clip_transforms = flatland_view->second.clip_transforms;
   if (!flatland_view->second.pending_create_viewport_callback) {
     flatland_->flatland()->ReleaseViewport(viewport_id, [](auto) {});
   }
-  auto itr =
-      std::find_if(child_transforms_.begin(), child_transforms_.end(),
-                   [transform_id](fuchsia::ui::composition::TransformId id) {
-                     return id.value == transform_id.value;
-                   });
+  auto itr = std::find_if(
+      child_transforms_.begin(), child_transforms_.end(),
+      [transform_id,
+       &clip_transforms](fuchsia::ui::composition::TransformId id) {
+        return id.value == transform_id.value ||
+               (!clip_transforms.empty() &&
+                (id.value == clip_transforms[0].transform_id.value));
+      });
   if (itr != child_transforms_.end()) {
-    flatland_->flatland()->RemoveChild(root_transform_id_, transform_id);
+    flatland_->flatland()->RemoveChild(root_transform_id_, *itr);
     child_transforms_.erase(itr);
   }
+  for (auto& clip_transform : clip_transforms) {
+    DetachClipTransformChildren(flatland_.get(), &clip_transform);
+  }
   flatland_->flatland()->ReleaseTransform(transform_id);
+  for (auto& clip_transform : clip_transforms) {
+    flatland_->flatland()->ReleaseTransform(clip_transform.transform_id);
+  }
 
   flatland_views_.erase(flatland_view);
   on_view_unbound(viewport_id);
diff --git a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h
index c6d6707..b12ef0f 100644
--- a/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h
+++ b/shell/platform/fuchsia/flutter/flatland_external_view_embedder.h
@@ -114,6 +114,12 @@
                          bool hit_testable,
                          bool focusable);
 
+  // Holds the clip transform that may be applied on a FlatlandView.
+  struct ClipTransform {
+    fuchsia::ui::composition::TransformId transform_id;
+    std::vector<fuchsia::ui::composition::TransformId> children;
+  };
+
  private:
   void Reset();  // Reset state for a new frame.
 
@@ -173,6 +179,7 @@
   constexpr static EmbedderLayerId kRootLayerId = EmbedderLayerId{};
 
   struct FlatlandView {
+    std::vector<ClipTransform> clip_transforms;
     fuchsia::ui::composition::TransformId transform_id;
     fuchsia::ui::composition::ContentId viewport_id;
     ViewMutators mutators;
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 1b0fc40..40f1648 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
@@ -124,6 +124,16 @@
   return true;
 }
 
+inline bool operator==(const std::optional<fuchsia::math::Rect>& a,
+                       const std::optional<fuchsia::math::Rect>& b) {
+  if (a.has_value() != b.has_value()) {
+    return false;
+  }
+  if (!a.has_value()) {
+  }
+  return a.value() == b.value();
+}
+
 namespace flutter_runner::testing {
 
 constexpr static fuchsia::ui::composition::TransformId kInvalidTransformId{0};
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 213efaa..eb9b79d 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
@@ -284,6 +284,19 @@
       /*hit_regions*/ _));
 }
 
+Matcher<std::shared_ptr<FakeTransform>> IsClipTransformLayer(
+    const fuchsia::math::Vec& transform_translation,
+    const fuchsia::math::VecF& transform_scale,
+    std::optional<fuchsia::math::Rect> clip_bounds,
+    Matcher<std::shared_ptr<FakeTransform>> viewport_matcher) {
+  return Pointee(FieldsAre(
+      /* id */ _, transform_translation, transform_scale,
+      FakeTransform::kDefaultOrientation, /*clip_bounds*/ clip_bounds,
+      FakeTransform::kDefaultOpacity,
+      /*children*/ ElementsAre(viewport_matcher),
+      /*content*/ _,
+      /*hit_regions*/ _));
+}
 fuchsia::ui::composition::OnNextFrameBeginValues WithPresentCredits(
     uint32_t additional_present_credits) {
   fuchsia::ui::composition::OnNextFrameBeginValues values;
@@ -725,6 +738,252 @@
                   fuchsia::ui::composition::HitTestInteraction::DEFAULT)})}));
 }
 
+TEST_F(FlatlandExternalViewEmbedderTest, SceneWithOneClippedView) {
+  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));
+
+  // Create the view before drawing the scene.
+  const SkSize child_view_size_signed = SkSize::Make(256.f, 512.f);
+  const fuchsia::math::SizeU child_view_size{
+      static_cast<uint32_t>(child_view_size_signed.width()),
+      static_cast<uint32_t>(child_view_size_signed.height())};
+  auto [child_view_token, child_viewport_token] = ViewTokenPair::New();
+  const uint32_t child_view_id = child_viewport_token.value.get();
+
+  const int kOpacity = 200;
+  const float kOpacityFloat = 200 / 255.0f;
+  const fuchsia::math::VecF kScale{3.0f, 4.0f};
+  const int kTranslateX = 10;
+  const int kTranslateY = 20;
+
+  auto matrix = SkMatrix::I();
+  matrix.setScaleX(kScale.x);
+  matrix.setScaleY(kScale.y);
+  matrix.setTranslateX(kTranslateX);
+  matrix.setTranslateY(kTranslateY);
+
+  SkRect kClipRect =
+      SkRect::MakeXYWH(30, 40, child_view_size_signed.width() - 50,
+                       child_view_size_signed.height() - 60);
+  fuchsia::math::Rect kClipInMathRect = {
+      static_cast<int32_t>(kClipRect.x()), static_cast<int32_t>(kClipRect.y()),
+      static_cast<int32_t>(kClipRect.width()),
+      static_cast<int32_t>(kClipRect.height())};
+
+  auto mutators_stack = flutter::MutatorsStack();
+  mutators_stack.PushOpacity(kOpacity);
+  mutators_stack.PushTransform(matrix);
+  mutators_stack.PushClipRect(kClipRect);
+
+  flutter::EmbeddedViewParams child_view_params(matrix, child_view_size_signed,
+                                                mutators_stack);
+  external_view_embedder.CreateView(
+      child_view_id, []() {},
+      [](fuchsia::ui::composition::ContentId,
+         fuchsia::ui::composition::ChildViewWatcherHandle) {});
+  const SkRect child_view_occlusion_hint = SkRect::MakeLTRB(1, 2, 3, 4);
+  const fuchsia::math::Inset child_view_inset{
+      static_cast<int32_t>(child_view_occlusion_hint.top()),
+      static_cast<int32_t>(child_view_occlusion_hint.right()),
+      static_cast<int32_t>(child_view_occlusion_hint.bottom()),
+      static_cast<int32_t>(child_view_occlusion_hint.left())};
+  external_view_embedder.SetViewProperties(
+      child_view_id, child_view_occlusion_hint, /*hit_testable=*/false,
+      /*focusable=*/false);
+
+  // We must take into account the effect of DPR on the view scale.
+  const float kDPR = 2.0f;
+  const float kInvDPR = 1.f / kDPR;
+
+  // 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())};
+  DrawFrameWithView(
+      external_view_embedder, frame_size_signed, kDPR, child_view_id,
+      child_view_params,
+      [](SkCanvas* canvas) {
+        const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(),
+                                                canvas->imageInfo().height());
+        SkPaint rect_paint;
+        rect_paint.setColor(SK_ColorGREEN);
+        canvas->translate(canvas_size.width() / 4.f,
+                          canvas_size.height() / 2.f);
+        canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
+                                        canvas_size.height() / 32.f),
+                         rect_paint);
+      },
+      [](SkCanvas* canvas) {
+        const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(),
+                                                canvas->imageInfo().height());
+        SkPaint rect_paint;
+        rect_paint.setColor(SK_ColorRED);
+        canvas->translate(canvas_size.width() * 3.f / 4.f,
+                          canvas_size.height() / 2.f);
+        canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
+                                        canvas_size.height() / 32.f),
+                         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();
+  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
+  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)}),
+           IsClipTransformLayer(
+               {kTranslateX, kTranslateY}, kScale, kClipInMathRect,
+               IsViewportLayer(child_view_token, child_view_size,
+                               child_view_inset, {0, 0},
+                               FakeTransform::kDefaultScale, kOpacityFloat)),
+           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 with view, but get rid of the clips this time. This
+  // should remove all ClipTransformLayer instances.
+  auto new_matrix = SkMatrix::I();
+  new_matrix.setScaleX(kScale.x);
+  new_matrix.setScaleY(kScale.y);
+  auto new_mutators_stack = flutter::MutatorsStack();
+  new_mutators_stack.PushOpacity(kOpacity);
+  new_mutators_stack.PushTransform(new_matrix);
+  flutter::EmbeddedViewParams new_child_view_params(
+      new_matrix, child_view_size_signed, new_mutators_stack);
+  DrawFrameWithView(
+      external_view_embedder, frame_size_signed, kDPR, child_view_id,
+      new_child_view_params,
+      [](SkCanvas* canvas) {
+        const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(),
+                                                canvas->imageInfo().height());
+        SkPaint rect_paint;
+        rect_paint.setColor(SK_ColorGREEN);
+        canvas->translate(canvas_size.width() / 4.f,
+                          canvas_size.height() / 2.f);
+        canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
+                                        canvas_size.height() / 32.f),
+                         rect_paint);
+      },
+      [](SkCanvas* canvas) {
+        const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(),
+                                                canvas->imageInfo().height());
+        SkPaint rect_paint;
+        rect_paint.setColor(SK_ColorRED);
+        canvas->translate(canvas_size.width() * 3.f / 4.f,
+                          canvas_size.height() / 2.f);
+        canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
+                                        canvas_size.height() / 32.f),
+                         rect_paint);
+      });
+  loop().RunUntilIdle();
+  fake_flatland().FireOnNextFrameBeginEvent(WithPresentCredits(1u));
+  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)}),
+           IsViewportLayer(child_view_token, child_view_size, child_view_inset,
+                           {0, 0}, kScale, kOpacityFloat),
+           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 and draw another frame without the view.
+  external_view_embedder.DestroyView(
+      child_view_id, [](fuchsia::ui::composition::ContentId) {});
+  DrawSimpleFrame(
+      external_view_embedder, frame_size_signed, 1.f, [](SkCanvas* canvas) {
+        const SkSize canvas_size = SkSize::Make(canvas->imageInfo().width(),
+                                                canvas->imageInfo().height());
+        SkPaint rect_paint;
+        rect_paint.setColor(SK_ColorGREEN);
+        canvas->translate(canvas_size.width() / 4.f,
+                          canvas_size.height() / 2.f);
+        canvas->drawRect(SkRect::MakeWH(canvas_size.width() / 32.f,
+                                        canvas_size.height() / 32.f),
+                         rect_paint);
+      });
+  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)})}));
+}
+
 TEST_F(FlatlandExternalViewEmbedderTest, SceneWithOneView_NoOverlay) {
   fuchsia::ui::composition::ParentViewportWatcherPtr parent_viewport_watcher;
   fuchsia::ui::views::ViewportCreationToken viewport_creation_token;