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