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_