chromium/ash/wm/overview/birch/tab_app_selection_view.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/wm/overview/birch/tab_app_selection_view.h"

#include "ash/public/cpp/saved_desk_delegate.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/style/close_button.h"
#include "ash/style/typography.h"
#include "base/task/cancelable_task_tracker.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/canvas.h"
#include "ui/views/background.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/highlight_border.h"
#include "ui/views/view_class_properties.h"

namespace ash {

namespace {

// TODO(http://b/361326120): The below are hardcoded temporary values.
constexpr int kScrollViewMaxHeight = 400;

constexpr int kItemChildSpacing = 16;
constexpr gfx::Insets kItemInsets = gfx::Insets::VH(8, 16);
constexpr int kImageSize = 20;
constexpr gfx::Size kImagePreferredSize(20, 20);

constexpr gfx::Insets kContentsInsets = gfx::Insets::VH(8, 0);

constexpr gfx::RoundedCornersF kContainerCornerRadius(20.f, 20.f, 0.f, 0.f);

constexpr gfx::Insets kSubtitleMargins = gfx::Insets::VH(8, 16);

std::unique_ptr<views::Label> CreateSubtitle(const std::u16string& text) {
  return views::Builder<views::Label>()
      .SetText(text)
      .SetHorizontalAlignment(gfx::ALIGN_LEFT)
      .SetEnabledColorId(cros_tokens::kCrosSysOnSurface)
      .SetProperty(views::kMarginsKey, kSubtitleMargins)
      .CustomConfigure(base::BindOnce([](views::Label* label) {
        TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton1,
                                              *label);
      }))
      .Build();
}

// Represents either a tab that will be moved into a new browser on a new desk
// or an app that will be moved to the new desk.
//
//   +-------------------------------------------+
//   |  +---+   +-----------------------+  +---+ |
//   |  |   |   |                       |  |   | |
//   |  +---+   +-----------------------+  +---+ |
//   +--^---------------^------------------^-----+
//   ^  |               |                  |
//   |  `ImageView`     |                  `CloseButton` (Visible on hover)
//   |                  `Label`
//   |
//   `TabAppSelectionItemView`
class TabAppSelectionItemView : public views::BoxLayoutView {
  METADATA_HEADER(TabAppSelectionItemView, views::BoxLayoutView)

 public:
  struct InitParams {
    InitParams() = default;
    InitParams(const InitParams&) = delete;
    InitParams& operator=(const InitParams&) = delete;
    ~InitParams() = default;

    InitParams(InitParams&& other) = default;

    enum class Type {
      kTab,
      kApp,
    } type;

    // For tabs, `identifier` is an url spec. For apps, its the app id. These
    // will use the favicon and app services to fetch the favicon and app icon.
    std::string identifier;

    using CloseCallback = base::OnceCallback<void(views::View*)>;
    CloseCallback close_callback;
  };

  explicit TabAppSelectionItemView(InitParams params) {
    views::Builder<views::BoxLayoutView>(this)
        .SetAccessibleRole(ax::mojom::Role::kMenuItem)
        .SetAccessibleName(u"TempAccessibleName")
        .SetBetweenChildSpacing(kItemChildSpacing)
        .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
        .SetFocusBehavior(views::View::FocusBehavior::ALWAYS)
        .SetInsideBorderInsets(kItemInsets)
        .SetNotifyEnterExitOnChild(true)
        .SetOrientation(views::LayoutOrientation::kHorizontal)
        .AddChildren(
            views::Builder<views::ImageView>()
                .CopyAddressTo(&image_)
                .SetImage(ui::ImageModel::FromVectorIcon(
                    kDefaultAppIcon, cros_tokens::kCrosSysOnPrimary))
                .SetImageSize(gfx::Size(kImageSize, kImageSize))
                .SetPreferredSize(kImagePreferredSize),
            views::Builder<views::Label>()
                .SetText(u"Title")
                .SetHorizontalAlignment(gfx::ALIGN_LEFT)
                .SetProperty(views::kBoxLayoutFlexKey,
                             views::BoxLayoutFlexSpecification())
                .CustomConfigure(base::BindOnce([](views::Label* label) {
                  TypographyProvider::Get()->StyleLabel(
                      TypographyToken::kCrosButton2, *label);
                })))
        .BuildChildren();

    close_button_ = AddChildView(std::make_unique<CloseButton>(
        base::BindOnce(
            [](const base::WeakPtr<TabAppSelectionItemView>& item_view,
               InitParams::CloseCallback callback) {
              if (item_view) {
                std::move(callback).Run(item_view.get());
              }
            },
            weak_ptr_factory_.GetWeakPtr(), std::move(params.close_callback)),
        CloseButton::Type::kMediumFloating));
    close_button_->SetVisible(false);

    auto* delegate = Shell::Get()->saved_desk_delegate();
    auto set_icon_image_callback = base::BindOnce(
        [](const base::WeakPtr<TabAppSelectionItemView>& item_view,
           const gfx::ImageSkia& favicon) {
          if (item_view) {
            item_view->image_->SetImage(favicon);
          }
        },
        weak_ptr_factory_.GetWeakPtr());

    switch (params.type) {
      case InitParams::Type::kTab: {
        delegate->GetFaviconForUrl(params.identifier, /*lacros_profile_id=*/0,
                                   std::move(set_icon_image_callback),
                                   &cancelable_favicon_task_tracker_);
        return;
      }
      case InitParams::Type::kApp: {
        //  The callback may be called synchronously.
        delegate->GetIconForAppId(params.identifier, kImageSize,
                                  std::move(set_icon_image_callback));
        return;
      }
    }
  }
  TabAppSelectionItemView(const TabAppSelectionItemView&) = delete;
  TabAppSelectionItemView& operator=(const TabAppSelectionItemView&) = delete;
  ~TabAppSelectionItemView() override = default;

  // views::BoxLayoutView:
  void OnMouseEntered(const ui::MouseEvent& event) override {
    SetSelected(true);
  }
  void OnMouseExited(const ui::MouseEvent& event) override {
    SetSelected(false);
  }
  void OnFocus() override { SetSelected(true); }
  void OnBlur() override { SetSelected(false); }

 private:
  void SetSelected(bool selected) {
    if (selected_ == selected) {
      return;
    }

    selected_ = selected;
    close_button_->SetVisible(selected);
    SetBackground(selected_ ? views::CreateThemedSolidBackground(
                                  cros_tokens::kCrosSysHoverOnSubtle)
                            : nullptr);
  }

  // True when the mouse is hovered over this view. The background is painted
  // differently.
  bool selected_ = false;

  // Owned by the views hierarchy.
  raw_ptr<views::ImageView> image_;
  raw_ptr<CloseButton> close_button_;

  base::CancelableTaskTracker cancelable_favicon_task_tracker_;
  base::WeakPtrFactory<TabAppSelectionItemView> weak_ptr_factory_{this};
};

BEGIN_METADATA(TabAppSelectionItemView)
END_METADATA

}  // namespace

TabAppSelectionView::TabAppSelectionView() {
  SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kStretch);
  SetOrientation(views::BoxLayout::Orientation::kVertical);

  scroll_view_ = AddChildView(std::make_unique<views::ScrollView>(
      views::ScrollView::ScrollWithLayers::kEnabled));
  scroll_view_->ClipHeightTo(/*min_height=*/0,
                             /*max_height=*/kScrollViewMaxHeight);
  // TODO(http://b/361326120): This applies a rectangle themed background. We
  // will need to set this to std::nullopt and apply a rounded rectangle
  // background elsewhere, or clip the contents after it has been set (painted
  // to a layer).
  scroll_view_->SetBackgroundThemeColorId(
      cros_tokens::kCrosSysSystemOnBaseOpaque);
  scroll_view_->SetBorder(std::make_unique<views::HighlightBorder>(
      kContainerCornerRadius,
      views::HighlightBorder::Type::kHighlightBorderOnShadow));
  scroll_view_->SetViewportRoundedCornerRadius(kContainerCornerRadius);

  AddChildView(views::Builder<views::Separator>()
                   .SetColorId(cros_tokens::kCrosSysSeparator)
                   .SetOrientation(views::Separator::Orientation::kHorizontal)
                   .Build());

  auto contents =
      views::Builder<views::BoxLayoutView>()
          .SetOrientation(views::BoxLayout::Orientation::kVertical)
          .SetCrossAxisAlignment(views::BoxLayout::CrossAxisAlignment::kStretch)
          .SetInsideBorderInsets(kContentsInsets)
          .Build();

  // TODO(http://b/361326120): Grab the lists of tabs and apps from the model or
  // provider.
  contents->AddChildView(CreateSubtitle(u"Tabs"));
  for (int i = 0; i < 10; ++i) {
    TabAppSelectionItemView::InitParams params;
    params.type = TabAppSelectionItemView::InitParams::Type::kTab;
    params.identifier = "https://www.nhl.com/";
    params.close_callback = base::BindOnce(
        &TabAppSelectionView::OnCloseButtonPressed, base::Unretained(this));
    contents->AddChildView(
        std::make_unique<TabAppSelectionItemView>(std::move(params)));
  }
  contents->AddChildView(CreateSubtitle(u"Apps"));
  for (int i = 0; i < 8; ++i) {
    TabAppSelectionItemView::InitParams params;
    params.type = TabAppSelectionItemView::InitParams::Type::kApp;
    params.identifier = "odknhmnlageboeamepcngndbggdpaobj";
    params.close_callback = base::BindOnce(
        &TabAppSelectionView::OnCloseButtonPressed, base::Unretained(this));
    contents->AddChildView(
        std::make_unique<TabAppSelectionItemView>(std::move(params)));
  }

  scroll_view_->SetContents(std::move(contents));
}

TabAppSelectionView::~TabAppSelectionView() = default;

void TabAppSelectionView::OnCloseButtonPressed(views::View* sender) {
  scroll_view_->contents()->RemoveChildViewT(sender);
}

BEGIN_METADATA(TabAppSelectionView)
END_METADATA

}  // namespace ash