chromium/ash/app_list/views/page_switcher.cc

// Copyright 2012 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/app_list/views/page_switcher.h"

#include <memory>
#include <utility>

#include "ash/app_list/app_list_metrics.h"
#include "ash/public/cpp/pagination/pagination_model.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/icon_button.h"
#include "base/functional/bind.h"
#include "base/i18n/number_formatting.h"
#include "base/metrics/histogram_macros.h"
#include "base/ranges/algorithm.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/animation/throb_animation.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/animation/flood_fill_ink_drop_ripple.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_highlight.h"
#include "ui/views/animation/ink_drop_impl.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/layout/box_layout.h"

namespace ash {

namespace {

constexpr int kNormalButtonRadius = 3;
constexpr int kSelectedButtonRadius = 4;
constexpr SkScalar kStrokeWidth = SkIntToScalar(2);

// Constants for the button strip that grows vertically.
// The padding on top/bottom side of each button.
constexpr int kVerticalButtonPadding = 0;

class PageSwitcherButton : public IconButton {
  METADATA_HEADER(PageSwitcherButton, IconButton)

 public:
  PageSwitcherButton(PressedCallback callback,
                     const std::u16string& accesible_name)
      : IconButton(std::move(callback),
                   IconButton::Type::kMediumFloating,
                   /*icon=*/nullptr,
                   accesible_name,
                   /*is_togglable=*/false,
                   /*has_border=*/false) {
    SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);
  }

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

  ~PageSwitcherButton() override {}

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

    selected_ = selected;
    SchedulePaint();
    if (selected)
      NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
  }

  // IconButton:
  void PaintButtonContents(gfx::Canvas* canvas) override {
    PaintButton(canvas, BuildPaintButtonInfo());
  }

 private:
  // Stores the information of how to paint the button.
  struct PaintButtonInfo {
    SkColor color;
    cc::PaintFlags::Style style;
    SkScalar radius;
    SkScalar stroke_width;
  };

  // Returns the information of how to paint selected/normal button.
  PaintButtonInfo BuildPaintButtonInfo() {
    PaintButtonInfo info;
    info.color = GetColorProvider()->GetColor(kColorAshButtonIconColor);
    if (selected_) {
      info.style = cc::PaintFlags::kFill_Style;
      info.radius = SkIntToScalar(kSelectedButtonRadius);
      info.stroke_width = SkIntToScalar(0);
    } else {
      info.style = cc::PaintFlags::kStroke_Style;
      info.radius = SkIntToScalar(kNormalButtonRadius);
      info.stroke_width = kStrokeWidth;
    }
    return info;
  }

  // Paints a button based on the |info|.
  void PaintButton(gfx::Canvas* canvas, PaintButtonInfo info) {
    gfx::Rect rect(GetContentsBounds());
    SkPath path;
    path.addCircle(rect.CenterPoint().x(), rect.CenterPoint().y(), info.radius);

    cc::PaintFlags flags;
    flags.setAntiAlias(true);
    flags.setStyle(info.style);
    flags.setColor(info.color);
    flags.setStrokeWidth(info.stroke_width);
    canvas->DrawPath(path, flags);
  }

  // If this button is selected, set to true. By default, set to false;
  bool selected_ = false;
};

// Gets PageSwitcherButton at |index| in |buttons|.
PageSwitcherButton* GetButtonByIndex(views::View* buttons, size_t index) {
  return static_cast<PageSwitcherButton*>(buttons->children()[index]);
}

BEGIN_METADATA(PageSwitcherButton)
END_METADATA

}  // namespace

PageSwitcher::PageSwitcher(PaginationModel* model)
    : model_(model), buttons_(new views::View) {
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);
  buttons_->SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical, gfx::Insets(),
      kVerticalButtonPadding));

  AddChildView(buttons_.get());

  TotalPagesChanged(0, model->total_pages());
  SelectedPageChanged(-1, model->selected_page());
  model_->AddObserver(this);
}

PageSwitcher::~PageSwitcher() {
  if (model_)
    model_->RemoveObserver(this);
}

gfx::Size PageSwitcher::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  views::SizeBounds content_available_size(available_size);
  content_available_size.set_width(2 * PageSwitcher::kMaxButtonRadius);

  gfx::Insets insets = GetInsets();
  content_available_size.Enlarge(-insets.width(), -insets.height());

  gfx::Size buttons_size = buttons_->GetPreferredSize(content_available_size);

  // Always return a size with correct width so that container resize is not
  // needed when more pages are added.
  return gfx::Size(2 * PageSwitcher::kMaxButtonRadius,
                   buttons_size.height() + insets.height());
}

void PageSwitcher::Layout(PassKey) {
  gfx::Rect rect(GetContentsBounds());
  if (rect.IsEmpty())
    return;
  gfx::Size buttons_size(buttons_->GetPreferredSize());
  rect.ClampToCenteredSize(buttons_size);
  buttons_->SetBoundsRect(rect);
}

void PageSwitcher::OnThemeChanged() {
  views::View::OnThemeChanged();
  if (!buttons_)
    return;
  for (views::View* child : buttons_->children()) {
    if (child->GetVisible())
      child->SchedulePaint();
  }
}

void PageSwitcher::HandlePageSwitch(const ui::Event& event) {
  if (!model_)
    return;

  const auto& children = buttons_->children();
  const auto it = base::ranges::find(children, event.target());
  DCHECK(it != children.end());
  const int page = std::distance(children.begin(), it);
  if (page == model_->selected_page())
    return;
  RecordPageSwitcherSource(event.IsGestureEvent() ? kTouchPageIndicator
                                                  : kClickPageIndicator);
  model_->SelectPage(page, true /* animate */);
}

void PageSwitcher::TotalPagesChanged(int previous_page_count,
                                     int new_page_count) {
  if (!model_)
    return;

  buttons_->RemoveAllChildViews();
  for (int i = 0; i < model_->total_pages(); ++i) {
    PageSwitcherButton* button =
        buttons_->AddChildView(std::make_unique<PageSwitcherButton>(
            base::BindRepeating(&PageSwitcher::HandlePageSwitch,
                                base::Unretained(this)),
            l10n_util::GetStringFUTF16(
                IDS_APP_LIST_PAGE_SWITCHER, base::FormatNumber(i + 1),
                base::FormatNumber(model_->total_pages()))));
    button->SetSelected(i == model_->selected_page() ? true : false);
  }
  buttons_->SetVisible(model_->total_pages() > 1);
  PreferredSizeChanged();
}

void PageSwitcher::SelectedPageChanged(int old_selected, int new_selected) {
  if (old_selected >= 0 &&
      static_cast<size_t>(old_selected) < buttons_->children().size())
    GetButtonByIndex(buttons_, static_cast<size_t>(old_selected))
        ->SetSelected(false);
  if (new_selected >= 0 &&
      static_cast<size_t>(new_selected) < buttons_->children().size())
    GetButtonByIndex(buttons_, static_cast<size_t>(new_selected))
        ->SetSelected(true);
}

BEGIN_METADATA(PageSwitcher)
END_METADATA

}  // namespace ash