Add action and close buttons to toast view

Screenshot: https://screenshot.googleplex.com/7U6DZ86wNtYxJVo.png

controller will be added in a later CL

Low-Coverage-Reason: TESTS_IN_SEPARATE_CL end-to-end tests for the taost
Fixed: 358617589
Change-Id: Ifb0c828125eb40b1b561de1476d297f29e4e9f2d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5817683
Reviewed-by: Eshwar Stalin <[email protected]>
Code-Coverage: [email protected] <[email protected]>
Commit-Queue: Charles Meng <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1351016}
diff --git a/chrome/browser/ui/toasts/api/toast_specification.h b/chrome/browser/ui/toasts/api/toast_specification.h
index c79dde4..783b13a 100644
--- a/chrome/browser/ui/toasts/api/toast_specification.h
+++ b/chrome/browser/ui/toasts/api/toast_specification.h
@@ -63,7 +63,7 @@
   std::optional<int> action_button_string_id() const {
     return action_button_string_id_;
   }
-  base::RepeatingClosure action_button_callback() {
+  base::RepeatingClosure action_button_callback() const {
     return action_button_closure_;
   }
   ui::SimpleMenuModel* menu_model() const { return menu_model_.get(); }
diff --git a/chrome/browser/ui/toasts/toast_controller.cc b/chrome/browser/ui/toasts/toast_controller.cc
index a7fd685..298ea34f 100644
--- a/chrome/browser/ui/toasts/toast_controller.cc
+++ b/chrome/browser/ui/toasts/toast_controller.cc
@@ -92,6 +92,7 @@
 
 void ToastController::OnWidgetDestroyed(views::Widget* widget) {
   current_toast_params_ = std::nullopt;
+  toast_ = nullptr;
   toast_observer_.Reset();
   toast_widget_ = nullptr;
   toast_close_timer_.Stop();
@@ -127,12 +128,9 @@
     return;
   }
 
+  CHECK(toast_);
   CHECK(toast_widget_);
-  // TODO(crbug.com/358615317): Make the toast animate out and then complete
-  // the rest of the logic synchronously afterwards.
-  // TODO(crbug.com/358610872): Log toast close reason metric and potentially
-  // integrate with Widget::CloseReason.
-  toast_widget_->Close();
+  toast_->Close(views::Widget::ClosedReason::kUnspecified);
 }
 
 void ToastController::CreateToast(const ToastSpecification* spec) {
@@ -146,7 +144,15 @@
           browser_window_interface_->TopContainer(),
           FormatString(spec->body_string_id(),
                        current_toast_params_->body_string_replacement_params_),
-          spec->icon());
+          spec->icon(), spec->has_close_button());
+  if (spec->action_button_string_id().has_value()) {
+    toast->AddActionButton(
+        FormatString(
+            spec->action_button_string_id().value(),
+            current_toast_params_->action_button_string_replacement_params_),
+        spec->action_button_callback());
+  }
+  toast_ = toast.get();
   toast_widget_ =
       views::BubbleDialogDelegateView::CreateBubble(std::move(toast));
   toast_observer_.Observe(toast_widget_);
diff --git a/chrome/browser/ui/toasts/toast_controller.h b/chrome/browser/ui/toasts/toast_controller.h
index 8fa974bd..d6c6321 100644
--- a/chrome/browser/ui/toasts/toast_controller.h
+++ b/chrome/browser/ui/toasts/toast_controller.h
@@ -19,6 +19,10 @@
 class ToastSpecification;
 enum class ToastId;
 
+namespace toasts {
+class ToastView;
+}
+
 namespace views {
 class Widget;
 }
@@ -58,6 +62,7 @@
   void ShowToast(ToastParams params);
   void CreateToast(const ToastSpecification*);
   virtual void CloseToast();
+  void OnToastClosed();
   std::u16string FormatString(int string_id,
                               std::vector<std::u16string> replacement);
 
@@ -71,6 +76,7 @@
   base::ScopedObservation<views::Widget, views::WidgetObserver> toast_observer_{
       this};
 
+  raw_ptr<toasts::ToastView> toast_;
   raw_ptr<views::Widget> toast_widget_;
 };
 
diff --git a/chrome/browser/ui/toasts/toast_view.cc b/chrome/browser/ui/toasts/toast_view.cc
index 5efa47f..8e2c7e8 100644
--- a/chrome/browser/ui/toasts/toast_view.cc
+++ b/chrome/browser/ui/toasts/toast_view.cc
@@ -9,9 +9,17 @@
 
 #include "chrome/browser/ui/browser_element_identifiers.h"
 #include "chrome/browser/ui/views/chrome_layout_provider.h"
+#include "components/strings/grit/components_strings.h"
+#include "components/vector_icons/vector_icons.h"
 #include "ui/base/interaction/element_identifier.h"
+#include "ui/base/l10n/l10n_util.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
+#include "ui/views/accessibility/view_accessibility.h"
+#include "ui/views/controls/button/image_button.h"
+#include "ui/views/controls/button/image_button_factory.h"
+#include "ui/views/controls/highlight_path_generator.h"
 #include "ui/views/view_class_properties.h"
+#include "ui/views/widget/widget.h"
 #include "ui/views/window/dialog_delegate.h"
 
 namespace toasts {
@@ -19,10 +27,12 @@
 
 ToastView::ToastView(views::View* anchor_view,
                      const std::u16string& toast_text,
-                     const gfx::VectorIcon& icon)
+                     const gfx::VectorIcon& icon,
+                     bool has_close_button)
     : BubbleDialogDelegateView(anchor_view, views::BubbleBorder::NONE),
       toast_text_(toast_text),
-      icon_(icon) {
+      icon_(icon),
+      has_close_button_(has_close_button) {
   SetShowCloseButton(false);
   DialogDelegate::SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone));
   set_corner_radius(ChromeLayoutProvider::Get()->GetDistanceMetric(
@@ -34,6 +44,13 @@
 
 ToastView::~ToastView() = default;
 
+void ToastView::AddActionButton(const std::u16string& action_button_text,
+                                base::RepeatingClosure action_button_callback) {
+  has_action_button_ = true;
+  action_button_text_ = action_button_text;
+  action_button_callback_ = std::move(action_button_callback);
+}
+
 void ToastView::Init() {
   ChromeLayoutProvider* lp = ChromeLayoutProvider::Get();
   SetLayoutManager(
@@ -46,6 +63,7 @@
 
   label_ = AddChildView(std::make_unique<views::Label>(
       toast_text_, views::style::CONTEXT_BUTTON, views::style::STYLE_PRIMARY));
+  label_->SetEnabledColorId(ui::kColorToastForeground);
   label_->SetMultiLine(false);
   label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
   label_->SetAllowCharacterBreak(false);
@@ -53,16 +71,65 @@
   label_->SetLineHeight(
       lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT));
 
+  if (has_action_button_) {
+    label_->SetProperty(
+        views::kMarginsKey,
+        gfx::Insets::TLBR(
+            0, 0, 0,
+            lp->GetDistanceMetric(
+                DISTANCE_TOAST_BUBBLE_BETWEEN_LABEL_ACTION_BUTTON_SPACING) -
+                lp->GetDistanceMetric(
+                    DISTANCE_TOAST_BUBBLE_BETWEEN_CHILD_SPACING)));
+
+    action_button_ = AddChildView(std::make_unique<views::MdTextButton>(
+        action_button_callback_.Then(base::BindRepeating(
+            &ToastView::Close, base::Unretained(this),
+            views::Widget::ClosedReason::kCloseButtonClicked)),
+        action_button_text_));
+    action_button_->SetEnabledTextColorIds(ui::kColorToastButton);
+    action_button_->SetBgColorIdOverride(ui::kColorToastBackground);
+    action_button_->SetStrokeColorIdOverride(ui::kColorToastButton);
+    action_button_->SetPreferredSize(gfx::Size(
+        action_button_->GetPreferredSize().width(),
+        lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_ACTION_BUTTON)));
+    action_button_->SetStyle(ui::ButtonStyle::kProminent);
+    action_button_->GetViewAccessibility().SetRole(ax::mojom::Role::kAlert);
+  }
+
+  if (has_close_button_) {
+    close_button_ = AddChildView(views::CreateVectorImageButtonWithNativeTheme(
+        base::BindRepeating(&ToastView::Close, base::Unretained(this),
+                            views::Widget::ClosedReason::kCloseButtonClicked),
+        vector_icons::kCloseIcon,
+        lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT) -
+            lp->GetInsetsMetric(views::INSETS_VECTOR_IMAGE_BUTTON).height(),
+        ui::kColorToastForeground));
+    views::InstallCircleHighlightPathGenerator(close_button_);
+    close_button_->SetAccessibleName(l10n_util::GetStringUTF16(IDS_CLOSE));
+  }
+
   // Height of the toast is set implicitly by adding margins depending on the
   // height of the tallest child.
-  int total_vertical_margins =
+  const int total_vertical_margins =
       lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT) -
-      lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT);
-  int top_margin = total_vertical_margins / 2;
+      lp->GetDistanceMetric(action_button_
+                                ? DISTANCE_TOAST_BUBBLE_HEIGHT_ACTION_BUTTON
+                                : DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT);
+  const int top_margin = total_vertical_margins / 2;
+  const int right_margin = lp->GetDistanceMetric(
+      close_button_    ? DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_CLOSE_BUTTON
+      : action_button_ ? DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_ACTION_BUTTON
+                       : DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_LABEL);
   set_margins(gfx::Insets::TLBR(
       top_margin, lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MARGIN_LEFT),
-      total_vertical_margins - top_margin,
-      lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_LABEL)));
+      total_vertical_margins - top_margin, right_margin));
+}
+
+void ToastView::Close(views::Widget::ClosedReason reason) {
+  // TODO(crbug.com/358610872): Take in and log toast close reason metric. Then
+  // map to Widget close reason for Widget::CloseWithReason.
+  // TODO(crbug.com/358615317): Make the toast animate out.
+  GetWidget()->CloseWithReason(reason);
 }
 
 gfx::Rect ToastView::GetBubbleBounds() {
@@ -89,7 +156,6 @@
       *icon_, color_provider->GetColor(ui::kColorToastForeground),
       ChromeLayoutProvider::Get()->GetDistanceMetric(
           DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT)));
-  label_->SetEnabledColor(color_provider->GetColor(ui::kColorToastForeground));
 }
 
 std::u16string ToastView::GetAccessibleWindowTitle() const {
diff --git a/chrome/browser/ui/toasts/toast_view.h b/chrome/browser/ui/toasts/toast_view.h
index aa38934..1a1a1bf 100644
--- a/chrome/browser/ui/toasts/toast_view.h
+++ b/chrome/browser/ui/toasts/toast_view.h
@@ -7,9 +7,18 @@
 
 #include <string>
 
+#include "base/functional/callback_forward.h"
 #include "base/memory/raw_ptr.h"
 #include "ui/base/interaction/element_identifier.h"
 #include "ui/views/bubble/bubble_dialog_delegate_view.h"
+#include "ui/views/widget/widget.h"
+
+namespace views {
+class ImageButton;
+class ImageView;
+class Label;
+class MdTextButton;
+}  // namespace views
 
 namespace toasts {
 // The view for toasts.
@@ -20,13 +29,25 @@
   DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kToastViewId);
   ToastView(views::View* anchor_view,
             const std::u16string& toast_text,
-            const gfx::VectorIcon& icon);
+            const gfx::VectorIcon& icon,
+            bool has_close_button);
   ~ToastView() override;
 
+  // Must be called prior to Init (which is called from
+  // views::BubbleDialogDelegateView::CreateBubble).
+  void AddActionButton(
+      const std::u16string& action_button_text,
+      base::RepeatingClosure action_button_callback = base::DoNothing());
+
   // views::BubbleDialogDelegateView:
   void Init() override;
 
+  // Animates out the toast, then closes the toast widget.
+  void Close(views::Widget::ClosedReason reason);
+
   views::Label* label_for_testing() { return label_; }
+  views::MdTextButton* action_button_for_testing() { return action_button_; }
+  views::ImageButton* close_button_for_testing() { return close_button_; }
 
  protected:
   // views::BubbleDialogDelegateView:
@@ -37,9 +58,16 @@
  private:
   const std::u16string toast_text_;
   const raw_ref<const gfx::VectorIcon> icon_;
+  const bool has_close_button_;
+  bool has_action_button_ = false;
+  std::u16string action_button_text_;
+  base::RepeatingClosure action_button_callback_;
 
+  // Raw pointers to child views.
   raw_ptr<views::Label> label_ = nullptr;
   raw_ptr<views::ImageView> icon_view_ = nullptr;
+  raw_ptr<views::MdTextButton> action_button_ = nullptr;
+  raw_ptr<views::ImageButton> close_button_ = nullptr;
 };
 
 }  // namespace toasts
diff --git a/chrome/browser/ui/toasts/toast_view_browsertest.cc b/chrome/browser/ui/toasts/toast_view_browsertest.cc
index 0bf9fc98..5404de6 100644
--- a/chrome/browser/ui/toasts/toast_view_browsertest.cc
+++ b/chrome/browser/ui/toasts/toast_view_browsertest.cc
@@ -22,17 +22,16 @@
   ToastViewTest() = default;
 
   void ShowUi(const std::string& name) override {
-    const std::u16string& toast_text =
-        l10n_util::GetStringUTF16(IDS_LINK_COPIED);
     views::View* anchor_view =
         BrowserView::GetBrowserViewForBrowser(browser())->top_container();
+    const std::u16string& toast_text =
+        l10n_util::GetStringUTF16(IDS_LINK_COPIED);
     const gfx::VectorIcon& icon = vector_icons::kLinkIcon;
-    std::unique_ptr<toasts::ToastView> toast;
-    if (name == "basic") {
-      toast =
-          std::make_unique<toasts::ToastView>(anchor_view, toast_text, icon);
-    } else {
-      ADD_FAILURE();
+    std::unique_ptr<toasts::ToastView> toast =
+        std::make_unique<toasts::ToastView>(anchor_view, toast_text, icon,
+                                            name == "CloseButton");
+    if (name == "ActionButton") {
+      toast->AddActionButton(l10n_util::GetStringUTF16(IDS_APP_OK));
     }
     toast_ = toast.get();
     widget_ = views::BubbleDialogDelegateView::CreateBubble(std::move(toast));
@@ -56,11 +55,39 @@
     widget_ = nullptr;
   }
 
+  toasts::ToastView* toast() { return toast_; }
+
  private:
   raw_ptr<toasts::ToastView> toast_;
   raw_ptr<views::Widget> widget_;
 };
 
-IN_PROC_BROWSER_TEST_F(ToastViewTest, InvokeUi_basic) {
+IN_PROC_BROWSER_TEST_F(ToastViewTest, InvokeUi_Basic) {
   ShowAndVerifyUi();
 }
+
+IN_PROC_BROWSER_TEST_F(ToastViewTest, InvokeUi_ActionButton) {
+  ShowUi("ActionButton");
+  ASSERT_TRUE(VerifyUi());
+  ChromeLayoutProvider* lp = ChromeLayoutProvider::Get();
+  EXPECT_TRUE(toast()->action_button_for_testing());
+  EXPECT_EQ(lp->GetDistanceMetric(
+                DISTANCE_TOAST_BUBBLE_BETWEEN_LABEL_ACTION_BUTTON_SPACING),
+            toast()->action_button_for_testing()->x() -
+                toast()->label_for_testing()->bounds().right());
+  EXPECT_EQ(
+      lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_ACTION_BUTTON),
+      toast()->GetDialogClientView()->width() - toast()->bounds().right());
+  DismissUi();
+}
+
+IN_PROC_BROWSER_TEST_F(ToastViewTest, InvokeUi_CloseButton) {
+  ShowUi("CloseButton");
+  ASSERT_TRUE(VerifyUi());
+  ChromeLayoutProvider* lp = ChromeLayoutProvider::Get();
+  EXPECT_TRUE(toast()->close_button_for_testing());
+  EXPECT_EQ(
+      lp->GetDistanceMetric(DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_CLOSE_BUTTON),
+      toast()->GetDialogClientView()->width() - toast()->bounds().right());
+  DismissUi();
+}
diff --git a/chrome/browser/ui/views/chrome_layout_provider.cc b/chrome/browser/ui/views/chrome_layout_provider.cc
index f35dc82d..f975f4a 100644
--- a/chrome/browser/ui/views/chrome_layout_provider.cc
+++ b/chrome/browser/ui/views/chrome_layout_provider.cc
@@ -164,12 +164,20 @@
       return 8;
     case DISTANCE_TOAST_BUBBLE_BETWEEN_CHILD_SPACING:
       return 4;
+    case DISTANCE_TOAST_BUBBLE_BETWEEN_LABEL_ACTION_BUTTON_SPACING:
+      return 16;
     case DISTANCE_TOAST_BUBBLE_HEIGHT:
       return 48;
+    case DISTANCE_TOAST_BUBBLE_HEIGHT_ACTION_BUTTON:
+      return 36;
     case DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT:
       return 24;
     case DISTANCE_TOAST_BUBBLE_MARGIN_LEFT:
       return 12;
+    case DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_ACTION_BUTTON:
+      return 6;
+    case DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_CLOSE_BUTTON:
+      return 12;
     case DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_LABEL:
       return 16;
   }
diff --git a/chrome/browser/ui/views/chrome_layout_provider.h b/chrome/browser/ui/views/chrome_layout_provider.h
index 133b481..45fe8da 100644
--- a/chrome/browser/ui/views/chrome_layout_provider.h
+++ b/chrome/browser/ui/views/chrome_layout_provider.h
@@ -102,12 +102,22 @@
   DISTANCE_RICH_HOVER_BUTTON_ICON_HORIZONTAL,
   // Distance between most child elements inside the toast.
   DISTANCE_TOAST_BUBBLE_BETWEEN_CHILD_SPACING,
+  // Distance between the toast label and action button.
+  DISTANCE_TOAST_BUBBLE_BETWEEN_LABEL_ACTION_BUTTON_SPACING,
   // Height of the toast.
   DISTANCE_TOAST_BUBBLE_HEIGHT,
+  // Height of toast action buttons.
+  DISTANCE_TOAST_BUBBLE_HEIGHT_ACTION_BUTTON,
   // Height of the toast text and icon.
   DISTANCE_TOAST_BUBBLE_HEIGHT_CONTENT,
   // Distance between left border of the toast and the icon.
   DISTANCE_TOAST_BUBBLE_MARGIN_LEFT,
+  // Distance between the right border of the toast and the action button, if
+  // the action button is the rightmost element.
+  DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_ACTION_BUTTON,
+  // Distance between the right border of the toast and the close button, if the
+  // close button is the rightmost element.
+  DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_CLOSE_BUTTON,
   // Distance between the right border of the toast and the label, if the label
   // is the rightmost element.
   DISTANCE_TOAST_BUBBLE_MARGIN_RIGHT_LABEL,