Create toast specification

Implements the toast specification class so that users can specify how
what should be shown on the toast. Users must go through the
ToastSpecification::Builder() to construct the specification since the
builder enforces design invariants about the toast.

Fixed: 358606511
Change-Id: I253d3b86b3c7361a968ec417efee8aeb36cab0f2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5788918
Code-Coverage: [email protected] <[email protected]>
Commit-Queue: Steven Luong <[email protected]>
Reviewed-by: David Pennington <[email protected]>
Reviewed-by: Eshwar Stalin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1343004}
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 0751ece..7a8c5ce 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -2021,6 +2021,7 @@
       "//chrome/browser/ui/color:mixers",
       "//chrome/browser/ui/exclusive_access",
       "//chrome/browser/ui/tabs",
+      "//chrome/browser/ui/toasts",
       "//chrome/browser/ui/views/side_panel:side_panel_enums",
       "//chrome/browser/ui/views/toolbar",
       "//chrome/browser/ui/webui/cr_components/theme_color_picker",
diff --git a/chrome/browser/ui/toasts/BUILD.gn b/chrome/browser/ui/toasts/BUILD.gn
new file mode 100644
index 0000000..fc25d9d
--- /dev/null
+++ b/chrome/browser/ui/toasts/BUILD.gn
@@ -0,0 +1,41 @@
+# Copyright 2024 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+assert(is_win || is_mac || is_linux || is_chromeos)
+
+import("//build/config/ui.gni")
+
+source_set("toasts") {
+  sources = [
+    "toast_specification.cc",
+    "toast_specification.h",
+  ]
+  public_deps = [
+    "//base",
+    "//content/public/browser",
+    "//ui/base",
+  ]
+  deps = [
+    "//chrome/app:generated_resources",
+    "//chrome/app:generated_resources_grit",
+  ]
+}
+
+source_set("unit_tests") {
+  testonly = true
+  sources = [ "toast_registry_unittest.cc" ]
+  deps = [
+    ":toasts",
+    "//base",
+    "//base/test:test_support",
+    "//chrome/app:generated_resources",
+    "//chrome/app:generated_resources_grit",
+    "//chrome/browser",
+    "//chrome/browser:browser_process",
+    "//chrome/test:test_support",
+    "//content/public/browser",
+    "//testing/gtest",
+    "//ui/gfx",
+  ]
+}
diff --git a/chrome/browser/ui/toasts/OWNERS b/chrome/browser/ui/toasts/OWNERS
new file mode 100644
index 0000000..dcb63d3
--- /dev/null
+++ b/chrome/browser/ui/toasts/OWNERS
@@ -0,0 +1,2 @@
[email protected]
[email protected]
diff --git a/chrome/browser/ui/toasts/toast_registry_unittest.cc b/chrome/browser/ui/toasts/toast_registry_unittest.cc
new file mode 100644
index 0000000..c6bfe82
--- /dev/null
+++ b/chrome/browser/ui/toasts/toast_registry_unittest.cc
@@ -0,0 +1,106 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+
+#include "base/functional/callback_helpers.h"
+#include "chrome/browser/ui/toasts/toast_specification.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/vector_icons/vector_icons.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/models/simple_menu_model.h"
+#include "ui/gfx/vector_icon_types.h"
+
+class ToastRegistryTest : public testing::Test {};
+
+// By default, the toast specification must have an icon and body string.
+TEST_F(ToastRegistryTest, DefaultToast) {
+  const int string_id = IDS_MEMORY_SAVER_DIALOG_TITLE;
+  std::unique_ptr<ToastSpecification> spec =
+      ToastSpecification::Builder(vector_icons::kEmailIcon, string_id).Build();
+
+  EXPECT_EQ(string_id, spec->body_string_id());
+  EXPECT_FALSE(spec->has_close_button());
+  EXPECT_FALSE(spec->action_button_string_id().has_value());
+  EXPECT_TRUE(spec->action_button_callback().is_null());
+  EXPECT_EQ(spec->menu_model(), nullptr);
+  EXPECT_FALSE(spec->is_persistent_toast());
+}
+
+TEST_F(ToastRegistryTest, ToastWithCloseButton) {
+  const int string_id = IDS_MEMORY_SAVER_DIALOG_TITLE;
+  std::unique_ptr<ToastSpecification> spec =
+      ToastSpecification::Builder(vector_icons::kEmailIcon, string_id)
+          .AddCloseButton()
+          .Build();
+
+  EXPECT_EQ(string_id, spec->body_string_id());
+  EXPECT_TRUE(spec->has_close_button());
+  EXPECT_FALSE(spec->action_button_string_id().has_value());
+  EXPECT_TRUE(spec->action_button_callback().is_null());
+  EXPECT_EQ(spec->menu_model(), nullptr);
+  EXPECT_FALSE(spec->is_persistent_toast());
+}
+
+TEST_F(ToastRegistryTest, ToastWithActionButton) {
+  const int body_string_id = IDS_MEMORY_SAVER_DIALOG_TITLE;
+  const int action_button_string_id =
+      IDS_PERFORMANCE_INTERVENTION_DEACTIVATE_TABS_BUTTON_V1;
+  std::unique_ptr<ToastSpecification> spec =
+      ToastSpecification::Builder(vector_icons::kEmailIcon, body_string_id)
+          .AddActionButton(action_button_string_id, base::DoNothing())
+          .AddCloseButton()
+          .Build();
+  EXPECT_EQ(body_string_id, spec->body_string_id());
+  EXPECT_TRUE(spec->has_close_button());
+  EXPECT_TRUE(spec->action_button_string_id().has_value());
+  EXPECT_EQ(action_button_string_id, spec->action_button_string_id().value());
+  EXPECT_FALSE(spec->action_button_callback().is_null());
+  EXPECT_EQ(spec->menu_model(), nullptr);
+  EXPECT_FALSE(spec->is_persistent_toast());
+
+  // Toasts with an action button must have a close button.
+  EXPECT_DEATH(
+      ToastSpecification::Builder(vector_icons::kEmailIcon, body_string_id)
+          .AddActionButton(action_button_string_id, base::DoNothing())
+          .Build(),
+      "");
+
+  // A toast cannot have an action button, close button, and a menu.
+  EXPECT_DEATH(
+      ToastSpecification::Builder(vector_icons::kEmailIcon, body_string_id)
+          .AddActionButton(action_button_string_id, base::DoNothing())
+          .AddCloseButton()
+          .AddMenu(std::make_unique<ui::SimpleMenuModel>(nullptr))
+          .Build(),
+      "");
+}
+
+TEST_F(ToastRegistryTest, ToastWithMenu) {
+  const int body_string_id = IDS_MEMORY_SAVER_DIALOG_TITLE;
+  std::unique_ptr<ToastSpecification> spec =
+      ToastSpecification::Builder(vector_icons::kEmailIcon, body_string_id)
+          .AddMenu(std::make_unique<ui::SimpleMenuModel>(nullptr))
+          .Build();
+  EXPECT_EQ(body_string_id, spec->body_string_id());
+  EXPECT_FALSE(spec->has_close_button());
+  EXPECT_FALSE(spec->action_button_string_id().has_value());
+  EXPECT_TRUE(spec->action_button_callback().is_null());
+  EXPECT_NE(spec->menu_model(), nullptr);
+  EXPECT_FALSE(spec->is_persistent_toast());
+}
+
+TEST_F(ToastRegistryTest, PersistentToast) {
+  const int body_string_id = IDS_MEMORY_SAVER_DIALOG_TITLE;
+  std::unique_ptr<ToastSpecification> spec =
+      ToastSpecification::Builder(vector_icons::kEmailIcon, body_string_id)
+          .AddPersistance()
+          .Build();
+  EXPECT_EQ(body_string_id, spec->body_string_id());
+  EXPECT_FALSE(spec->has_close_button());
+  EXPECT_FALSE(spec->action_button_string_id().has_value());
+  EXPECT_TRUE(spec->action_button_callback().is_null());
+  EXPECT_EQ(spec->menu_model(), nullptr);
+  EXPECT_TRUE(spec->is_persistent_toast());
+}
diff --git a/chrome/browser/ui/toasts/toast_specification.cc b/chrome/browser/ui/toasts/toast_specification.cc
new file mode 100644
index 0000000..efc6d31
--- /dev/null
+++ b/chrome/browser/ui/toasts/toast_specification.cc
@@ -0,0 +1,91 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/toasts/toast_specification.h"
+
+#include <memory>
+
+#include "base/check.h"
+#include "base/functional/callback.h"
+#include "base/functional/callback_forward.h"
+#include "base/types/pass_key.h"
+#include "ui/base/models/simple_menu_model.h"
+
+ToastSpecification::Builder::Builder(const gfx::VectorIcon& icon,
+                                     int body_string_id)
+    : toast_specification_(
+          std::make_unique<ToastSpecification>(base::PassKey<Builder>(),
+                                               icon,
+                                               body_string_id)) {}
+
+ToastSpecification::Builder::~Builder() {
+  // Verify that ToastSpecification::Builder::Build() has been called
+  // so the toast specification is completely built.
+  CHECK(!toast_specification_);
+}
+
+ToastSpecification::Builder& ToastSpecification::Builder::AddCloseButton() {
+  toast_specification_->AddCloseButton();
+  return *this;
+}
+
+ToastSpecification::Builder& ToastSpecification::Builder::AddActionButton(
+    int action_button_string_id,
+    base::RepeatingClosure closure) {
+  toast_specification_->AddActionButton(action_button_string_id,
+                                        std::move(closure));
+  return *this;
+}
+
+ToastSpecification::Builder& ToastSpecification::Builder::AddMenu(
+    std::unique_ptr<ui::SimpleMenuModel> menu_model) {
+  toast_specification_->AddMenu(std::move(menu_model));
+  return *this;
+}
+
+ToastSpecification::Builder& ToastSpecification::Builder::AddPersistance() {
+  toast_specification_->AddPersistance();
+  return *this;
+}
+
+std::unique_ptr<ToastSpecification> ToastSpecification::Builder::Build() {
+  ValidateSpecification();
+  return std::move(toast_specification_);
+}
+
+void ToastSpecification::Builder::ValidateSpecification() {
+  // Toasts with an action button must have a close button and not a menu.
+  if (toast_specification_->action_button_string_id().has_value()) {
+    CHECK(toast_specification_->has_close_button());
+    CHECK(!toast_specification_->menu_model());
+  }
+}
+
+ToastSpecification::ToastSpecification(
+    base::PassKey<ToastSpecification::Builder>,
+    const gfx::VectorIcon& icon,
+    int string_id)
+    : icon_(icon), body_string_id_(string_id) {}
+
+ToastSpecification::~ToastSpecification() = default;
+
+void ToastSpecification::AddCloseButton() {
+  has_close_button_ = true;
+}
+
+void ToastSpecification::AddActionButton(int string_id,
+                                         base::RepeatingClosure closure) {
+  CHECK(!closure.is_null());
+  action_button_string_id_ = string_id;
+  action_button_closure_ = std::move(closure);
+}
+
+void ToastSpecification::AddMenu(
+    std::unique_ptr<ui::SimpleMenuModel> menu_model) {
+  menu_model_ = std::move(menu_model);
+}
+
+void ToastSpecification::AddPersistance() {
+  is_persistent_toast_ = true;
+}
diff --git a/chrome/browser/ui/toasts/toast_specification.h b/chrome/browser/ui/toasts/toast_specification.h
new file mode 100644
index 0000000..288f4e2b
--- /dev/null
+++ b/chrome/browser/ui/toasts/toast_specification.h
@@ -0,0 +1,87 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_TOASTS_TOAST_SPECIFICATION_H_
+#define CHROME_BROWSER_UI_TOASTS_TOAST_SPECIFICATION_H_
+
+#include <memory>
+#include <optional>
+
+#include "base/functional/callback.h"
+#include "base/functional/callback_forward.h"
+#include "base/memory/raw_ref.h"
+#include "base/types/pass_key.h"
+#include "ui/base/models/simple_menu_model.h"
+#include "ui/gfx/vector_icon_types.h"
+
+// ToastSpecification details what the toast should contain when shown.
+class ToastSpecification {
+ public:
+  class Builder final {
+   public:
+    Builder(const gfx::VectorIcon& icon, int body_string_id);
+    Builder(const Builder& other) = delete;
+    Builder& operator=(const Builder&) = delete;
+
+    ~Builder();
+
+    // Adds a "X" close button to the toast. However, if the toast have a menu,
+    // the close button will be a rounded dismiss button instead.
+    Builder& AddCloseButton();
+
+    // Adds a rounded action button toast with the `action_button_string_id`
+    // for the label. All toasts with an action button must also have a close
+    // button.
+    Builder& AddActionButton(int action_button_string_id,
+                             base::RepeatingClosure closure);
+
+    // Adds a three dot menu to the toast. Toasts with an action button are not
+    // allowed to have menu because they must have an "X" close button instead.
+    Builder& AddMenu(std::unique_ptr<ui::SimpleMenuModel> menu_model);
+
+    // Toast should only dismiss when explicitly instructed to by feature.
+    // There can only be one persistent toast shown at a time.
+    Builder& AddPersistance();
+
+    std::unique_ptr<ToastSpecification> Build();
+
+   private:
+    void ValidateSpecification();
+
+    std::unique_ptr<ToastSpecification> toast_specification_;
+  };
+
+  ToastSpecification(base::PassKey<ToastSpecification::Builder>,
+                     const gfx::VectorIcon& icon,
+                     int string_id);
+  ~ToastSpecification();
+
+  int body_string_id() const { return body_string_id_; }
+  const gfx::VectorIcon& icon() const { return *icon_; }
+  bool has_close_button() const { return has_close_button_; }
+  std::optional<int> action_button_string_id() const {
+    return action_button_string_id_;
+  }
+  base::RepeatingClosure action_button_callback() {
+    return action_button_closure_;
+  }
+  ui::SimpleMenuModel* menu_model() const { return menu_model_.get(); }
+  bool is_persistent_toast() const { return is_persistent_toast_; }
+
+  void AddCloseButton();
+  void AddActionButton(int string_id, base::RepeatingClosure closure);
+  void AddMenu(std::unique_ptr<ui::SimpleMenuModel> menu_model);
+  void AddPersistance();
+
+ private:
+  const base::raw_ref<const gfx::VectorIcon> icon_;
+  int body_string_id_;
+  bool has_close_button_ = false;
+  std::optional<int> action_button_string_id_;
+  base::RepeatingClosure action_button_closure_;
+  std::unique_ptr<ui::SimpleMenuModel> menu_model_;
+  bool is_persistent_toast_ = false;
+};
+
+#endif  // CHROME_BROWSER_UI_TOASTS_TOAST_SPECIFICATION_H_