1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ash/assistant/ui/main_stage/assistant_onboarding_suggestion_view.h"
6 
7 #include "ash/assistant/ui/assistant_ui_constants.h"
8 #include "ash/assistant/ui/assistant_view_delegate.h"
9 #include "ash/assistant/util/resource_util.h"
10 #include "base/bind.h"
11 #include "base/stl_util.h"
12 #include "base/strings/utf_string_conversions.h"
13 #include "chromeos/services/assistant/public/cpp/assistant_service.h"
14 #include "ui/gfx/color_palette.h"
15 #include "ui/views/animation/ink_drop.h"
16 #include "ui/views/background.h"
17 #include "ui/views/controls/highlight_path_generator.h"
18 #include "ui/views/controls/image_view.h"
19 #include "ui/views/controls/label.h"
20 #include "ui/views/layout/flex_layout.h"
21 #include "ui/views/metadata/metadata_impl_macros.h"
22 #include "ui/views/view_class_properties.h"
23 
24 namespace ash {
25 
26 namespace {
27 
28 using assistant::util::ResourceLinkType;
29 
30 // Appearance.
31 constexpr int kCornerRadiusDip = 12;
32 constexpr int kIconSizeDip = 24;
33 constexpr int kLabelLineHeight = 20;
34 constexpr int kLabelSizeDelta = 2;
35 constexpr int kPreferredHeightDip = 72;
36 
37 // Ink Drop.
38 constexpr float kInkDropVisibleOpacity = 0.06f;
39 constexpr float kInkDropHighlightOpacity = 0.08f;
40 
41 // Helpers ---------------------------------------------------------------------
42 
GetBackgroundColor(int index)43 SkColor GetBackgroundColor(int index) {
44   constexpr SkColor kBackgroundColors[] = {gfx::kGoogleBlue050,
45                                            gfx::kGoogleRed050,
46                                            gfx::kGoogleYellow050,
47                                            gfx::kGoogleGreen050,
48                                            SkColorSetRGB(0xF6, 0xE9, 0xF8),
49                                            gfx::kGoogleBlue050};
50   DCHECK_GE(index, 0);
51   DCHECK_LT(index, static_cast<int>(base::size(kBackgroundColors)));
52   return kBackgroundColors[index];
53 }
54 
GetForegroundColor(int index)55 SkColor GetForegroundColor(int index) {
56   constexpr SkColor kForegroundColors[] = {gfx::kGoogleBlue800,
57                                            gfx::kGoogleRed800,
58                                            SkColorSetRGB(0xBF, 0x50, 0x00),
59                                            gfx::kGoogleGreen800,
60                                            SkColorSetRGB(0x8A, 0x0E, 0x9E),
61                                            gfx::kGoogleBlue800};
62   DCHECK_GE(index, 0);
63   DCHECK_LT(index, static_cast<int>(base::size(kForegroundColors)));
64   return kForegroundColors[index];
65 }
66 
67 }  // namespace
68 
69 // AssistantOnboardingSuggestionView -------------------------------------------
70 
AssistantOnboardingSuggestionView(AssistantViewDelegate * delegate,const chromeos::assistant::AssistantSuggestion & suggestion,int index)71 AssistantOnboardingSuggestionView::AssistantOnboardingSuggestionView(
72     AssistantViewDelegate* delegate,
73     const chromeos::assistant::AssistantSuggestion& suggestion,
74     int index)
75     : views::Button(base::BindRepeating(
76           &AssistantOnboardingSuggestionView::OnButtonPressed,
77           base::Unretained(this))),
78       delegate_(delegate),
79       suggestion_id_(suggestion.id),
80       index_(index) {
81   InitLayout(suggestion);
82 }
83 
84 AssistantOnboardingSuggestionView::~AssistantOnboardingSuggestionView() =
85     default;
86 
GetHeightForWidth(int width) const87 int AssistantOnboardingSuggestionView::GetHeightForWidth(int width) const {
88   return kPreferredHeightDip;
89 }
90 
ChildPreferredSizeChanged(views::View * child)91 void AssistantOnboardingSuggestionView::ChildPreferredSizeChanged(
92     views::View* child) {
93   PreferredSizeChanged();
94 }
95 
AddLayerBeneathView(ui::Layer * layer)96 void AssistantOnboardingSuggestionView::AddLayerBeneathView(ui::Layer* layer) {
97   // This method is called by InkDropHostView, a base class of Button, to add
98   // ink drop layers beneath |this| view. Unfortunately, this will cause ink
99   // drop layers to also paint below our background and, because our background
100   // is opaque, they will not be visible to the user. To work around this, we
101   // instead add ink drop layers beneath |ink_drop_container_| which *will*
102   // paint above our background.
103   ink_drop_container_->AddLayerBeneathView(layer);
104 }
105 
RemoveLayerBeneathView(ui::Layer * layer)106 void AssistantOnboardingSuggestionView::RemoveLayerBeneathView(
107     ui::Layer* layer) {
108   // This method is called by InkDropHostView, a base class of Button, to remove
109   // ink drop layers beneath |this| view. Because we instead added ink drop
110   // layers beneath |ink_drop_container_| to work around paint ordering issues,
111   // we inversely need to remove ink drop layers from |ink_drop_container_|
112   // here. See also comments in AddLayerBeneathView().
113   ink_drop_container_->RemoveLayerBeneathView(layer);
114 }
115 
GetIcon() const116 const gfx::ImageSkia& AssistantOnboardingSuggestionView::GetIcon() const {
117   return icon_->GetImage();
118 }
119 
GetText() const120 const base::string16& AssistantOnboardingSuggestionView::GetText() const {
121   return label_->GetText();
122 }
123 
InitLayout(const chromeos::assistant::AssistantSuggestion & suggestion)124 void AssistantOnboardingSuggestionView::InitLayout(
125     const chromeos::assistant::AssistantSuggestion& suggestion) {
126   // A11y.
127   SetAccessibleName(base::UTF8ToUTF16(suggestion.text));
128 
129   // Background.
130   SetBackground(views::CreateRoundedRectBackground(GetBackgroundColor(index_),
131                                                    kCornerRadiusDip));
132 
133   // Focus.
134   SetFocusBehavior(FocusBehavior::ALWAYS);
135   focus_ring()->SetColor(gfx::kGoogleBlue300);
136 
137   // Ink Drop.
138   SetInkDropMode(InkDropMode::ON);
139   SetHasInkDropActionOnClick(true);
140   SetInkDropBaseColor(GetForegroundColor(index_));
141   SetInkDropVisibleOpacity(kInkDropVisibleOpacity);
142   SetInkDropHighlightOpacity(kInkDropHighlightOpacity);
143 
144   // Installing this highlight path generator will set the desired shape for
145   // both ink drop effects as well as our focus ring.
146   views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
147                                                 kCornerRadiusDip);
148 
149   // By default, InkDropHostView, a base class of Button, will add ink drop
150   // layers beneath |this| view. Unfortunately, this will cause ink drop layers
151   // to paint below our background and, because our background is opaque, they
152   // will not be visible to the user. To work around this, we will instead be
153   // adding/removing ink drop layers as necessary to/from |ink_drop_container_|
154   // which *will* paint above our background.
155   ink_drop_container_ =
156       AddChildView(std::make_unique<views::InkDropContainerView>());
157 
158   // Layout.
159   auto& layout =
160       SetLayoutManager(std::make_unique<views::FlexLayout>())
161           ->SetCollapseMargins(true)
162           .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
163           .SetDefault(views::kFlexBehaviorKey, views::FlexSpecification())
164           .SetDefault(views::kMarginsKey, gfx::Insets(0, 2 * kSpacingDip))
165           .SetInteriorMargin(gfx::Insets(0, 2 * kMarginDip))
166           .SetOrientation(views::LayoutOrientation::kHorizontal);
167 
168   // NOTE: Our |layout| ignores the view for drawing focus as it is a special
169   // view which lays out itself. Removing this would cause it *not* to paint.
170   layout.SetChildViewIgnoredByLayout(focus_ring(), true);
171 
172   // NOTE: Our |ink_drop_container_| serves only to hold reference to ink drop
173   // layers for painting purposes. It can be completely ignored by our |layout|.
174   layout.SetChildViewIgnoredByLayout(ink_drop_container_, true);
175 
176   // Icon.
177   icon_ = AddChildView(std::make_unique<views::ImageView>());
178   icon_->SetImageSize({kIconSizeDip, kIconSizeDip});
179   icon_->SetPreferredSize({kIconSizeDip, kIconSizeDip});
180 
181   const GURL& url = suggestion.icon_url;
182   if (assistant::util::IsResourceLinkType(url, ResourceLinkType::kIcon)) {
183     // Handle local images.
184     icon_->SetImage(assistant::util::CreateVectorIcon(
185         assistant::util::AppendOrReplaceColorParam(url,
186                                                    GetForegroundColor(index_)),
187         kIconSizeDip));
188   } else if (url.is_valid()) {
189     // Handle remote images.
190     delegate_->DownloadImage(
191         url, base::BindOnce(&AssistantOnboardingSuggestionView::UpdateIcon,
192                             weak_factory_.GetWeakPtr()));
193   }
194 
195   // Label.
196   label_ = AddChildView(std::make_unique<views::Label>());
197   label_->SetAutoColorReadabilityEnabled(false);
198   label_->SetEnabledColor(GetForegroundColor(index_));
199   label_->SetFontList(assistant::ui::GetDefaultFontList()
200                           .DeriveWithSizeDelta(kLabelSizeDelta)
201                           .DeriveWithWeight(gfx::Font::Weight::MEDIUM));
202   label_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
203   label_->SetLineHeight(kLabelLineHeight);
204   label_->SetMaxLines(2);
205   label_->SetMultiLine(true);
206   label_->SetProperty(
207       views::kFlexBehaviorKey,
208       views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
209                                views::MaximumFlexSizeRule::kUnbounded));
210   label_->SetText(base::UTF8ToUTF16(suggestion.text));
211 
212   // Workaround issue where multiline label is not allocated enough height.
213   label_->SetPreferredSize(
214       gfx::Size(label_->GetPreferredSize().width(), 2 * kLabelLineHeight));
215 }
216 
UpdateIcon(const gfx::ImageSkia & icon)217 void AssistantOnboardingSuggestionView::UpdateIcon(const gfx::ImageSkia& icon) {
218   if (!icon.isNull())
219     icon_->SetImage(icon);
220 }
221 
OnButtonPressed()222 void AssistantOnboardingSuggestionView::OnButtonPressed() {
223   delegate_->OnSuggestionPressed(suggestion_id_);
224 }
225 
226 BEGIN_METADATA(AssistantOnboardingSuggestionView, views::Button)
227 END_METADATA
228 
229 }  // namespace ash