chromium/ash/system/tray/tray_popup_utils.cc

// Copyright 2016 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/tray/tray_popup_utils.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "ash/constants/ash_constants.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/ash_color_provider.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/size_range_layout.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/tray/tray_utils.h"
#include "ash/system/tray/unfocusable_label.h"
#include "base/functional/bind.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rrect_f.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/vector_icon_utils.h"
#include "ui/views/animation/ink_drop_host.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/separator.h"
#include "ui/views/controls/slider.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/painter.h"
#include "ui/views/view_utils.h"

namespace ash {

namespace {

// Creates a layout manager that positions Views vertically. The Views will be
// stretched horizontally and centered vertically.
std::unique_ptr<views::LayoutManager> CreateDefaultCenterLayoutManager(
    bool use_wide_layout) {
  const auto insets =
      gfx::Insets::VH(kTrayPopupLabelVerticalPadding,
                      use_wide_layout ? kWideTrayPopupLabelHorizontalPadding
                                      : kTrayPopupLabelHorizontalPadding);
  auto box_layout = std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical, insets);
  box_layout->set_main_axis_alignment(
      views::BoxLayout::MainAxisAlignment::kCenter);
  box_layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kStretch);
  return box_layout;
}

// Creates a layout manager that positions Views horizontally. The Views will be
// centered along the horizontal and vertical axis.
std::unique_ptr<views::LayoutManager> CreateDefaultEndsLayoutManager() {
  auto box_layout = std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal);
  box_layout->set_main_axis_alignment(
      views::BoxLayout::MainAxisAlignment::kCenter);
  box_layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  return box_layout;
}

std::unique_ptr<views::LayoutManager> CreateDefaultLayoutManager(
    TriView::Container container,
    bool use_wide_layout) {
  switch (container) {
    case TriView::Container::START:
    case TriView::Container::END:
      return CreateDefaultEndsLayoutManager();
    case TriView::Container::CENTER:
      return CreateDefaultCenterLayoutManager(use_wide_layout);
  }
  // Required by some compilers.
  NOTREACHED();
}

// Configures the default size and flex value for the specified |container|
// of the given |tri_view|. Used by CreateDefaultRowView().
void ConfigureDefaultSizeAndFlex(TriView* tri_view,
                                 TriView::Container container,
                                 bool use_wide_layout) {
  int min_width = 0;
  switch (container) {
    case TriView::Container::START:
      min_width = use_wide_layout ? kTrayPopupItemMinStartWidth
                                  : kWideTrayPopupItemMinStartWidth;
      break;
    case TriView::Container::CENTER:
      tri_view->SetFlexForContainer(TriView::Container::CENTER, 1.f);
      break;
    case TriView::Container::END:
      min_width = use_wide_layout ? kWideTrayPopupItemMinEndWidth
                                  : kTrayPopupItemMinEndWidth;
      break;
  }

  tri_view->SetMinSize(container,
                       gfx::Size(min_width, kTrayPopupItemMinHeight));
  constexpr int kTrayPopupItemMaxHeight = 144;
  tri_view->SetMaxSize(container, gfx::Size(SizeRangeLayout::kAbsoluteMaxSize,
                                            kTrayPopupItemMaxHeight));
}

class HighlightPathGenerator : public views::HighlightPathGenerator {
 public:
  explicit HighlightPathGenerator(TrayPopupInkDropStyle ink_drop_style)
      : ink_drop_style_(ink_drop_style) {}

  HighlightPathGenerator(const HighlightPathGenerator&) = delete;
  HighlightPathGenerator& operator=(const HighlightPathGenerator&) = delete;

  // views::HighlightPathGenerator:
  std::optional<gfx::RRectF> GetRoundRect(const gfx::RectF& rect) override {
    gfx::RectF bounds = rect;
    bounds.Inset(gfx::InsetsF(GetInkDropInsets(ink_drop_style_)));
    float corner_radius = 0.f;
    switch (ink_drop_style_) {
      case TrayPopupInkDropStyle::HOST_CENTERED:
        corner_radius = std::min(bounds.width(), bounds.height()) / 2.f;
        bounds.ClampToCenteredSize(gfx::SizeF(corner_radius, corner_radius));
        break;
      case TrayPopupInkDropStyle::INSET_BOUNDS:
        corner_radius = kTrayPopupInkDropCornerRadius;
        break;
      case TrayPopupInkDropStyle::FILL_BOUNDS:
        break;
    }

    return gfx::RRectF(bounds, corner_radius);
  }

 private:
  const TrayPopupInkDropStyle ink_drop_style_;
};

}  // namespace

TriView* TrayPopupUtils::CreateDefaultRowView(bool use_wide_layout) {
  TriView* tri_view = CreateMultiTargetRowView(use_wide_layout);

  tri_view->SetContainerLayout(
      TriView::Container::START,
      CreateDefaultLayoutManager(TriView::Container::START, use_wide_layout));
  tri_view->SetContainerLayout(
      TriView::Container::CENTER,
      CreateDefaultLayoutManager(TriView::Container::CENTER, use_wide_layout));
  tri_view->SetContainerLayout(
      TriView::Container::END,
      CreateDefaultLayoutManager(TriView::Container::END, use_wide_layout));

  return tri_view;
}

TriView* TrayPopupUtils::CreateSubHeaderRowView(bool start_visible) {
  TriView* tri_view = CreateDefaultRowView(/*use_wide_layout=*/false);
  if (!start_visible) {
    tri_view->SetInsets(gfx::Insets::TLBR(
        0, kTrayPopupPaddingHorizontal - kTrayPopupLabelHorizontalPadding, 0,
        0));
    tri_view->SetContainerVisible(TriView::Container::START, false);
  }
  return tri_view;
}

TriView* TrayPopupUtils::CreateMultiTargetRowView(bool use_wide_layout) {
  TriView* tri_view = new TriView(0 /* padding_between_items */);

  tri_view->SetInsets(gfx::Insets::TLBR(
      0,
      use_wide_layout ? kWideMenuExtraMarginFromLeftEdge
                      : kMenuExtraMarginFromLeftEdge,
      0, use_wide_layout ? kWideMenuExtraMarginsFromRightEdge : 0));

  ConfigureDefaultSizeAndFlex(tri_view, TriView::Container::START,
                              use_wide_layout);
  ConfigureDefaultSizeAndFlex(tri_view, TriView::Container::CENTER,
                              use_wide_layout);
  ConfigureDefaultSizeAndFlex(tri_view, TriView::Container::END,
                              use_wide_layout);

  tri_view->SetContainerLayout(TriView::Container::START,
                               std::make_unique<views::FillLayout>());
  tri_view->SetContainerLayout(TriView::Container::CENTER,
                               std::make_unique<views::FillLayout>());
  tri_view->SetContainerLayout(TriView::Container::END,
                               std::make_unique<views::FillLayout>());

  return tri_view;
}

views::Label* TrayPopupUtils::CreateDefaultLabel() {
  views::Label* label = new views::Label();
  label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  label->SetSubpixelRenderingEnabled(false);
  return label;
}

UnfocusableLabel* TrayPopupUtils::CreateUnfocusableLabel() {
  UnfocusableLabel* label = new UnfocusableLabel();
  label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  label->SetSubpixelRenderingEnabled(false);
  return label;
}

views::ImageView* TrayPopupUtils::CreateMainImageView(bool use_wide_layout) {
  auto* image = new views::ImageView;
  if (use_wide_layout) {
    image->SetPreferredSize(gfx::Size(kMenuIconSize, kMenuIconSize));
  } else {
    image->SetPreferredSize(
        gfx::Size(kWideTrayPopupItemMinStartWidth, kTrayPopupItemMinHeight));
  }
  return image;
}

std::unique_ptr<views::Painter> TrayPopupUtils::CreateFocusPainter() {
  return views::Painter::CreateSolidFocusPainter(
      AshColorProvider::Get()->GetControlsLayerColor(
          AshColorProvider::ControlsLayerType::kFocusRingColor),
      kFocusBorderThickness, gfx::InsetsF());
}

void TrayPopupUtils::ConfigureHeader(views::View* view) {
  view->SetBorder(views::CreateEmptyBorder(
      gfx::Insets::VH(kMenuSeparatorVerticalPadding, 0)));
  view->SetPaintToLayer();
  view->layer()->SetFillsBoundsOpaquely(false);
}

void TrayPopupUtils::ConfigureRowButtonInkdrop(views::InkDropHost* ink_drop) {
  ink_drop->SetMode(views::InkDropHost::InkDropMode::ON);
  ink_drop->SetVisibleOpacity(1.0f);  // The colors already contain opacity
  ink_drop->SetBaseColorId(cros_tokens::kCrosSysRippleNeutralOnSubtle);
}

views::LabelButton* TrayPopupUtils::CreateTrayPopupButton(
    views::Button::PressedCallback callback,
    const std::u16string& text) {
  auto button =
      std::make_unique<views::MdTextButton>(std::move(callback), text);
  button->SetStyle(ui::ButtonStyle::kProminent);
  return button.release();
}

views::Separator* TrayPopupUtils::CreateVerticalSeparator() {
  views::Separator* separator = new views::Separator();
  separator->SetPreferredLength(24);
  separator->SetColorId(ui::kColorAshSystemUIMenuSeparator);
  return separator;
}

void TrayPopupUtils::InstallHighlightPathGenerator(
    views::View* host,
    TrayPopupInkDropStyle ink_drop_style) {
  views::HighlightPathGenerator::Install(
      host, std::make_unique<HighlightPathGenerator>(ink_drop_style));
}

views::Separator* TrayPopupUtils::CreateListItemSeparator(bool left_inset) {
  views::Separator* separator = new views::Separator();
  separator->SetColorId(ui::kColorAshSystemUIMenuSeparator);
  separator->SetBorder(views::CreateEmptyBorder(gfx::Insets::TLBR(
      kMenuSeparatorVerticalPadding - views::Separator::kThickness,
      left_inset ? kMenuExtraMarginFromLeftEdge + kMenuButtonSize +
                       kTrayPopupLabelHorizontalPadding
                 : 0,
      kMenuSeparatorVerticalPadding, 0)));
  return separator;
}

bool TrayPopupUtils::CanOpenWebUISettings() {
  return Shell::Get()->session_controller()->ShouldEnableSettings();
}

bool TrayPopupUtils::CanShowNightLightFeatureTile() {
  return Shell::Get()->session_controller()->ShouldEnableSettings() ||
         (Shell::Get()->session_controller()->GetSessionState() ==
          session_manager::SessionState::LOCKED);
}

void TrayPopupUtils::InitializeAsCheckableRow(HoverHighlightView* container,
                                              bool checked,
                                              bool enterprise_managed) {
  const int dip_size = GetDefaultSizeOfVectorIcon(kCheckCircleIcon);
  // The mapping of `cros_tokens::kCrosSysSystemOnPrimaryContainer` cannot
  // accommodate `check_mark` and other components, so we still want to
  // guard with Jelly flag here.
  ui::ImageModel check_mark =
      CreateCheckMark(cros_tokens::kCrosSysSystemOnPrimaryContainer);
  if (enterprise_managed) {
    ui::ImageModel enterprise_managed_icon = ui::ImageModel::FromVectorIcon(
        chromeos::kEnterpriseIcon, kColorAshIconColorBlocked, dip_size);
    container->AddRightIcon(enterprise_managed_icon,
                            enterprise_managed_icon.Size().width());
  }
  container->AddRightIcon(check_mark, check_mark.Size().width());
  UpdateCheckMarkVisibility(container, checked);
}

void TrayPopupUtils::UpdateCheckMarkVisibility(HoverHighlightView* container,
                                               bool visible) {
  if (!container) {
    return;
  }

  container->SetRightViewVisible(visible);
  container->SetAccessibilityState(
      visible ? HoverHighlightView::AccessibilityState::CHECKED_CHECKBOX
              : HoverHighlightView::AccessibilityState::UNCHECKED_CHECKBOX);
}

void TrayPopupUtils::UpdateCheckMarkColor(HoverHighlightView* container,
                                          ui::ColorId color_id) {
  if (!container || !container->right_view()) {
    return;
  }

  auto check_mark = CreateCheckMark(color_id);
  if (views::IsViewClass<views::ImageView>(container->right_view())) {
    static_cast<views::ImageView*>(container->right_view())
        ->SetImage(check_mark);
  }
}

ui::ImageModel TrayPopupUtils::CreateCheckMark(ui::ColorId color_id) {
  return ui::ImageModel::FromVectorIcon(
      kHollowCheckCircleIcon, color_id,
      GetDefaultSizeOfVectorIcon(kCheckCircleIcon));
}

}  // namespace ash