chromium/ash/system/cast/media_cast_list_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/system/cast/media_cast_list_view.h"

#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "ash/public/cpp/system_tray_client.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/pill_button.h"
#include "ash/style/typography.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_detailed_view.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/tray/tri_view.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "components/global_media_controls/public/mojom/device_service.mojom-shared.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/throbber.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"

namespace ash {

namespace {

DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(std::string, kDeviceIdKey, nullptr)

// Extra spacing to add between cast stop buttons and the edge of `tri_view()`
// header entry.
constexpr gfx::Insets kItemTriViewPaddings =
    gfx::Insets::TLBR(0, 0, 0, kTrayPopupLabelRightPadding);

// The paddings for the header entry and cast item entries.
constexpr gfx::Insets kHeaderInsets = gfx::Insets::TLBR(0, 0, 0, 8);
constexpr gfx::Insets kHighlightHoverViewInsets = gfx::Insets::VH(0, 16);

// Returns the correct vector icon for the icon type.
// TODO(b/327507429): Revisit the icons for each casting type.
const gfx::VectorIcon& GetVectorIcon(
    global_media_controls::mojom::IconType icon) {
  switch (icon) {
    case global_media_controls::mojom::IconType::kInfo:
      return kSystemMenuCastGenericIcon;
    case global_media_controls::mojom::IconType::kInput:
      return kSystemMenuCastGenericIcon;
    case global_media_controls::mojom::IconType::kSpeaker:
      return kSystemMenuCastAudioIcon;
    case global_media_controls::mojom::IconType::kSpeakerGroup:
      return kSystemMenuCastAudioGroupIcon;
    case global_media_controls::mojom::IconType::kTv:
      return kSystemMenuCastGenericIcon;
      // In these cases the icon is a placeholder and doesn't actually get
      // shown.
      // TODO(b/327507429): Show the loading throbber in the
      // `MediaCastListView`.
    case global_media_controls::mojom::IconType::kThrobber:
    case global_media_controls::mojom::IconType::kUnknown:
      return kSystemMenuCastGenericIcon;
  }
}

}  // namespace

MediaCastListView::MediaCastListView(
    base::RepeatingClosure stop_casting_callback,
    base::RepeatingCallback<void(const std::string& device_id)>
        start_casting_callback,
    base::RepeatingCallback<void(const bool has_devices)>
        on_devices_updated_callback,
    mojo::PendingReceiver<global_media_controls::mojom::DeviceListClient>
        receiver)
    : TrayDetailedView(/*delegate=*/nullptr),
      on_stop_casting_callback_(std::move(stop_casting_callback)),
      on_start_casting_callback_(std::move(start_casting_callback)),
      on_devices_updated_callback_(std::move(on_devices_updated_callback)),
      receiver_(this, std::move(receiver)) {
  // Creates the cast item container.
  item_container_ =
      AddChildView(views::Builder<views::BoxLayoutView>()
                       .SetOrientation(views::BoxLayout::Orientation::kVertical)
                       .Build());
}

MediaCastListView::~MediaCastListView() = default;

void MediaCastListView::OnDevicesUpdated(
    std::vector<global_media_controls::mojom::DevicePtr> devices) {
  // Update UI.
  item_container_->RemoveAllChildViews();

  if (!devices.empty()) {
    CreateCastingHeader();
  }

  // Add a view for each receiver.
  for (const auto& device : devices) {
    HoverHighlightView* container =
        AddScrollListItem(item_container_, GetVectorIcon(device->icon),
                          base::UTF8ToUTF16(device->name));
    container->tri_view()->SetInsets(kHighlightHoverViewInsets);
    container->SetProperty(kDeviceIdKey, device->id);
  }

  // Inform the Panel view on devices update.
  on_devices_updated_callback_.Run(
      /*has_devices=*/!devices.empty());

  item_container_->InvalidateLayout();
}

void MediaCastListView::HandleViewClicked(views::View* view) {
  if (view->GetProperty(kDeviceIdKey)) {
    on_start_casting_callback_.Run(*view->GetProperty(kDeviceIdKey));
  }
}

// TODO(b/327507429): Refactor this method to share it with the
// CastDetailedView.
std::unique_ptr<PillButton> MediaCastListView::CreateStopButton() {
  auto stop_button = std::make_unique<PillButton>(
      base::BindRepeating(
          [](base::WeakPtr<MediaCastListView> view) {
            view->on_stop_casting_callback_.Run();
          },
          weak_factory_.GetWeakPtr()),
      l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_STOP_CASTING),
      PillButton::kDefaultWithIconLeading, &kQuickSettingsCircleStopIcon);
  stop_button->SetBackgroundColorId(cros_tokens::kCrosSysErrorContainer);
  stop_button->SetIconColorId(cros_tokens::kCrosSysError);
  stop_button->SetButtonTextColorId(cros_tokens::kCrosSysError);
  stop_button->SetID(kStopCastingButtonId);
  return stop_button;
}

void MediaCastListView::CreateCastingHeader() {
  auto casting_header = base::WrapUnique(TrayPopupUtils::CreateDefaultRowView(
      /*use_wide_layout=*/false));
  TrayPopupUtils::ConfigureHeader(casting_header.get());

  // Set casting icon on left side.
  auto image_view = base::WrapUnique(
      TrayPopupUtils::CreateMainImageView(/*use_wide_layout=*/false));
  image_view->SetImage(gfx::CreateVectorIcon(
      on_stop_casting_callback_.is_null() ? kQuickSettingsCastIcon
                                          : kQuickSettingsCastConnectedIcon,
      GetColorProvider()->GetColor(cros_tokens::kCrosSysOnSurface)));
  image_view->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
  casting_header->AddView(TriView::Container::START, image_view.release());

  // Set message label in middle of row.
  std::unique_ptr<views::Label> label =
      base::WrapUnique(TrayPopupUtils::CreateDefaultLabel());
  label->SetText(l10n_util::GetStringUTF16(
      IDS_ASH_GLOBAL_MEDIA_CONTROLS_CAST_LIST_HEADER));
  label->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
  label->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
  label->SetAutoColorReadabilityEnabled(false);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosBody2, *label);
  casting_header->AddView(TriView::Container::CENTER, std::move(label));
  casting_header->SetContainerBorder(
      TriView::Container::CENTER,
      views::CreateEmptyBorder(kItemTriViewPaddings));

  // Add stop button to the entry if it's casting.
  if (!on_stop_casting_callback_.is_null()) {
    std::unique_ptr<PillButton> stop_button = CreateStopButton();
    casting_header->AddView(TriView::Container::END, stop_button.release());
    casting_header->SetContainerVisible(TriView::Container::END, true);
  } else {
    // Nothing to the right of the text.
    casting_header->SetContainerVisible(TriView::Container::END, false);
  }
  casting_header->SetInsets(kHeaderInsets);
  item_container_->AddChildView(std::move(casting_header));
}

BEGIN_METADATA(MediaCastListView)
END_METADATA

}  // namespace ash