diff --git a/ash/capture_mode/action_button_container_view.cc b/ash/capture_mode/action_button_container_view.cc index 2d235c22c9f8f..783db7bc5130d 100644 --- a/ash/capture_mode/action_button_container_view.cc +++ b/ash/capture_mode/action_button_container_view.cc @@ -11,24 +11,35 @@ #include "ash/capture_mode/action_button_view.h" #include "ash/capture_mode/capture_mode_types.h" +#include "ash/capture_mode/capture_mode_util.h" +#include "ash/style/system_shadow.h" +#include "ash/style/typography.h" #include "base/check.h" #include "base/functional/bind.h" #include "base/ranges/algorithm.h" #include "base/time/time.h" #include "ui/aura/window.h" #include "ui/base/metadata/metadata_impl_macros.h" +#include "ui/base/models/image_model.h" +#include "ui/chromeos/styles/cros_tokens_color_mappings.h" #include "ui/compositor/layer.h" #include "ui/compositor/layer_animator.h" #include "ui/gfx/animation/tween.h" #include "ui/gfx/geometry/rect.h" +#include "ui/gfx/geometry/rounded_corners_f.h" #include "ui/gfx/geometry/size.h" #include "ui/gfx/geometry/transform.h" #include "ui/gfx/geometry/vector2d.h" #include "ui/views/animation/animation_builder.h" +#include "ui/views/background.h" #include "ui/views/controls/button/button.h" +#include "ui/views/controls/image_view.h" +#include "ui/views/controls/label.h" #include "ui/views/layout/box_layout.h" #include "ui/views/layout/box_layout_view.h" +#include "ui/views/vector_icons.h" #include "ui/views/view.h" +#include "ui/views/view_class_properties.h" #include "ui/views/view_utils.h" #include "ui/views/widget/widget.h" @@ -36,6 +47,19 @@ namespace ash { namespace { +// Horizontal spacing between the error view and action buttons. +constexpr int kErrorViewActionButtonSpacing = 6; + +constexpr auto kErrorViewBorderInsets = gfx::Insets::TLBR(8, 8, 8, 12); + +constexpr int kErrorViewCornerRadius = 18; + +constexpr int kErrorViewLeadingIconSize = 20; + +// Padding to the right of the error view's leading icon, to separate the icon +// from the error message label. +constexpr auto kErrorViewLeadingIconRightPadding = 4; + // The horizontal distance between action buttons in a row. constexpr int kActionButtonSpacing = 10; @@ -56,8 +80,82 @@ constexpr base::TimeDelta kSmartActionsButtonTransitionSlideInDuration = } // namespace +ActionButtonContainerView::ErrorView::ErrorView() + : shadow_(SystemShadow::CreateShadowOnTextureLayer( + SystemShadow::Type::kElevation12)) { + SetOrientation(views::BoxLayout::Orientation::kHorizontal); + SetInsideBorderInsets(kErrorViewBorderInsets); + + SetBackground(views::CreateThemedRoundedRectBackground( + cros_tokens::kCrosSysSystemBaseElevated, + gfx::RoundedCornersF(kErrorViewCornerRadius))); + SetPaintToLayer(); + layer()->SetFillsBoundsOpaquely(false); + shadow_->SetRoundedCornerRadius(kErrorViewCornerRadius); + capture_mode_util::SetHighlightBorder( + this, kErrorViewCornerRadius, + views::HighlightBorder::Type::kHighlightBorderNoShadow); + + AddChildView(views::Builder<views::ImageView>() + .SetPreferredSize(gfx::Size(kErrorViewLeadingIconSize, + kErrorViewLeadingIconSize)) + .SetImage(ui::ImageModel::FromVectorIcon( + views::kInfoIcon, cros_tokens::kCrosSysSecondary)) + .SetProperty(views::kMarginsKey, + gfx::Insets::TLBR( + 0, 0, 0, kErrorViewLeadingIconRightPadding)) + .Build()); + + AddChildView( + views::Builder<views::Label>() + .CopyAddressTo(&error_label_) + .SetEnabledColorId(cros_tokens::kCrosSysSecondary) + .SetFontList(TypographyProvider::Get()->ResolveTypographyToken( + TypographyToken::kCrosAnnotation1)) + .Build()); +} + +ActionButtonContainerView::ErrorView::~ErrorView() = default; + +void ActionButtonContainerView::ErrorView::AddedToWidget() { + views::BoxLayoutView::AddedToWidget(); + + // Attach the shadow at the bottom of the widget layer. + ui::Layer* shadow_layer = shadow_->GetLayer(); + ui::Layer* widget_layer = GetWidget()->GetLayer(); + widget_layer->Add(shadow_layer); + widget_layer->StackAtBottom(shadow_layer); + + // Make the shadow observe the color provider source change to update the + // colors. + shadow_->ObserveColorProviderSource(GetWidget()); +} + +void ActionButtonContainerView::ErrorView::OnBoundsChanged( + const gfx::Rect& previous_bounds) { + // The shadow layer is a sibling of this view's layer, and should have the + // same bounds. + shadow_->SetContentBounds(layer()->bounds()); +} + +void ActionButtonContainerView::ErrorView::SetErrorMessage( + const std::u16string& error_message) { + error_label_->SetText(error_message); +} + +const std::u16string& +ActionButtonContainerView::ErrorView::GetErrorMessageForTesting() const { + return error_label_->GetText(); +} + ActionButtonContainerView::ActionButtonContainerView() { - SetUseDefaultFillLayout(true); + auto* box_layout = SetLayoutManager(std::make_unique<views::BoxLayout>( + views::BoxLayout::Orientation::kHorizontal)); + box_layout->set_between_child_spacing(kErrorViewActionButtonSpacing); + + error_view_ = AddChildView(std::make_unique<ErrorView>()); + error_view_->SetVisible(false); + AddChildView( views::Builder<views::BoxLayoutView>() .CopyAddressTo(&action_button_row_) @@ -124,6 +222,16 @@ const views::View::Views& ActionButtonContainerView::GetActionButtons() const { return action_button_row_->children(); } +void ActionButtonContainerView::ShowErrorView( + const std::u16string& error_message) { + error_view_->SetErrorMessage(error_message); + error_view_->SetVisible(true); +} + +void ActionButtonContainerView::HideErrorView() { + error_view_->SetVisible(false); +} + void ActionButtonContainerView::StartSmartActionsButtonTransition() { views::Widget* widget = GetWidget(); if (!widget) { @@ -214,6 +322,9 @@ void ActionButtonContainerView::SetWidgetEventsEnabled(bool enabled) { : aura::EventTargetingPolicy::kNone); } +BEGIN_METADATA(ActionButtonContainerView, ErrorView) +END_METADATA + BEGIN_METADATA(ActionButtonContainerView) END_METADATA diff --git a/ash/capture_mode/action_button_container_view.h b/ash/capture_mode/action_button_container_view.h index cffce6f621f1b..26a7059355c3f 100644 --- a/ash/capture_mode/action_button_container_view.h +++ b/ash/capture_mode/action_button_container_view.h @@ -5,6 +5,7 @@ #ifndef ASH_CAPTURE_MODE_ACTION_BUTTON_CONTAINER_VIEW_H_ #define ASH_CAPTURE_MODE_ACTION_BUTTON_CONTAINER_VIEW_H_ +#include <memory> #include <string> #include "ash/ash_export.h" @@ -13,21 +14,53 @@ #include "base/memory/weak_ptr.h" #include "ui/base/metadata/metadata_header_macros.h" #include "ui/views/controls/button/button.h" +#include "ui/views/layout/box_layout_view.h" #include "ui/views/view.h" namespace gfx { struct VectorIcon; } // namespace gfx +namespace views { +class Label; +} + namespace ash { class ActionButtonView; +class SystemShadow; -// A view that displays a row of action buttons near the capture region. +// A view that displays a row of action buttons near the capture region. It may +// display an error message if actions are not available. class ASH_EXPORT ActionButtonContainerView : public views::View { METADATA_HEADER(ActionButtonContainerView, views::View) public: + // A view that displays an error message and icon. + class ASH_EXPORT ErrorView : public views::BoxLayoutView { + METADATA_HEADER(ErrorView, views::BoxLayoutView) + + public: + ErrorView(); + ErrorView(const ErrorView&) = delete; + ErrorView& operator=(const ErrorView&) = delete; + ~ErrorView() override; + + // views::BoxLayoutView: + void AddedToWidget() override; + void OnBoundsChanged(const gfx::Rect& previous_bounds) override; + + // Sets the error message to show on the error view. + void SetErrorMessage(const std::u16string& error_message); + + const std::u16string& GetErrorMessageForTesting() const; + + private: + std::unique_ptr<SystemShadow> shadow_; + + raw_ptr<views::Label> error_label_ = nullptr; + }; + ActionButtonContainerView(); ActionButtonContainerView(const ActionButtonContainerView&) = delete; ActionButtonContainerView& operator=(const ActionButtonContainerView&) = @@ -50,12 +83,20 @@ class ASH_EXPORT ActionButtonContainerView : public views::View { // Returns the action buttons in this container. const views::View::Views& GetActionButtons() const; + // Shows an error view with the given `error_message`. + void ShowErrorView(const std::u16string& error_message); + + // Hides the error view. + void HideErrorView(); + // Starts performing the button transition triggered after pressing the smart // actions button. This will fade out existing action buttons, remove the // smart actions button, then animate in new icon buttons to replace the old // copy text and search buttons. void StartSmartActionsButtonTransition(); + const ErrorView* error_view_for_testing() const { return error_view_; } + private: // Called when the smart actions button has faded out, to start the transition // to new buttons. See `StartSmartActionsButtonTransition()`. @@ -67,6 +108,9 @@ class ASH_EXPORT ActionButtonContainerView : public views::View { // Contains the row of action buttons. raw_ptr<views::View> action_button_row_ = nullptr; + // Used to show an error message. + raw_ptr<ErrorView> error_view_ = nullptr; + base::WeakPtrFactory<ActionButtonContainerView> weak_ptr_factory_{this}; }; diff --git a/ash/capture_mode/action_button_container_view_unittest.cc b/ash/capture_mode/action_button_container_view_unittest.cc index 778837c6a63ac..d753487cc44b2 100644 --- a/ash/capture_mode/action_button_container_view_unittest.cc +++ b/ash/capture_mode/action_button_container_view_unittest.cc @@ -124,5 +124,22 @@ TEST_F(ActionButtonContainerViewTest, SmartActionsButtonTransition) { EXPECT_TRUE(IsActionButtonCollapsed(search_button)); } +TEST_F(ActionButtonContainerViewTest, ShowsErrorView) { + ActionButtonContainerView action_button_container; + const ActionButtonContainerView::ErrorView* error_view = + action_button_container.error_view_for_testing(); + + EXPECT_FALSE(error_view->GetVisible()); + + action_button_container.ShowErrorView(u"Error message"); + + EXPECT_TRUE(error_view->GetVisible()); + EXPECT_EQ(error_view->GetErrorMessageForTesting(), u"Error message"); + + action_button_container.HideErrorView(); + + EXPECT_FALSE(error_view->GetVisible()); +} + } // namespace } // namespace ash