chromium/ash/style/tab_slider.cc

// Copyright 2022 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/style/tab_slider.h"

#include <cstddef>

#include "ash/style/style_util.h"
#include "ash/style/tab_slider_button.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/layout/table_layout.h"
#include "ui/views/view_class_properties.h"

namespace ash {

namespace {

constexpr ui::ColorId kSliderBackgroundColorId =
    cros_tokens::kCrosSysSystemOnBase;
constexpr ui::ColorId kSelectorBackgroundColorId =
    cros_tokens::kCrosSysSystemPrimaryContainer;

constexpr base::TimeDelta kSelectorAnimationDuration = base::Milliseconds(150);

}  // namespace

//------------------------------------------------------------------------------
// TabSlider::SelectorView:

// The selector shows behind the selected slider button. When a button is
// selected, it moves from the previously selected button to the currently
// selected button.
class TabSlider::SelectorView : public views::View {
  METADATA_HEADER(SelectorView, views::View)

 public:
  explicit SelectorView(bool has_animation) : has_animation_(has_animation) {
    SetPaintToLayer();
    layer()->SetFillsBoundsOpaquely(false);
    SetBackground(StyleUtil::CreateThemedFullyRoundedRectBackground(
        kSelectorBackgroundColorId));
  }

  SelectorView(const SelectorView&) = delete;
  SelectorView& operator=(const SelectorView&) = delete;
  ~SelectorView() override = default;

  // Moves the selector to the selected button. Performs animation if
  // `has_animation_` is true.
  void MoveToSelectedButton(TabSliderButton* button) {
    DCHECK(button);
    DCHECK(button->selected());

    if (button_ == button) {
      return;
    }

    TabSliderButton* previous_button = button_;
    button_ = button;

    // Update selector's bounds with the selected button's bounds.
    SetBoundsRect(button_->bounds());

    // Performs an animation of the selector moving from the position of last
    // selected button to the position of currently selected button, if needed.
    if (!previous_button || !has_animation_) {
      return;
    }

    auto* view_layer = layer();

    gfx::Transform reverse_transform = gfx::TransformBetweenRects(
        gfx::RectF(button_->GetMirroredBounds()),
        gfx::RectF(previous_button->GetMirroredBounds()));
    view_layer->SetTransform(reverse_transform);
    ui::ScopedLayerAnimationSettings settings(view_layer->GetAnimator());
    settings.SetTransitionDuration(kSelectorAnimationDuration);
    view_layer->SetTransform(gfx::Transform());
  }

 private:
  // Indicates if there is a movement animation.
  const bool has_animation_;
  // Now owned.
  raw_ptr<TabSliderButton> button_ = nullptr;
};

BEGIN_METADATA(TabSlider, SelectorView)
END_METADATA

//------------------------------------------------------------------------------
// TabSlider:

TabSlider::TabSlider(size_t max_tab_num, const InitParams& params)
    : max_tab_num_(max_tab_num),
      params_(params),
      selector_view_(AddChildView(
          std::make_unique<SelectorView>(params.has_selector_animation))) {
  // Add a fully rounded rect background if needed.
  if (params_.has_background) {
    SetPaintToLayer();
    layer()->SetFillsBoundsOpaquely(false);
    SetBackground(StyleUtil::CreateThemedFullyRoundedRectBackground(
        kSliderBackgroundColorId));
  }

  Init();

  selector_view_->SetProperty(views::kViewIgnoredByLayoutKey, true);

  enabled_changed_subscription_ = AddEnabledChangedCallback(base::BindRepeating(
      &TabSlider::OnEnabledStateChanged, base::Unretained(this)));
}

TabSlider::~TabSlider() = default;

views::View* TabSlider::GetSelectorView() {
  return selector_view_;
}

TabSliderButton* TabSlider::GetButtonAtIndex(size_t index) {
  CHECK(index < buttons_.size());
  return buttons_[index];
}

void TabSlider::OnButtonSelected(TabSliderButton* button) {
  DCHECK(button);
  DCHECK(base::Contains(buttons_, button));
  DCHECK(button->selected());

  // Deselect all the other buttons and check if the tab slider has focus.
  bool has_focus = false;
  for (ash::TabSliderButton* b : buttons_) {
    b->SetSelected(b == button);
    has_focus |= b->HasFocus();
  }

  // Move the selector to the selected button.
  selector_view_->MoveToSelectedButton(button);

  // Move the focus to the selected button.
  if (has_focus) {
    GetFocusManager()->SetFocusedView(button);
  }
}

void TabSlider::Layout(PassKey) {
  LayoutSuperclass<views::View>(this);

  // Synchronize the selector bounds with selected button's bounds.
  auto it =
      std::find_if(buttons_.begin(), buttons_.end(),
                   [](TabSliderButton* button) { return button->selected(); });
  if (it == buttons_.end()) {
    return;
  }
  selector_view_->SetBoundsRect((*it)->bounds());
}

void TabSlider::Init() {
  const int internal_border_padding = params_.internal_border_padding;

  // Create rows:
  // Add top border padding row.
  AddPaddingRow(views::TableLayout::kFixedSize, internal_border_padding);
  // Add middle buttons row.
  AddRows(1, views::TableLayout::kFixedSize);
  // Add bottom border padding row.
  AddPaddingRow(views::TableLayout::kFixedSize, internal_border_padding);

  // Create columns:
  // Add left border padding column.
  AddPaddingColumn(views::TableLayout::kFixedSize, internal_border_padding);
  // Alternatively add button column and padding column.
  std::vector<size_t> columns_containing_buttons;
  for (size_t i = 0; i < max_tab_num_; ++i) {
    AddColumn(views::LayoutAlignment::kStretch, views::LayoutAlignment::kCenter,
              1.0f, views::TableLayout::ColumnSize::kUsePreferred, 0, 0);
    columns_containing_buttons.push_back(2 * i + 1);
    if (i != max_tab_num_ - 1) {
      AddPaddingColumn(views::TableLayout::kFixedSize,
                       params_.between_buttons_spacing);
    }
  }
  // Add right border padding column.
  AddPaddingColumn(views::TableLayout::kFixedSize, internal_border_padding);

  if (params_.distribute_space_evenly) {
    // Ensure extra space is spread evenly between the button containing
    // columns.
    LinkColumnSizes(columns_containing_buttons);
  }
}

void TabSlider::AddButtonInternal(TabSliderButton* button) {
  CHECK(button);
  CHECK_LT(buttons_.size(), max_tab_num_)
      << "Number of buttons reaches the limit";

  // Add the button as a child of the tab slider and insert it in the
  // `buttons_` list.
  AddChildView(button);
  buttons_.emplace_back(button);
  button->AddedToSlider(this);
}

void TabSlider::OnEnabledStateChanged() {
  // Propagate the enabled state to all slider buttons and the selector view.
  const bool enabled = GetEnabled();

  for (ash::TabSliderButton* b : buttons_) {
    b->SetEnabled(enabled);
  }

  selector_view_->SetEnabled(enabled);
  SchedulePaint();
}

BEGIN_METADATA(TabSlider)
END_METADATA

}  // namespace ash