holding_space_drag_util.cc 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  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. #include "ash/system/holding_space/holding_space_drag_util.h"
  5. #include <memory>
  6. #include "ash/bubble/bubble_utils.h"
  7. #include "ash/constants/ash_features.h"
  8. #include "ash/public/cpp/holding_space/holding_space_image.h"
  9. #include "ash/public/cpp/holding_space/holding_space_item.h"
  10. #include "ash/public/cpp/rounded_image_view.h"
  11. #include "ash/public/cpp/style/scoped_light_mode_as_default.h"
  12. #include "ash/style/ash_color_provider.h"
  13. #include "ash/style/dark_light_mode_controller_impl.h"
  14. #include "ash/system/holding_space/holding_space_item_view.h"
  15. #include "base/containers/adapters.h"
  16. #include "base/i18n/rtl.h"
  17. #include "ui/compositor/canvas_painter.h"
  18. #include "ui/compositor/compositor.h"
  19. #include "ui/gfx/canvas.h"
  20. #include "ui/gfx/image/image_skia.h"
  21. #include "ui/gfx/shadow_util.h"
  22. #include "ui/gfx/skia_paint_util.h"
  23. #include "ui/views/background.h"
  24. #include "ui/views/controls/label.h"
  25. #include "ui/views/drag_utils.h"
  26. #include "ui/views/layout/box_layout.h"
  27. #include "ui/views/layout/fill_layout.h"
  28. #include "ui/views/layout/layout_manager_base.h"
  29. #include "ui/views/view.h"
  30. #include "ui/views/widget/widget.h"
  31. namespace ash {
  32. namespace holding_space_util {
  33. namespace {
  34. // Appearance.
  35. constexpr int kDragImageItemViewCornerRadius = 8;
  36. constexpr int kDragImageItemViewElevation = 2;
  37. constexpr int kDragImageItemChipViewIconSize = 24;
  38. constexpr auto kDragImageItemChipViewInsets = gfx::Insets::TLBR(8, 8, 8, 12);
  39. constexpr gfx::Size kDragImageItemChipViewPreferredSize(160, 40);
  40. constexpr int kDragImageItemChipViewSpacing = 8;
  41. constexpr gfx::Size kDragImageItemScreenCaptureViewPreferredSize(104, 80);
  42. constexpr auto kDragImageOverflowBadgeInsets = gfx::Insets::VH(0, 8);
  43. constexpr gfx::Size kDragImageOverflowBadgeMinimumSize(24, 24);
  44. constexpr int kDragImageViewChildOffset = 8;
  45. // The maximum number of items to paint to the drag image. If more items exist
  46. // they will be represented by an overflow badge.
  47. constexpr size_t kDragImageViewMaxItemsToPaint = 2;
  48. // Helpers ---------------------------------------------------------------------
  49. #if DCHECK_IS_ON()
  50. // Asserts that there are no `ui::Layer`s in the specified `view` hierarchy.
  51. void AssertNoLayers(const views::View* view) {
  52. DCHECK(!view->layer());
  53. for (const views::View* child : view->children())
  54. AssertNoLayers(child);
  55. }
  56. #endif // DCHECK_IS_ON()
  57. // Returns the holding space items associated with the specified `views`.
  58. std::vector<const HoldingSpaceItem*> GetHoldingSpaceItems(
  59. const std::vector<const HoldingSpaceItemView*> views) {
  60. std::vector<const HoldingSpaceItem*> items;
  61. for (const HoldingSpaceItemView* view : views)
  62. items.push_back(view->item());
  63. return items;
  64. }
  65. // DragImageLayoutManager ------------------------------------------------------
  66. // A `views::LayoutManager` which lays out its children atop each other with a
  67. // specified `child_offset`. Note that children are painted in reverse order.
  68. class DragImageLayoutManager : public views::LayoutManagerBase {
  69. public:
  70. explicit DragImageLayoutManager(int child_offset)
  71. : child_offset_(child_offset) {}
  72. DragImageLayoutManager(const DragImageLayoutManager&) = delete;
  73. DragImageLayoutManager& operator=(const DragImageLayoutManager&) = delete;
  74. ~DragImageLayoutManager() override = default;
  75. private:
  76. // views::LayoutManagerBase:
  77. views::ProposedLayout CalculateProposedLayout(
  78. const views::SizeBounds& size_bounds) const override {
  79. views::ProposedLayout proposed_layout;
  80. int left = 0, top = 0;
  81. for (views::View* child_view : host_view()->children()) {
  82. const gfx::Size child_preferred_size = child_view->GetPreferredSize();
  83. // Child layout.
  84. views::ChildLayout child_layout;
  85. child_layout.available_size = views::SizeBounds(child_preferred_size);
  86. child_layout.bounds = gfx::Rect({left, top}, child_preferred_size);
  87. child_layout.child_view = child_view;
  88. child_layout.visible = true;
  89. proposed_layout.child_layouts.push_back(std::move(child_layout));
  90. // Host size.
  91. if (proposed_layout.host_size.IsEmpty()) {
  92. proposed_layout.host_size = child_preferred_size;
  93. } else {
  94. int host_width = left + child_preferred_size.width();
  95. int host_height = top + child_preferred_size.height();
  96. proposed_layout.host_size.SetToMax(gfx::Size(host_width, host_height));
  97. }
  98. left += child_offset_;
  99. top += child_offset_;
  100. }
  101. return proposed_layout;
  102. }
  103. std::vector<views::View*> GetChildViewsInPaintOrder(
  104. const views::View* host) const override {
  105. // Paint `children` in reverse order so that earlier views paint at a higher
  106. // z-index than later views, like a deck of cards with the first `child`
  107. // stacked on top.
  108. std::vector<views::View*> children;
  109. for (views::View* child : base::Reversed(host->children()))
  110. children.push_back(child);
  111. return children;
  112. }
  113. const int child_offset_;
  114. };
  115. // DragImageItemView -----------------------------------------------------------
  116. // An abstract `views::View` which represents a single holding space item in the
  117. // drag image for a collection of holding space item views. The main purpose of
  118. // this view is to implement the shadow which is intentionally done without use
  119. // of `ui::Layer`s to accommodate painting to an `SkBitmap`.
  120. class DragImageItemView : public views::View {
  121. public:
  122. DragImageItemView(const DragImageItemView&) = delete;
  123. DragImageItemView& operator=(const DragImageItemView&) = delete;
  124. ~DragImageItemView() override = default;
  125. protected:
  126. DragImageItemView() = default;
  127. // views::View:
  128. gfx::Insets GetInsets() const final {
  129. // Add insets to accommodate the shadow so that the view's content will be
  130. // laid out within the appropriate shadow margins.
  131. return gfx::Insets(-gfx::ShadowValue::GetMargin(GetShadowDetails().values));
  132. }
  133. void OnPaintBackground(gfx::Canvas* canvas) override {
  134. // NOTE: The contents bounds are shrunk by a single pixel to avoid
  135. // painting the background outside content bounds as might otherwise occur
  136. // due to pixel rounding. Failure to do so could result in paint artifacts.
  137. gfx::RectF bounds(GetContentsBounds());
  138. bounds.Inset(gfx::InsetsF(0.5f));
  139. // NOTE: Background is white when the dark/light mode feature is disabled.
  140. cc::PaintFlags flags;
  141. flags.setAntiAlias(true);
  142. flags.setColor(features::IsDarkLightModeEnabled()
  143. ? AshColorProvider::Get()->GetBaseLayerColor(
  144. AshColorProvider::BaseLayerType::kOpaque)
  145. : SK_ColorWHITE);
  146. flags.setLooper(gfx::CreateShadowDrawLooper(GetShadowDetails().values));
  147. canvas->DrawRoundRect(bounds, kDragImageItemViewCornerRadius, flags);
  148. }
  149. private:
  150. const gfx::ShadowDetails& GetShadowDetails() const {
  151. return gfx::ShadowDetails::Get(kDragImageItemViewElevation,
  152. kDragImageItemViewCornerRadius);
  153. }
  154. };
  155. // DragImageItemChipView -------------------------------------------------------
  156. // A `DragImageItemView` which represents a single holding space `item` as a
  157. // chip in the drag image for a collection of holding space item views.
  158. class DragImageItemChipView : public DragImageItemView {
  159. public:
  160. explicit DragImageItemChipView(const HoldingSpaceItem* item) {
  161. InitLayout(item);
  162. }
  163. private:
  164. void InitLayout(const HoldingSpaceItem* item) {
  165. // NOTE: Enlarge `preferred_size` to accommodate the view's shadow.
  166. gfx::Size preferred_size(kDragImageItemChipViewPreferredSize);
  167. preferred_size.Enlarge(GetInsets().width(), GetInsets().height());
  168. SetPreferredSize(preferred_size);
  169. // Layout.
  170. views::BoxLayout* layout =
  171. SetLayoutManager(std::make_unique<views::BoxLayout>(
  172. views::BoxLayout::Orientation::kHorizontal,
  173. kDragImageItemChipViewInsets, kDragImageItemChipViewSpacing));
  174. layout->set_cross_axis_alignment(
  175. views::BoxLayout::CrossAxisAlignment::kCenter);
  176. layout->set_main_axis_alignment(
  177. views::BoxLayout::MainAxisAlignment::kCenter);
  178. // Icon.
  179. auto* icon = AddChildView(std::make_unique<RoundedImageView>(
  180. /*radius=*/kDragImageItemChipViewIconSize / 2,
  181. RoundedImageView::Alignment::kCenter));
  182. icon->SetPreferredSize(gfx::Size(kDragImageItemChipViewIconSize,
  183. kDragImageItemChipViewIconSize));
  184. // NOTE: The view's background is white when the dark/light mode feature is
  185. // disabled. Otherwise, the view's background depends on theming.
  186. icon->SetImage(item->image().GetImageSkia(
  187. icon->GetPreferredSize(),
  188. /*dark_background=*/features::IsDarkLightModeEnabled() &&
  189. DarkLightModeControllerImpl::Get()->IsDarkModeEnabled()));
  190. // Label.
  191. ScopedLightModeAsDefault scoped_light_mode;
  192. auto* label = AddChildView(bubble_utils::CreateLabel(
  193. bubble_utils::LabelStyle::kChipTitle, item->GetText()));
  194. label->SetElideBehavior(gfx::ElideBehavior::ELIDE_MIDDLE);
  195. label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
  196. layout->SetFlexForView(label, 1);
  197. }
  198. };
  199. // DragImageItemScreenCaptureView ----------------------------------------------
  200. // A `DragImageItemView` which represents a single holding space screen capture
  201. // `item` in the drag image for a collection of holding space item views.
  202. class DragImageItemScreenCaptureView : public DragImageItemView {
  203. public:
  204. explicit DragImageItemScreenCaptureView(const HoldingSpaceItem* item) {
  205. DCHECK(item->IsScreenCapture());
  206. InitLayout(item);
  207. }
  208. private:
  209. void InitLayout(const HoldingSpaceItem* item) {
  210. // NOTE: Enlarge `preferred_size` to accommodate the view's shadow.
  211. gfx::Size preferred_size(kDragImageItemScreenCaptureViewPreferredSize);
  212. preferred_size.Enlarge(GetInsets().width(), GetInsets().height());
  213. SetPreferredSize(preferred_size);
  214. // Layout.
  215. SetLayoutManager(std::make_unique<views::FillLayout>());
  216. // Image.
  217. auto* image = AddChildView(std::make_unique<RoundedImageView>(
  218. kDragImageItemViewCornerRadius, RoundedImageView::Alignment::kCenter));
  219. image->SetPreferredSize(kDragImageItemScreenCaptureViewPreferredSize);
  220. // NOTE: The view's background is white when the dark/light mode feature is
  221. // disabled. Otherwise, the view's background depends on theming.
  222. image->SetImage(item->image().GetImageSkia(
  223. image->GetPreferredSize(),
  224. /*dark_background=*/features::IsDarkLightModeEnabled() &&
  225. DarkLightModeControllerImpl::Get()->IsDarkModeEnabled()));
  226. }
  227. };
  228. // DragImageOverflowBadge ------------------------------------------------------
  229. // A `views::View` which indicates the number of items being dragged in the
  230. // drag image for a collection of holding space items. This view is only created
  231. // if the number of dragged items is > `kDragImageViewMaxItemsToPaint`.
  232. class DragImageOverflowBadge : public views::View {
  233. public:
  234. explicit DragImageOverflowBadge(size_t count) {
  235. DCHECK_GT(count, kDragImageViewMaxItemsToPaint);
  236. InitLayout(count);
  237. }
  238. DragImageOverflowBadge(const DragImageOverflowBadge&) = delete;
  239. DragImageOverflowBadge& operator=(const DragImageOverflowBadge&) = delete;
  240. ~DragImageOverflowBadge() override = default;
  241. private:
  242. // views::View:
  243. gfx::Size CalculatePreferredSize() const override {
  244. gfx::Size preferred_size = views::View::CalculatePreferredSize();
  245. preferred_size.SetToMax(kDragImageOverflowBadgeMinimumSize);
  246. return preferred_size;
  247. }
  248. void InitLayout(size_t count) {
  249. // NOTE: If the dark/light mode feature is disabled, the overflow badge
  250. // should use light mode to be consistent with the `DragItemImageView`s.
  251. ScopedLightModeAsDefault scoped_light_mode;
  252. // Background.
  253. SetBackground(views::CreateRoundedRectBackground(
  254. AshColorProvider::Get()->GetControlsLayerColor(
  255. AshColorProvider::ControlsLayerType::kFocusRingColor),
  256. /*radius=*/kDragImageOverflowBadgeMinimumSize.height() / 2));
  257. // Layout.
  258. auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
  259. views::BoxLayout::Orientation::kHorizontal,
  260. kDragImageOverflowBadgeInsets));
  261. layout->set_cross_axis_alignment(
  262. views::BoxLayout::CrossAxisAlignment::kCenter);
  263. layout->set_main_axis_alignment(
  264. views::BoxLayout::MainAxisAlignment::kCenter);
  265. // Label.
  266. auto* label = AddChildView(
  267. bubble_utils::CreateLabel(bubble_utils::LabelStyle::kBadge));
  268. label->SetEnabledColor(
  269. DarkLightModeControllerImpl::Get()->IsDarkModeEnabled()
  270. ? gfx::kGoogleGrey900
  271. : gfx::kGoogleGrey200);
  272. label->SetText(base::UTF8ToUTF16(base::NumberToString(count)));
  273. }
  274. };
  275. // DragImageView ---------------------------------------------------------------
  276. // A `views::View` for use as a drag image for a collection of holding space
  277. // item `views`. This view expects to be painted to an `SkBitmap`.
  278. class DragImageView : public views::View {
  279. public:
  280. explicit DragImageView(const std::vector<const HoldingSpaceItem*>& items) {
  281. InitLayout(items);
  282. }
  283. DragImageView(const DragImageView&) = delete;
  284. DragImageView& operator=(const DragImageView&) = delete;
  285. ~DragImageView() override = default;
  286. // Paints this view to a `gfx::ImageSkia` for use as a drag image.
  287. gfx::ImageSkia GetDragImage(float scale, bool is_pixel_canvas) {
  288. #if DCHECK_IS_ON()
  289. // NOTE: This method will *not* paint `ui::Layer`s, so it is expected that
  290. // all views in this view hierarchy *not* paint to layers.
  291. AssertNoLayers(this);
  292. #endif // DCHECK_IS_ON()
  293. SkBitmap bitmap;
  294. Paint(views::PaintInfo::CreateRootPaintInfo(
  295. ui::CanvasPainter(&bitmap, size(), scale,
  296. /*clear_color=*/SK_ColorTRANSPARENT, is_pixel_canvas)
  297. .context(),
  298. size()));
  299. return gfx::ImageSkia::CreateFromBitmap(bitmap, scale);
  300. }
  301. // Returns the drag offset to use when rendering this view as a drag image.
  302. // This offset will position the cursor directly over the top left hand corner
  303. // of the first dragged item (or flipped for RTL).
  304. gfx::Vector2d GetDragOffset() const {
  305. DCHECK(first_drag_image_item_view_);
  306. const gfx::Rect contents_bounds =
  307. first_drag_image_item_view_->GetContentsBounds();
  308. // Use the contents origin of the first dragged item instead of its local
  309. // bounds origin to exclude the region reserved for its shadow margins.
  310. gfx::Point contents_origin = contents_bounds.origin();
  311. views::View::ConvertPointToTarget(first_drag_image_item_view_->parent(),
  312. /*target=*/this, &contents_origin);
  313. gfx::Vector2d drag_offset = contents_origin.OffsetFromOrigin();
  314. // In RTL, its necessary to offset by the contents width of the first
  315. // dragged item so that the cursor is positioned over its top right hand
  316. // corner. Again, contents width is used instead of local bounds width to
  317. // exclude shadow margins.
  318. if (base::i18n::IsRTL())
  319. drag_offset += gfx::Vector2d(contents_bounds.width(), 0);
  320. return drag_offset;
  321. }
  322. private:
  323. // views::View:
  324. gfx::Insets GetInsets() const override {
  325. if (!drag_image_overflow_badge_)
  326. return gfx::Insets();
  327. // When the number of dragged items is > `kDragImageViewMaxItemsToPaint`,
  328. // add insets in which to layout `drag_image_overflow_badge_`. Note that
  329. // because the badge is centered at the top right hand corner of the
  330. // `first_drag_image_item_view_`, half of the badge will be positioned
  331. // within contents bounds so only half of the badge's preferred `size` needs
  332. // to be added as insets.
  333. gfx::Size size = drag_image_overflow_badge_->GetPreferredSize();
  334. return gfx::Insets::TLBR(size.height() / 2, 0, 0, size.width() / 2);
  335. }
  336. void Layout() override {
  337. views::View::Layout();
  338. if (!drag_image_overflow_badge_)
  339. return;
  340. DCHECK(first_drag_image_item_view_);
  341. // Manually position `drag_image_overflow_badge_` to be centered at the top
  342. // right hand corner of the `first_drag_image_item_view_`.
  343. const gfx::Size badge_size = drag_image_overflow_badge_->GetPreferredSize();
  344. const gfx::Point badge_origin =
  345. first_drag_image_item_view_->GetContentsBounds().top_right() -
  346. gfx::Vector2d(badge_size.width() / 2, 0);
  347. drag_image_overflow_badge_->SetBoundsRect(
  348. gfx::Rect(badge_origin, badge_size));
  349. }
  350. void InitLayout(const std::vector<const HoldingSpaceItem*>& items) {
  351. auto* layout = SetLayoutManager(std::make_unique<views::FillLayout>());
  352. AddDragImageItemViews(items);
  353. AddDragImageOverflowBadge(layout, items.size());
  354. }
  355. void AddDragImageItemViews(
  356. const std::vector<const HoldingSpaceItem*>& items) {
  357. auto* container = AddChildView(std::make_unique<views::View>());
  358. container->SetLayoutManager(
  359. std::make_unique<DragImageLayoutManager>(kDragImageViewChildOffset));
  360. const bool contains_only_screen_captures = std::all_of(
  361. items.begin(), items.end(),
  362. [](const HoldingSpaceItem* item) { return item->IsScreenCapture(); });
  363. // Show at most `kDragImageViewMaxItemsToPaint` items in the drag image. If
  364. // more items exist, `drag_image_overflow_badge_` will be added to indicate
  365. // the total number of dragged items.
  366. const size_t count = std::min(items.size(), kDragImageViewMaxItemsToPaint);
  367. for (size_t i = 0; i < count; ++i) {
  368. if (contains_only_screen_captures) {
  369. container->AddChildView(
  370. std::make_unique<DragImageItemScreenCaptureView>(items[i]));
  371. } else {
  372. container->AddChildView(
  373. std::make_unique<DragImageItemChipView>(items[i]));
  374. }
  375. }
  376. // Cache the first `DragImageItemView` so `drag_image_overflow_badge_` can
  377. // be relatively positioned if `kDragImageViewMaxItemsToPaint` is met.
  378. DCHECK(!container->children().empty());
  379. first_drag_image_item_view_ = container->children()[0];
  380. }
  381. void AddDragImageOverflowBadge(views::FillLayout* layout, size_t count) {
  382. if (count <= kDragImageViewMaxItemsToPaint)
  383. return;
  384. drag_image_overflow_badge_ =
  385. AddChildView(std::make_unique<DragImageOverflowBadge>(count));
  386. // This view's `layout` manager ignores `drag_image_overflow_badge_` as it
  387. // is manually positioned relative to the `first_drag_image_item_view_`.
  388. layout->SetChildViewIgnoredByLayout(drag_image_overflow_badge_, true);
  389. }
  390. views::View* first_drag_image_item_view_ = nullptr;
  391. views::View* drag_image_overflow_badge_ = nullptr;
  392. };
  393. } // namespace
  394. // Utilities -------------------------------------------------------------------
  395. void CreateDragImage(const std::vector<const HoldingSpaceItemView*>& views,
  396. gfx::ImageSkia* drag_image,
  397. gfx::Vector2d* drag_offset) {
  398. if (views.empty()) {
  399. *drag_image = gfx::ImageSkia();
  400. *drag_offset = gfx::Vector2d();
  401. return;
  402. }
  403. const views::Widget* widget = views[0]->GetWidget();
  404. const float scale = views::ScaleFactorForDragFromWidget(widget);
  405. const bool is_pixel_canvas = widget->GetCompositor()->is_pixel_canvas();
  406. DragImageView drag_image_view(GetHoldingSpaceItems(views));
  407. drag_image_view.SetSize(drag_image_view.GetPreferredSize());
  408. *drag_image = drag_image_view.GetDragImage(scale, is_pixel_canvas);
  409. *drag_offset = drag_image_view.GetDragOffset();
  410. }
  411. } // namespace holding_space_util
  412. } // namespace ash