search_result_tile_item_view.cc 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. // Copyright 2014 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. #include "ash/app_list/views/search_result_tile_item_view.h"
  5. #include <utility>
  6. #include "ash/app_list/app_list_metrics.h"
  7. #include "ash/app_list/app_list_util.h"
  8. #include "ash/app_list/app_list_view_delegate.h"
  9. #include "ash/app_list/model/app_list_model.h"
  10. #include "ash/app_list/model/search/search_model.h"
  11. #include "ash/app_list/model/search/search_result.h"
  12. #include "ash/app_list/views/app_list_item_view.h"
  13. #include "ash/public/cpp/app_list/app_list_color_provider.h"
  14. #include "ash/public/cpp/app_list/app_list_config.h"
  15. #include "ash/public/cpp/app_list/app_list_features.h"
  16. #include "ash/public/cpp/app_list/app_list_types.h"
  17. #include "ash/public/cpp/app_list/vector_icons/vector_icons.h"
  18. #include "ash/public/cpp/ash_typography.h"
  19. #include "ash/public/cpp/pagination/pagination_model.h"
  20. #include "ash/strings/grit/ash_strings.h"
  21. #include "base/bind.h"
  22. #include "base/i18n/number_formatting.h"
  23. #include "base/metrics/histogram_macros.h"
  24. #include "base/metrics/user_metrics.h"
  25. #include "base/strings/utf_string_conversions.h"
  26. #include "ui/accessibility/ax_node_data.h"
  27. #include "ui/base/l10n/l10n_util.h"
  28. #include "ui/base/models/image_model.h"
  29. #include "ui/base/themed_vector_icon.h"
  30. #include "ui/gfx/canvas.h"
  31. #include "ui/gfx/color_palette.h"
  32. #include "ui/gfx/geometry/rect_conversions.h"
  33. #include "ui/gfx/image/image_skia_operations.h"
  34. #include "ui/gfx/paint_vector_icon.h"
  35. #include "ui/views/accessibility/accessibility_paint_checks.h"
  36. #include "ui/views/accessibility/view_accessibility.h"
  37. #include "ui/views/background.h"
  38. #include "ui/views/controls/image_view.h"
  39. #include "ui/views/controls/label.h"
  40. #include "ui/views/controls/menu/menu_runner.h"
  41. #include "ui/views/focus/focus_manager.h"
  42. #include "ui/views/style/typography.h"
  43. namespace ash {
  44. namespace {
  45. // The width of the focus ring.
  46. constexpr int kSelectionRingWidth = 2;
  47. constexpr int kSearchTileWidth = 80;
  48. constexpr int kSearchTileTopPadding = 4;
  49. constexpr int kSearchTitleSpacing = 7;
  50. constexpr int kSearchPriceSize = 37;
  51. constexpr int kSearchRatingSize = 26;
  52. constexpr int kSearchRatingStarSize = 12;
  53. constexpr int kSearchRatingStarHorizontalSpacing = 1;
  54. constexpr int kSearchRatingStarVerticalSpacing = 2;
  55. // Text line height in the search result tile.
  56. constexpr int kTileTextLineHeight = 16;
  57. constexpr int kBadgeIconShadowWidth = 1;
  58. // Delta applied to the font size of SearchResultTile title.
  59. constexpr int kSearchResultTileTitleTextSizeDelta = 1;
  60. constexpr int kIconSelectedSize = 58;
  61. constexpr int kIconSelectedCornerRadius = 4;
  62. // Offset for centering star rating when there is no price.
  63. constexpr int kSearchRatingCenteringOffset =
  64. ((kSearchTileWidth -
  65. (kSearchRatingSize + kSearchRatingStarHorizontalSpacing +
  66. kSearchRatingStarSize)) /
  67. 2);
  68. } // namespace
  69. SearchResultTileItemView::SearchResultTileItemView(
  70. AppListViewDelegate* view_delegate)
  71. : view_delegate_(view_delegate),
  72. is_app_reinstall_recommendation_enabled_(
  73. app_list_features::IsAppReinstallZeroStateEnabled()) {
  74. SetCallback(base::BindRepeating(&SearchResultTileItemView::OnButtonPressed,
  75. base::Unretained(this)));
  76. SetFocusBehavior(FocusBehavior::ALWAYS);
  77. // TODO(crbug.com/1218186): Remove this, this is in place temporarily to be
  78. // able to submit accessibility checks, but this focusable View needs to
  79. // add a name so that the screen reader knows what to announce.
  80. SetProperty(views::kSkipAccessibilityPaintChecks, true);
  81. // When |result_| is null, the tile is invisible. Calling SetSearchResult with
  82. // a non-null item makes the tile visible.
  83. SetVisible(false);
  84. GetViewAccessibility().OverrideIsLeaf(true);
  85. // Prevent the icon view from interfering with our mouse events.
  86. icon_ = AddChildView(std::make_unique<views::ImageView>());
  87. icon_->SetCanProcessEventsWithinSubtree(false);
  88. icon_->SetVerticalAlignment(views::ImageView::Alignment::kLeading);
  89. badge_ = AddChildView(std::make_unique<views::ImageView>());
  90. badge_->SetCanProcessEventsWithinSubtree(false);
  91. badge_->SetVerticalAlignment(views::ImageView::Alignment::kLeading);
  92. badge_->SetVisible(false);
  93. title_ = AddChildView(std::make_unique<views::Label>());
  94. title_->SetAutoColorReadabilityEnabled(false);
  95. title_->SetEnabledColor(AppListColorProvider::Get()->GetSearchBoxTextColor(
  96. /*default_color*/ SK_ColorWHITE));
  97. title_->SetLineHeight(kTileTextLineHeight);
  98. title_->SetHorizontalAlignment(gfx::ALIGN_CENTER);
  99. title_->SetHandlesTooltips(false);
  100. title_->SetAllowCharacterBreak(true);
  101. rating_ = AddChildView(std::make_unique<views::Label>());
  102. rating_->SetEnabledColor(
  103. AppListColorProvider::Get()->GetSearchBoxSecondaryTextColor(
  104. /*default_color*/ gfx::kGoogleGrey700));
  105. rating_->SetLineHeight(kTileTextLineHeight);
  106. rating_->SetHorizontalAlignment(gfx::ALIGN_RIGHT);
  107. rating_->SetVisible(false);
  108. rating_star_ = AddChildView(std::make_unique<views::ImageView>());
  109. rating_star_->SetCanProcessEventsWithinSubtree(false);
  110. rating_star_->SetVerticalAlignment(views::ImageView::Alignment::kLeading);
  111. rating_star_->SetImage(gfx::CreateVectorIcon(
  112. kBadgeRatingIcon, kSearchRatingStarSize,
  113. AppListColorProvider::Get()->GetSearchBoxSecondaryTextColor(
  114. gfx::kGoogleGrey700)));
  115. rating_star_->SetVisible(false);
  116. price_ = AddChildView(std::make_unique<views::Label>());
  117. price_->SetEnabledColor(
  118. AppListColorProvider::Get()->GetSearchBoxSecondaryTextColor(
  119. /*default_color*/ gfx::kGoogleGreen600));
  120. price_->SetLineHeight(kTileTextLineHeight);
  121. price_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  122. price_->SetVisible(false);
  123. set_context_menu_controller(this);
  124. }
  125. SearchResultTileItemView::~SearchResultTileItemView() = default;
  126. void SearchResultTileItemView::OnResultChanged() {
  127. // Handle the case where this may be called from a nested run loop while its
  128. // context menu is showing. This cancels the menu (it's for the old item).
  129. context_menu_.reset();
  130. SetVisible(!!result());
  131. if (!result())
  132. return;
  133. SetTitle(result()->title());
  134. SetTitleTags(result()->title_tags());
  135. SetRating(result()->rating());
  136. SetPrice(result()->formatted_price());
  137. const gfx::FontList& font =
  138. SharedAppListConfig::instance().search_result_recommendation_title_font();
  139. if (rating_) {
  140. if (!IsSuggestedAppTile()) {
  141. // App search results use different fonts than AppList apps.
  142. rating_->SetFontList(ui::ResourceBundle::GetSharedInstance().GetFontList(
  143. SharedAppListConfig::instance().search_result_title_font_style()));
  144. } else {
  145. rating_->SetFontList(font);
  146. }
  147. }
  148. if (price_) {
  149. if (!IsSuggestedAppTile()) {
  150. // App search results use different fonts than AppList apps.
  151. price_->SetFontList(ui::ResourceBundle::GetSharedInstance().GetFontList(
  152. SharedAppListConfig::instance().search_result_title_font_style()));
  153. } else {
  154. price_->SetFontList(font);
  155. }
  156. }
  157. if (!IsSuggestedAppTile()) {
  158. // App search results use different fonts than AppList apps.
  159. title_->SetFontList(
  160. ui::ResourceBundle::GetSharedInstance()
  161. .GetFontList(SharedAppListConfig::instance()
  162. .search_result_title_font_style())
  163. .DeriveWithSizeDelta(kSearchResultTileTitleTextSizeDelta));
  164. } else {
  165. title_->SetFontList(font);
  166. }
  167. title_->SetEnabledColor(AppListColorProvider::Get()->GetSearchBoxTextColor(
  168. /*default_color*/ gfx::kGoogleGrey900));
  169. title_->SetMaxLines(2);
  170. title_->SetMultiLine(
  171. (result()->display_type() == SearchResultDisplayType::kTile ||
  172. IsSuggestedAppTile()) &&
  173. (result()->result_type() == AppListSearchResultType::kInstalledApp ||
  174. result()->result_type() == AppListSearchResultType::kArcAppShortcut));
  175. // If the new icon is null, it's being decoded asynchronously. Not updating it
  176. // now to prevent flickering from showing an empty icon while decoding.
  177. if (!result()->icon().icon.isNull())
  178. OnMetadataChanged();
  179. UpdateAccessibleName();
  180. }
  181. void SearchResultTileItemView::SetParentBackgroundColor(SkColor color) {
  182. parent_background_color_ = color;
  183. UpdateBackgroundColor();
  184. }
  185. void SearchResultTileItemView::GetAccessibleNodeData(
  186. ui::AXNodeData* node_data) {
  187. views::Button::GetAccessibleNodeData(node_data);
  188. // The tile is a list item in the search result page's result list.
  189. node_data->role = ax::mojom::Role::kListBoxOption;
  190. node_data->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick);
  191. // Specify |ax::mojom::StringAttribute::kDescription| with an empty string, so
  192. // that long truncated names are not read twice. Details of this issue: - The
  193. // Play Store app's name is shown in a label |title_|. - If the name is too
  194. // long, it'll get truncated and the full name will
  195. // go to the label's tooltip.
  196. // - SearchResultTileItemView uses that label's tooltip as its tooltip.
  197. // - If a view doesn't have |ax::mojom::StringAttribute::kDescription| defined
  198. // in the
  199. // |AXNodeData|, |AXViewObjWrapper::Serialize| will use the tooltip text
  200. // as its description.
  201. // - We're customizing this view's accessible name, so it get focused
  202. // ChromeVox will read its accessible name and then its description.
  203. node_data->AddStringAttribute(ax::mojom::StringAttribute::kDescription, "");
  204. }
  205. bool SearchResultTileItemView::OnKeyPressed(const ui::KeyEvent& event) {
  206. // Return early if |result()| was deleted due to the search result list
  207. // changing. see crbug.com/801142
  208. if (!result())
  209. return true;
  210. if (event.key_code() == ui::VKEY_RETURN) {
  211. ActivateResult(event.flags(), false /* by_button_press */);
  212. return true;
  213. }
  214. return false;
  215. }
  216. void SearchResultTileItemView::StateChanged(ButtonState old_state) {
  217. SearchResultBaseView::StateChanged(old_state);
  218. UpdateBackgroundColor();
  219. }
  220. void SearchResultTileItemView::PaintButtonContents(gfx::Canvas* canvas) {
  221. if (!result() || !selected())
  222. return;
  223. cc::PaintFlags flags;
  224. flags.setAntiAlias(true);
  225. flags.setStyle(cc::PaintFlags::kStroke_Style);
  226. flags.setStrokeWidth(kSelectionRingWidth);
  227. flags.setColor(AppListColorProvider::Get()->GetFocusRingColor());
  228. gfx::RectF selection_ring = GetSelectionRingBounds();
  229. selection_ring.Inset(gfx::InsetsF::VH(kSelectionRingWidth / 2.0, 0));
  230. canvas->DrawRoundRect(selection_ring, kIconSelectedCornerRadius, flags);
  231. }
  232. gfx::RectF SearchResultTileItemView::GetSelectionRingBounds() const {
  233. gfx::RectF bounds(GetContentsBounds());
  234. const float horizontal_padding = (bounds.width() - kIconSelectedSize) / 2.0;
  235. bounds.Inset(gfx::InsetsF::VH(0, horizontal_padding));
  236. bounds.set_height(kIconSelectedSize);
  237. return bounds;
  238. }
  239. void SearchResultTileItemView::OnMetadataChanged() {
  240. SetIcon(result()->icon().icon);
  241. SetTitle(result()->title());
  242. SetTitleTags(result()->title_tags());
  243. SetBadgeIcon(result()->badge_icon(), result()->use_badge_icon_background());
  244. SetRating(result()->rating());
  245. SetPrice(result()->formatted_price());
  246. Layout();
  247. }
  248. void SearchResultTileItemView::ShowContextMenuForViewImpl(
  249. views::View* source,
  250. const gfx::Point& point,
  251. ui::MenuSourceType source_type) {
  252. // |result()| could be null when result list is changing.
  253. if (!result())
  254. return;
  255. view_delegate_->GetSearchResultContextMenuModel(
  256. result()->id(),
  257. base::BindOnce(&SearchResultTileItemView::OnGetContextMenuModel,
  258. weak_ptr_factory_.GetWeakPtr(), source, point,
  259. source_type));
  260. }
  261. void SearchResultTileItemView::OnGetContextMenuModel(
  262. views::View* source,
  263. const gfx::Point& point,
  264. ui::MenuSourceType source_type,
  265. std::unique_ptr<ui::SimpleMenuModel> menu_model) {
  266. if (!menu_model || (context_menu_ && context_menu_->IsShowingMenu()))
  267. return;
  268. // Anchor the menu to the same rect that is used for selection ring.
  269. gfx::Rect anchor_rect = gfx::ToEnclosingRect(GetSelectionRingBounds());
  270. views::View::ConvertRectToScreen(this, &anchor_rect);
  271. AppLaunchedMetricParams metric_params(
  272. AppListLaunchedFrom::kLaunchedFromSearchBox,
  273. AppListLaunchType::kAppSearchResult);
  274. view_delegate_->GetAppLaunchedMetricParams(&metric_params);
  275. context_menu_ = std::make_unique<AppListMenuModelAdapter>(
  276. result()->id(), std::move(menu_model), GetWidget(), source_type,
  277. metric_params, GetAppType(),
  278. base::BindOnce(&SearchResultTileItemView::OnMenuClosed,
  279. weak_ptr_factory_.GetWeakPtr()),
  280. view_delegate_->IsInTabletMode());
  281. context_menu_->Run(anchor_rect, views::MenuAnchorPosition::kBubbleRight,
  282. views::MenuRunner::HAS_MNEMONICS |
  283. views::MenuRunner::USE_ASH_SYS_UI_LAYOUT |
  284. views::MenuRunner::CONTEXT_MENU |
  285. views::MenuRunner::FIXED_ANCHOR);
  286. if (!selected()) {
  287. selected_for_context_menu_ = true;
  288. SetSelected(true, absl::nullopt);
  289. }
  290. }
  291. void SearchResultTileItemView::OnMenuClosed() {
  292. // Release menu since its menu model delegate (AppContextMenu) could be
  293. // released as a result of menu command execution.
  294. context_menu_.reset();
  295. if (selected_for_context_menu_) {
  296. selected_for_context_menu_ = false;
  297. SetSelected(false, absl::nullopt);
  298. }
  299. }
  300. void SearchResultTileItemView::OnButtonPressed(const ui::Event& event) {
  301. ActivateResult(event.flags(), true /* by_button_press */);
  302. }
  303. void SearchResultTileItemView::ActivateResult(int event_flags,
  304. bool by_button_press) {
  305. const bool launch_as_default = is_default_result() && !by_button_press;
  306. if (result()->result_type() == AppListSearchResultType::kPlayStoreApp) {
  307. const base::TimeDelta activation_delay =
  308. base::TimeTicks::Now() - result_display_start_time();
  309. UMA_HISTOGRAM_MEDIUM_TIMES("Arc.PlayStoreSearch.ResultClickLatency",
  310. activation_delay);
  311. UMA_HISTOGRAM_EXACT_LINEAR(
  312. "Apps.AppListPlayStoreAppLaunchedIndex",
  313. group_index_in_container_view(),
  314. SharedAppListConfig::instance().max_search_result_tiles());
  315. if (launch_as_default) {
  316. UMA_HISTOGRAM_MEDIUM_TIMES(
  317. "Arc.PlayStoreSearch.DefaultResultClickLatency", activation_delay);
  318. }
  319. }
  320. LogAppLaunchForSuggestedApp();
  321. RecordSearchResultOpenSource(result(), view_delegate_->GetAppListViewState(),
  322. view_delegate_->IsInTabletMode());
  323. view_delegate_->OpenSearchResult(result()->id(), event_flags,
  324. AppListLaunchedFrom::kLaunchedFromSearchBox,
  325. AppListLaunchType::kAppSearchResult,
  326. index_in_container(), launch_as_default);
  327. }
  328. void SearchResultTileItemView::SetIcon(const gfx::ImageSkia& icon) {
  329. const int icon_size =
  330. SharedAppListConfig::instance().search_tile_icon_dimension();
  331. gfx::ImageSkia resized(gfx::ImageSkiaOperations::CreateResizedImage(
  332. icon, skia::ImageOperations::RESIZE_BEST,
  333. gfx::Size(icon_size, icon_size)));
  334. icon_->SetImage(resized);
  335. }
  336. void SearchResultTileItemView::SetBadgeIcon(const ui::ImageModel& badge_icon,
  337. bool use_badge_icon_background) {
  338. if (badge_icon.IsEmpty()) {
  339. badge_->SetVisible(false);
  340. return;
  341. }
  342. gfx::ImageSkia badge_icon_skia = badge_icon.Rasterize(GetColorProvider());
  343. if (use_badge_icon_background) {
  344. badge_icon_skia = CreateIconWithCircleBackground(badge_icon_skia);
  345. }
  346. gfx::ImageSkia resized_badge_icon(
  347. gfx::ImageSkiaOperations::CreateResizedImage(
  348. badge_icon_skia, skia::ImageOperations::RESIZE_BEST,
  349. SharedAppListConfig::instance().search_tile_badge_icon_size()));
  350. gfx::ShadowValues shadow_values;
  351. shadow_values.push_back(
  352. gfx::ShadowValue(gfx::Vector2d(0, kBadgeIconShadowWidth), 0,
  353. SkColorSetARGB(0x33, 0, 0, 0)));
  354. shadow_values.push_back(
  355. gfx::ShadowValue(gfx::Vector2d(0, kBadgeIconShadowWidth), 2,
  356. SkColorSetARGB(0x33, 0, 0, 0)));
  357. badge_->SetImage(gfx::ImageSkiaOperations::CreateImageWithDropShadow(
  358. resized_badge_icon, shadow_values));
  359. badge_->SetVisible(true);
  360. }
  361. void SearchResultTileItemView::SetTitle(const std::u16string& title) {
  362. title_->SetText(title);
  363. }
  364. void SearchResultTileItemView::SetTitleTags(const SearchResultTags& tags) {
  365. for (const auto& tag : tags) {
  366. if (tag.styles & SearchResult::Tag::MATCH) {
  367. title_->SetTextStyleRange(views::style::STYLE_EMPHASIZED, tag.range);
  368. }
  369. }
  370. }
  371. void SearchResultTileItemView::SetRating(float rating) {
  372. if (!rating_)
  373. return;
  374. if (rating < 0) {
  375. rating_->SetVisible(false);
  376. rating_star_->SetVisible(false);
  377. return;
  378. }
  379. rating_->SetText(base::FormatDouble(rating, 1));
  380. rating_->SetVisible(true);
  381. rating_star_->SetVisible(true);
  382. }
  383. void SearchResultTileItemView::SetPrice(const std::u16string& price) {
  384. if (!price_)
  385. return;
  386. if (price.empty()) {
  387. price_->SetVisible(false);
  388. return;
  389. }
  390. price_->SetText(price);
  391. price_->SetVisible(true);
  392. }
  393. AppListMenuModelAdapter::AppListViewAppType
  394. SearchResultTileItemView::GetAppType() const {
  395. if (IsSuggestedAppTile()) {
  396. if (view_delegate_->GetAppListViewState() == AppListViewState::kPeeking) {
  397. return AppListMenuModelAdapter::PEEKING_SUGGESTED;
  398. } else {
  399. return AppListMenuModelAdapter::FULLSCREEN_SUGGESTED;
  400. }
  401. } else {
  402. if (view_delegate_->GetAppListViewState() == AppListViewState::kHalf) {
  403. return AppListMenuModelAdapter::HALF_SEARCH_RESULT;
  404. } else if (view_delegate_->GetAppListViewState() ==
  405. AppListViewState::kFullscreenSearch) {
  406. return AppListMenuModelAdapter::FULLSCREEN_SEARCH_RESULT;
  407. }
  408. }
  409. NOTREACHED();
  410. return AppListMenuModelAdapter::APP_LIST_APP_TYPE_LAST;
  411. }
  412. bool SearchResultTileItemView::IsSuggestedAppTile() const {
  413. return result() && result()->is_recommendation();
  414. }
  415. void SearchResultTileItemView::LogAppLaunchForSuggestedApp() const {
  416. // Only log the app launch if the class is being used as a suggested app.
  417. if (!IsSuggestedAppTile())
  418. return;
  419. // We only need to record opening the installed app in zero state, no need to
  420. // record the opening of a fast re-installed app, since the latter is already
  421. // recorded in ArcAppReinstallAppResult::Open.
  422. if (result()->result_type() !=
  423. AppListSearchResultType::kPlayStoreReinstallApp) {
  424. base::RecordAction(
  425. base::UserMetricsAction("AppList_ZeroStateOpenInstalledApp"));
  426. }
  427. }
  428. void SearchResultTileItemView::UpdateBackgroundColor() {
  429. // Tell the label what color it will be drawn onto. It will use whether the
  430. // background color is opaque or transparent to decide whether to use subpixel
  431. // rendering. Does not actually set the label's background color.
  432. title_->SetBackgroundColor(parent_background_color_);
  433. SchedulePaint();
  434. }
  435. void SearchResultTileItemView::Layout() {
  436. gfx::Rect rect(GetContentsBounds());
  437. if (rect.IsEmpty() || !result())
  438. return;
  439. gfx::Rect icon_rect(rect);
  440. icon_rect.ClampToCenteredSize(icon_->GetImage().size());
  441. icon_rect.set_y(kSearchTileTopPadding);
  442. icon_->SetBoundsRect(icon_rect);
  443. const int badge_icon_dimension =
  444. SharedAppListConfig::instance().search_tile_badge_icon_dimension() +
  445. 2 * kBadgeIconShadowWidth;
  446. const int badge_icon_offset =
  447. SharedAppListConfig::instance().search_tile_badge_icon_offset();
  448. const gfx::Rect badge_rect(
  449. icon_rect.right() - badge_icon_dimension + badge_icon_offset,
  450. icon_rect.bottom() - badge_icon_dimension + badge_icon_offset,
  451. badge_icon_dimension, badge_icon_dimension);
  452. badge_->SetBoundsRect(badge_rect);
  453. rect.set_y(icon_rect.bottom() + kSearchTitleSpacing);
  454. rect.set_height(title_->GetPreferredSize().height());
  455. title_->SetBoundsRect(rect);
  456. // If there is no price set, we center the rating.
  457. const bool center_rating =
  458. rating_ && rating_star_ && price_ && price_->GetText().empty();
  459. const int rating_horizontal_offset =
  460. center_rating ? kSearchRatingCenteringOffset : 0;
  461. if (rating_) {
  462. gfx::Rect rating_rect(rect);
  463. rating_rect.Inset(gfx::Insets::TLBR(title_->GetPreferredSize().height(),
  464. rating_horizontal_offset, 0, 0));
  465. rating_rect.set_size(rating_->GetPreferredSize());
  466. rating_rect.set_width(kSearchRatingSize);
  467. rating_->SetBoundsRect(rating_rect);
  468. }
  469. if (rating_star_) {
  470. gfx::Rect rating_star_rect(rect);
  471. rating_star_rect.Inset(gfx::Insets::TLBR(
  472. title_->GetPreferredSize().height() + kSearchRatingStarVerticalSpacing,
  473. rating_horizontal_offset + kSearchRatingSize +
  474. kSearchRatingStarHorizontalSpacing,
  475. 0, 0));
  476. rating_star_rect.set_size(rating_star_->GetPreferredSize());
  477. rating_star_->SetBoundsRect(rating_star_rect);
  478. }
  479. if (price_) {
  480. gfx::Rect price_rect(rect);
  481. price_rect.Inset(gfx::Insets::TLBR(title_->GetPreferredSize().height(),
  482. rect.width() - kSearchPriceSize, 0, 0));
  483. price_rect.set_size(price_->GetPreferredSize());
  484. price_->SetBoundsRect(price_rect);
  485. }
  486. }
  487. const char* SearchResultTileItemView::GetClassName() const {
  488. return "SearchResultTileItemView";
  489. }
  490. gfx::Size SearchResultTileItemView::CalculatePreferredSize() const {
  491. if (!result())
  492. return gfx::Size();
  493. return gfx::Size(kSearchTileWidth,
  494. SharedAppListConfig::instance().search_tile_height());
  495. }
  496. std::u16string SearchResultTileItemView::GetTooltipText(
  497. const gfx::Point& p) const {
  498. // Use the label to generate a tooltip, so that it will consider its text
  499. // truncation in making the tooltip. We do not want the label itself to have a
  500. // tooltip, so we only temporarily enable it to get the tooltip text from the
  501. // label, then disable it again.
  502. title_->SetHandlesTooltips(true);
  503. std::u16string tooltip = title_->GetTooltipText(p);
  504. title_->SetHandlesTooltips(false);
  505. return tooltip;
  506. }
  507. } // namespace ash