blob: f750ee4bf6314bdfd6f9f08395ffcfaf235a116b [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/tabs/tab_strip_scroll_container.h"
#include "cc/paint/paint_shader.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/canvas.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/image_button_factory.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
namespace {
// Define a custom FlexRule for |scroll_view_|. Equivalent to using a
// (kScaleToMinimum, kPreferred) flex specification on the tabstrip itself,
// bypassing the ScrollView.
// TODO(1132488): Make ScrollView take on TabStrip's preferred size instead.
gfx::Size TabScrollContainerFlexRule(const views::View* tab_strip,
const views::View* view,
const views::SizeBounds& size_bounds) {
const gfx::Size preferred_size = tab_strip->GetPreferredSize();
const int minimum_width = tab_strip->GetMinimumSize().width();
const int width = std::max(
minimum_width, size_bounds.width().min_of(preferred_size.width()));
return gfx::Size(width, preferred_size.height());
}
std::unique_ptr<views::ImageButton> CreateScrollButton(
views::Button::PressedCallback callback) {
// TODO(tbergquist): These have a lot in common with the NTB and the tab
// search buttons. Could probably extract a base class.
auto scroll_button =
std::make_unique<views::ImageButton>(std::move(callback));
scroll_button->SetImageVerticalAlignment(
views::ImageButton::VerticalAlignment::ALIGN_MIDDLE);
scroll_button->SetImageHorizontalAlignment(
views::ImageButton::HorizontalAlignment::ALIGN_CENTER);
scroll_button->SetHasInkDropActionOnClick(true);
views::InkDrop::Get(scroll_button.get())
->SetMode(views::InkDropHost::InkDropMode::ON);
scroll_button->SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);
scroll_button->SetPreferredSize(gfx::Size(28, 28));
views::HighlightPathGenerator::Install(
scroll_button.get(),
std::make_unique<views::CircleHighlightPathGenerator>(gfx::Insets()));
const views::FlexSpecification button_flex_spec =
views::FlexSpecification(views::LayoutOrientation::kHorizontal,
views::MinimumFlexSizeRule::kScaleToMinimum,
views::MaximumFlexSizeRule::kPreferred);
scroll_button->SetProperty(views::kFlexBehaviorKey, button_flex_spec);
return scroll_button;
}
// A customized overflow indicator that paints a shadow-like gradient over the
// tabstrip.
class TabStripContainerOverflowIndicator : public views::View {
public:
METADATA_HEADER(TabStripContainerOverflowIndicator);
TabStripContainerOverflowIndicator(TabStrip* tab_strip,
views::OverflowIndicatorAlignment side)
: tab_strip_(tab_strip), side_(side) {
DCHECK(side_ == views::OverflowIndicatorAlignment::kLeft ||
side_ == views::OverflowIndicatorAlignment::kRight);
}
// Making this smaller than the margin provided by the leftmost/rightmost
// tab's tail (TabStyle::kTabOverlap / 2) makes the transition in and out of
// the scroll state smoother.
static constexpr int kOpaqueWidth = 8;
// The width of the full opacity part of the shadow.
static constexpr int kShadowSpread = 1;
// The width of the soft edge of the shadow.
static constexpr int kShadowBlur = 3;
static constexpr int kTotalWidth = kOpaqueWidth + kShadowSpread + kShadowBlur;
// views::View overrides:
void OnPaint(gfx::Canvas* canvas) override {
// TODO(tbergquist): Handle themes with titlebar background images.
// TODO(tbergquist): Handle dark themes where GG800 doesn't contrast well.
SkColor frame_color = tab_strip_->controller()->GetFrameColor(
BrowserFrameActiveState::kUseCurrent);
SkColor shadow_color = gfx::kGoogleGrey800;
// Mirror how the indicator is painted for the right vs left sides.
SkPoint points[2];
if (side_ == views::OverflowIndicatorAlignment::kLeft) {
points[0].iset(GetContentsBounds().origin().x(), GetContentsBounds().y());
points[1].iset(GetContentsBounds().right(), GetContentsBounds().y());
} else {
points[0].iset(GetContentsBounds().right(), GetContentsBounds().y());
points[1].iset(GetContentsBounds().origin().x(), GetContentsBounds().y());
}
SkColor colors[5];
SkScalar color_positions[5];
// Paint an opaque region on the outside.
colors[0] = frame_color;
colors[1] = frame_color;
color_positions[0] = 0;
color_positions[1] = static_cast<float>(kOpaqueWidth) / kTotalWidth;
// Paint a shadow-like gradient on the inside.
colors[2] = SkColorSetA(shadow_color, 0x4D);
colors[3] = SkColorSetA(shadow_color, 0x4D);
colors[4] = SkColorSetA(shadow_color, SK_AlphaTRANSPARENT);
color_positions[2] = static_cast<float>(kOpaqueWidth) / kTotalWidth;
color_positions[3] =
static_cast<float>(kOpaqueWidth + kShadowSpread) / kTotalWidth;
color_positions[4] = 1;
cc::PaintFlags flags;
flags.setShader(cc::PaintShader::MakeLinearGradient(
points, colors, color_positions, 5, SkTileMode::kClamp));
canvas->DrawRect(GetContentsBounds(), flags);
}
private:
TabStrip* tab_strip_;
views::OverflowIndicatorAlignment side_;
};
BEGIN_METADATA(TabStripContainerOverflowIndicator, views::View)
END_METADATA
} // namespace
TabStripScrollContainer::TabStripScrollContainer(
std::unique_ptr<TabStrip> tab_strip)
: tab_strip_(tab_strip.get()) {
SetLayoutManager(std::make_unique<views::FillLayout>())
->SetMinimumSizeEnabled(true);
// TODO(https://crbug.com/1132488): ScrollView doesn't propagate changes to
// the TabStrip's preferred size; observe that manually.
tab_strip->View::AddObserver(this);
tab_strip->SetAvailableWidthCallback(
base::BindRepeating(&TabStripScrollContainer::GetTabStripAvailableWidth,
base::Unretained(this)));
std::unique_ptr<views::ScrollView> scroll_view =
std::make_unique<views::ScrollView>(
views::ScrollView::ScrollWithLayers::kEnabled);
scroll_view_ = scroll_view.get();
scroll_view->SetBackgroundColor(absl::nullopt);
scroll_view->SetHorizontalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
scroll_view->SetTreatAllScrollEventsAsHorizontal(true);
scroll_view->SetContents(std::move(tab_strip));
scroll_view->SetDrawOverflowIndicator(true);
left_overflow_indicator_ = scroll_view->SetCustomOverflowIndicator(
views::OverflowIndicatorAlignment::kLeft,
std::make_unique<TabStripContainerOverflowIndicator>(
tab_strip_, views::OverflowIndicatorAlignment::kLeft),
TabStripContainerOverflowIndicator::kTotalWidth, false);
right_overflow_indicator_ = scroll_view->SetCustomOverflowIndicator(
views::OverflowIndicatorAlignment::kRight,
std::make_unique<TabStripContainerOverflowIndicator>(
tab_strip_, views::OverflowIndicatorAlignment::kRight),
TabStripContainerOverflowIndicator::kTotalWidth, false);
// This base::Unretained is safe because the callback is called by the
// layout manager, which is cleaned up before view children like
// |scroll_view| (which owns |tab_strip_|).
scroll_view->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(base::BindRepeating(
&TabScrollContainerFlexRule, base::Unretained(tab_strip_))));
std::unique_ptr<views::View> scroll_button_container =
std::make_unique<views::View>();
views::FlexLayout* scroll_button_layout =
scroll_button_container->SetLayoutManager(
std::make_unique<views::FlexLayout>());
scroll_button_layout->SetOrientation(views::LayoutOrientation::kHorizontal);
leading_scroll_button_ =
scroll_button_container->AddChildView(CreateScrollButton(
base::BindRepeating(&TabStripScrollContainer::ScrollTowardsLeadingTab,
base::Unretained(this))));
trailing_scroll_button_ = scroll_button_container->AddChildView(
CreateScrollButton(base::BindRepeating(
&TabStripScrollContainer::ScrollTowardsTrailingTab,
base::Unretained(this))));
// The space in dips between the scroll buttons and the NTB.
constexpr int kScrollButtonsTrailingMargin = 8;
trailing_scroll_button_->SetProperty(
views::kMarginsKey, gfx::Insets(0, 0, 0, kScrollButtonsTrailingMargin));
// The default layout orientation (kHorizontal) and cross axis alignment
// (kStretch) work for our use case.
overflow_view_ = AddChildView(std::make_unique<OverflowView>(
std::move(scroll_view), std::move(scroll_button_container)));
}
TabStripScrollContainer::~TabStripScrollContainer() = default;
void TabStripScrollContainer::OnViewPreferredSizeChanged(views::View* view) {
DCHECK_EQ(tab_strip_, view);
PreferredSizeChanged();
}
int TabStripScrollContainer::GetTabStripAvailableWidth() const {
return overflow_view_->GetAvailableSize(scroll_view_).width().value();
}
void TabStripScrollContainer::ScrollTowardsLeadingTab() {
gfx::Rect visible_content = scroll_view_->GetVisibleRect();
gfx::Rect scroll(visible_content.x() - visible_content.width(),
visible_content.y(), visible_content.width(),
visible_content.height());
scroll_view_->contents()->ScrollRectToVisible(scroll);
}
void TabStripScrollContainer::ScrollTowardsTrailingTab() {
gfx::Rect visible_content = scroll_view_->GetVisibleRect();
gfx::Rect scroll(visible_content.x() + visible_content.width(),
visible_content.y(), visible_content.width(),
visible_content.height());
scroll_view_->contents()->ScrollRectToVisible(scroll);
}
void TabStripScrollContainer::FrameColorsChanged() {
const SkColor background_color = tab_strip_->GetTabBackgroundColor(
TabActive::kInactive, BrowserFrameActiveState::kUseCurrent);
SkColor foreground_color =
tab_strip_->GetTabForegroundColor(TabActive::kInactive, background_color);
views::SetImageFromVectorIconWithColor(
leading_scroll_button_, kScrollingTabstripLeadingIcon, foreground_color);
views::SetImageFromVectorIconWithColor(trailing_scroll_button_,
kScrollingTabstripTrailingIcon,
foreground_color);
left_overflow_indicator_->SchedulePaint();
right_overflow_indicator_->SchedulePaint();
}
void TabStripScrollContainer::OnThemeChanged() {
View::OnThemeChanged();
FrameColorsChanged();
}
BEGIN_METADATA(TabStripScrollContainer, views::View)
ADD_READONLY_PROPERTY_METADATA(int, TabStripAvailableWidth)
END_METADATA