15 changes: 11 additions & 4 deletions browser/ui/views/frame/vertical_tab_strip_region_view.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class VerticalTabStripRegionView : public views::View,

TabSearchBubbleHost* GetTabSearchBubbleHost();

int GetScrollViewViewportHeight() const;
int GetTabStripViewportHeight() const;

void set_layout_dirty(base::PassKey<VerticalTabStripScrollContentsView>) {
layout_dirty_ = true;
Expand All @@ -89,13 +89,15 @@ class VerticalTabStripRegionView : public views::View,
void PreferredSizeChanged() override;

// TabStripModelObserver:
// TODO(sko) Remove this once the "sticky pinned tabs" is enabled by default.
// https://github.com/brave/brave-browser/issues/29935
void OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) override;

private:
class ScrollHeaderView;
class HeaderView;

FRIEND_TEST_ALL_PREFIXES(VerticalTabStripBrowserTest, VisualState);

Expand All @@ -122,6 +124,8 @@ class VerticalTabStripRegionView : public views::View,
// Returns valid object only when the related flag is enabled.
TabStripScrollContainer* GetTabStripScrollContainer();

// TODO(sko) Remove this once the "sticky pinned tabs" is enabled by default.
// https://github.com/brave/brave-browser/issues/29935
void ScrollActiveTabToBeVisible();

std::u16string GetShortcutTextForNewTabButton(BrowserView* browser_view);
Expand All @@ -132,9 +136,12 @@ class VerticalTabStripRegionView : public views::View,
raw_ptr<TabStripRegionView> original_region_view_ = nullptr;

// Contains TabStripRegion.
// TODO(sko) Remove this once the "sticky pinned tabs" is enabled by default.
// https://github.com/brave/brave-browser/issues/29935
raw_ptr<views::ScrollView> scroll_view_ = nullptr;
raw_ptr<views::View> scroll_contents_view_ = nullptr;
raw_ptr<ScrollHeaderView> scroll_view_header_ = nullptr;

raw_ptr<HeaderView> header_view_ = nullptr;
raw_ptr<views::View> contents_view_ = nullptr;

// New tab button created for vertical tabs
raw_ptr<BraveNewTabButton> new_tab_button_ = nullptr;
Expand Down
256 changes: 243 additions & 13 deletions browser/ui/views/tabs/brave_compound_tab_container.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "brave/browser/ui/views/tabs/brave_compound_tab_container.h"

#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
Expand All @@ -17,10 +18,65 @@
#include "brave/browser/ui/views/tabs/vertical_tab_utils.h"
#include "chrome/browser/ui/ui_features.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/scrollbar/overlay_scroll_bar.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/view_utils.h"

namespace {
class ContentsView : public views::View {
public:
explicit ContentsView(BraveCompoundTabContainer* container)
: container_(container) {
SetLayoutManager(std::make_unique<views::FillLayout>());
}
~ContentsView() override = default;

// views::View:
void ChildPreferredSizeChanged(views::View* child) override {
// Bypass ScrollView and notify the BraveCompoundTabContainer directly.
container_->ChildPreferredSizeChanged(child);
}

base::raw_ptr<BraveCompoundTabContainer> container_;
};

// A custom scroll view to avoid bugs from upstream
// At the moment,
// * ScrollRectToVisible() doesn't work well, so disable layer and make it
// easier to manipulate offset.
// * When disabling ScrollWithLayers, OnScrollEvent cause DCHECK failure.
// * Even when scrollbar is kHiddenButEnabled, the width for contents view is
// cut off.
// In order to avoid that, attach overlay scroll bar which doesn't take
// space.
class CustomScrollView : public views::ScrollView {
public:
METADATA_HEADER(CustomScrollView);

CustomScrollView()
: views::ScrollView(views::ScrollView::ScrollWithLayers::kDisabled) {
SetDrawOverflowIndicator(false);
SetVerticalScrollBarMode(
views::ScrollView::ScrollBarMode::kHiddenButEnabled);
SetHorizontalScrollBarMode(views::ScrollView::ScrollBarMode::kDisabled);
SetVerticalScrollBar(
std::make_unique<views::OverlayScrollBar>(/* horizontal= */ false));
}
~CustomScrollView() override = default;

// views::ScrollView:
void OnScrollEvent(ui::ScrollEvent* event) override {
// DO NOTHING to avoid crash when layer is disabled.
}
};

BEGIN_METADATA(CustomScrollView, views::ScrollView)
END_METADATA

} // namespace

BraveCompoundTabContainer::BraveCompoundTabContainer(
TabContainerController& controller,
TabHoverCardController* hover_card_controller,
Expand Down Expand Up @@ -54,7 +110,13 @@ void BraveCompoundTabContainer::SetAvailableWidthCallback(
unpinned_tab_container_->SetAvailableWidthCallback(base::NullCallback());
}

BraveCompoundTabContainer::~BraveCompoundTabContainer() = default;
BraveCompoundTabContainer::~BraveCompoundTabContainer() {
if (scroll_view_) {
// Remove scroll view and set this as the parent of
// |unpinned_tab_container_| so that clean up can be done by upstream code.
SetScrollEnabled(false);
}
}

base::OnceClosure BraveCompoundTabContainer::LockLayout() {
std::vector<base::OnceClosure> closures;
Expand All @@ -73,6 +135,29 @@ base::OnceClosure BraveCompoundTabContainer::LockLayout() {
std::move(closures));
}

void BraveCompoundTabContainer::SetScrollEnabled(bool enabled) {
DCHECK(base::FeatureList::IsEnabled(
tabs::features::kBraveVerticalTabsStickyPinnedTabs));
if (enabled == !!scroll_view_) {
return;
}

if (enabled) {
scroll_view_ = AddChildView(std::make_unique<CustomScrollView>());
auto* contents_view =
scroll_view_->SetContents(std::make_unique<ContentsView>(this));
contents_view->AddChildView(base::to_address(unpinned_tab_container_));
Layout();
} else {
unpinned_tab_container_->parent()->RemoveChildView(
base::to_address(unpinned_tab_container_));
AddChildView(base::to_address(unpinned_tab_container_));

RemoveChildViewT(scroll_view_.get());
scroll_view_ = nullptr;
}
}

void BraveCompoundTabContainer::TransferTabBetweenContainers(
int from_model_index,
int to_model_index) {
Expand Down Expand Up @@ -122,17 +207,50 @@ void BraveCompoundTabContainer::Layout() {
return;
}

// We use flex layout manager so let it do its job.
views::View::Layout();
if (!base::FeatureList::IsEnabled(
tabs::features::kBraveVerticalTabsStickyPinnedTabs)) {
// We use flex layout manager so let it do its job.
views::View::Layout();
return;
}

const auto contents_bounds = GetContentsBounds();

// Pinned container gets however much space it wants.
pinned_tab_container_->SetBoundsRect(
gfx::Rect(gfx::Size(contents_bounds.width(),
pinned_tab_container_->GetPreferredSize().height())));

// Unpinned container gets the left over.
if (scroll_view_) {
auto bounds = gfx::Rect(
contents_bounds.x(), pinned_tab_container_->bounds().bottom(), width(),
contents_bounds.height() - pinned_tab_container_->height());
scroll_view_->SetBoundsRect(bounds);
if (scroll_view_->GetMaxHeight() != bounds.height()) {
scroll_view_->ClipHeightTo(0, scroll_view_->height());
}

UpdateUnpinnedContainerSize();
} else {
unpinned_tab_container_->SetBoundsRect(
gfx::Rect(contents_bounds.x(), pinned_tab_container_->bounds().bottom(),
contents_bounds.width(),
contents_bounds.height() - pinned_tab_container_->height()));
}
}

gfx::Size BraveCompoundTabContainer::CalculatePreferredSize() const {
if (!ShouldShowVerticalTabs()) {
return CompoundTabContainer::CalculatePreferredSize();
}

// We use flex layout manager so let it do its job.
auto preferred_size = views::View::CalculatePreferredSize();
const auto sticky_pinned_tabs_enabled = base::FeatureList::IsEnabled(
tabs::features::kBraveVerticalTabsStickyPinnedTabs);

auto preferred_size = sticky_pinned_tabs_enabled
? CompoundTabContainer::CalculatePreferredSize()
: views::View::CalculatePreferredSize();

// Check if we can expand height to fill the entire scroll area's viewport.
for (auto* parent_view = parent(); parent_view;
Expand All @@ -142,8 +260,13 @@ gfx::Size BraveCompoundTabContainer::CalculatePreferredSize() const {
if (!region_view) {
continue;
}
preferred_size.set_height(std::max(
region_view->GetScrollViewViewportHeight(), preferred_size.height()));

if (sticky_pinned_tabs_enabled) {
preferred_size.set_height(region_view->GetTabStripViewportHeight());
} else {
preferred_size.set_height(std::max(
region_view->GetTabStripViewportHeight(), preferred_size.height()));
}
break;
}

Expand All @@ -155,6 +278,11 @@ gfx::Size BraveCompoundTabContainer::GetMinimumSize() const {
return CompoundTabContainer::GetMinimumSize();
}

if (base::FeatureList::IsEnabled(
tabs::features::kBraveVerticalTabsStickyPinnedTabs)) {
return {};
}

// We use flex layout manager so let it do its job.
return views::View::GetMinimumSize();
}
Expand All @@ -172,20 +300,25 @@ views::SizeBounds BraveCompoundTabContainer::GetAvailableSize(
Tab* BraveCompoundTabContainer::AddTab(std::unique_ptr<Tab> tab,
int model_index,
TabPinned pinned) {
auto* result =
auto* new_tab =
CompoundTabContainer::AddTab(std::move(tab), model_index, pinned);
if (!base::FeatureList::IsEnabled(tabs::features::kBraveVerticalTabs) ||
!tabs::utils::ShouldShowVerticalTabs(
tab_slot_controller_->GetBrowser())) {
return result;
return new_tab;
}

if (pinned == TabPinned::kPinned && !pinned_tab_container_->GetVisible()) {
// When the browser was initialized without any pinned tabs, pinned tabs
// could be hidden initially by the FlexLayout.
pinned_tab_container_->SetVisible(true);
}
return result;

if (scroll_view_ && pinned == TabPinned::kUnpinned && new_tab->IsActive()) {
ScrollTabToBeVisible(model_index);
}

return new_tab;
}

int BraveCompoundTabContainer::GetUnpinnedContainerIdealLeadingX() const {
Expand Down Expand Up @@ -224,15 +357,55 @@ void BraveCompoundTabContainer::OnThemeChanged() {
}
}

void BraveCompoundTabContainer::PaintChildren(const views::PaintInfo& info) {
if (ShouldShowVerticalTabs() &&
base::FeatureList::IsEnabled(
tabs::features::kBraveVerticalTabsStickyPinnedTabs)) {
// Bypass CompoundTabContainer::PaintChildren() implementation.
// CompoundTabContainer calls children's View::Paint() even when they have
// their own layer, which shouldn't happen.
views::View::PaintChildren(info);
} else {
CompoundTabContainer::PaintChildren(info);
}
}

void BraveCompoundTabContainer::ChildPreferredSizeChanged(views::View* child) {
if (ShouldShowVerticalTabs() && scroll_view_ &&
child->Contains(base::to_address(unpinned_tab_container_))) {
UpdateUnpinnedContainerSize();
}

CompoundTabContainer::ChildPreferredSizeChanged(child);
}

void BraveCompoundTabContainer::SetActiveTab(
absl::optional<size_t> prev_active_index,
absl::optional<size_t> new_active_index) {
CompoundTabContainer::SetActiveTab(prev_active_index, new_active_index);
if (new_active_index.has_value()) {
ScrollTabToBeVisible(*new_active_index);
}
}

TabContainer* BraveCompoundTabContainer::GetTabContainerAt(
gfx::Point point_in_local_coords) const {
if (!ShouldShowVerticalTabs()) {
return CompoundTabContainer::GetTabContainerAt(point_in_local_coords);
}

return point_in_local_coords.y() < pinned_tab_container_->bounds().bottom()
? base::to_address(pinned_tab_container_)
: base::to_address(unpinned_tab_container_);
auto* container =
point_in_local_coords.y() < pinned_tab_container_->bounds().bottom()
? base::to_address(pinned_tab_container_)
: base::to_address(unpinned_tab_container_);

if (!container->GetWidget()) {
// Note that this could be happen when we're detaching tabs and we're still
// changing view hierarchy.
return nullptr;
}

return container;
}

gfx::Rect BraveCompoundTabContainer::ConvertUnpinnedContainerIdealBoundsToLocal(
Expand All @@ -242,6 +415,12 @@ gfx::Rect BraveCompoundTabContainer::ConvertUnpinnedContainerIdealBoundsToLocal(
ideal_bounds);
}

if (scroll_view_) {
return views::View::ConvertRectToTarget(
/*source=*/base::to_address(unpinned_tab_container_), /*target=*/this,
ideal_bounds);
}

ideal_bounds.Offset(0, unpinned_tab_container_->y());
return ideal_bounds;
}
Expand All @@ -252,5 +431,56 @@ bool BraveCompoundTabContainer::ShouldShowVerticalTabs() const {
tab_slot_controller_->GetBrowser());
}

void BraveCompoundTabContainer::UpdateUnpinnedContainerSize() {
DCHECK(scroll_view_);

auto preferred_size = unpinned_tab_container_->GetPreferredSize();
preferred_size.set_width(scroll_view_->width());
preferred_size.set_height(
std::max(scroll_view_->height(), preferred_size.height()));
if (scroll_view_->contents()->height() != preferred_size.height()) {
scroll_view_->contents()->SetSize(preferred_size);
}
}

void BraveCompoundTabContainer::ScrollTabToBeVisible(int model_index) {
if (!scroll_view_) {
return;
}

auto* tab = GetTabAtModelIndex(model_index);
if (tab->data().pinned) {
return;
}

DCHECK(scroll_view_->contents()->Contains(tab));

gfx::RectF tab_bounds_in_contents_view(tab->GetLocalBounds());
views::View::ConvertRectToTarget(tab, scroll_view_->contents(),
&tab_bounds_in_contents_view);

const auto visible_rect = scroll_view_->GetVisibleRect();
if (visible_rect.Contains(gfx::Rect(0, tab_bounds_in_contents_view.y(),
1 /*in order to ignore width */,
tab_bounds_in_contents_view.height()))) {
return;
}

// Unfortunately, ScrollView's API doesn't work well for us. So we manually
// adjust scroll offset. Note that we change contents view's position as
// we disabled layered scroll view.
if (visible_rect.CenterPoint().y() >=
tab_bounds_in_contents_view.CenterPoint().y()) {
scroll_view_->contents()->SetPosition(
{0, -static_cast<int>(tab_bounds_in_contents_view.y())});
} else {
scroll_view_->contents()->SetPosition(
{0, std::min(0, scroll_view_->height() -
static_cast<int>(
tab_bounds_in_contents_view.bottom() +
tabs::kMarginForVerticalTabContainers))});
}
}

BEGIN_METADATA(BraveCompoundTabContainer, CompoundTabContainer)
END_METADATA
16 changes: 16 additions & 0 deletions browser/ui/views/tabs/brave_compound_tab_container.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

#include "chrome/browser/ui/views/tabs/compound_tab_container.h"

namespace views {
class ScrollView;
} // namespace views

class BraveCompoundTabContainer : public CompoundTabContainer {
public:
METADATA_HEADER(BraveCompoundTabContainer);
Expand All @@ -25,6 +29,8 @@ class BraveCompoundTabContainer : public CompoundTabContainer {
// un pinned tabs.
base::OnceClosure LockLayout();

void SetScrollEnabled(bool enabled);

// CompoundTabContainer:
void SetAvailableWidthCallback(
base::RepeatingCallback<int()> available_width_callback) override;
Expand All @@ -45,10 +51,20 @@ class BraveCompoundTabContainer : public CompoundTabContainer {
BrowserRootView::DropTarget* GetDropTarget(
gfx::Point loc_in_local_coords) override;
void OnThemeChanged() override;
void PaintChildren(const views::PaintInfo& info) override;
void ChildPreferredSizeChanged(views::View* child) override;
void SetActiveTab(absl::optional<size_t> prev_active_index,
absl::optional<size_t> new_active_index) override;

private:
bool ShouldShowVerticalTabs() const;

void UpdateUnpinnedContainerSize();
void ScrollTabToBeVisible(int model_index);

base::raw_ref<TabSlotController> tab_slot_controller_;

base::raw_ptr<views::ScrollView> scroll_view_ = nullptr;
};

#endif // BRAVE_BROWSER_UI_VIEWS_TABS_BRAVE_COMPOUND_TAB_CONTAINER_H_
13 changes: 13 additions & 0 deletions browser/ui/views/tabs/brave_tab.cc
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,17 @@ void BraveTab::UpdateIconVisibility() {
}
}

void BraveTab::ViewHierarchyChanged(
const views::ViewHierarchyChangedDetails& details) {
if (details.child != this) {
return;
}

if (details.is_add && shadow_layer_) {
AddLayerToBelowThis();
}
}

void BraveTab::OnLayerBoundsChanged(const gfx::Rect& old_bounds,
ui::PropertyChangeReason reason) {
Tab::OnLayerBoundsChanged(old_bounds, reason);
Expand Down Expand Up @@ -246,6 +257,8 @@ void BraveTab::ReorderChildLayers(ui::Layer* parent_layer) {

DCHECK_EQ(shadow_layer_->parent(), layer()->parent());
layer()->parent()->StackBelow(shadow_layer_.get(), layer());

LayoutShadowLayer();
}

void BraveTab::MaybeAdjustLeftForPinnedTab(gfx::Rect* bounds,
Expand Down
2 changes: 2 additions & 0 deletions browser/ui/views/tabs/brave_tab.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class BraveTab : public Tab {
void MaybeAdjustLeftForPinnedTab(gfx::Rect* bounds,
int visual_width) const override;

void ViewHierarchyChanged(
const views::ViewHierarchyChangedDetails& details) override;
void OnLayerBoundsChanged(const gfx::Rect& old_bounds,
ui::PropertyChangeReason reason) override;

Expand Down
41 changes: 30 additions & 11 deletions browser/ui/views/tabs/brave_tab_strip.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "chrome/browser/ui/views/tabs/tab_strip_scroll_container.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/views/layout/flex_layout.h"

BraveTabStrip::BraveTabStrip(std::unique_ptr<TabStripController> controller)
Expand Down Expand Up @@ -201,6 +202,9 @@ void BraveTabStrip::UpdateTabContainer() {
const bool is_using_compound_tab_container =
tab_container_->GetClassName() ==
BraveCompoundTabContainer::kViewClassName;
const bool using_sticky_pinned_tabs = base::FeatureList::IsEnabled(
tabs::features::kBraveVerticalTabsStickyPinnedTabs);

base::ScopedClosureRunner layout_lock;
if (should_use_compound_tab_container != is_using_compound_tab_container) {
// Before removing any child, we should complete the 'tab closing animation'
Expand All @@ -214,14 +218,23 @@ void BraveTabStrip::UpdateTabContainer() {
if (should_use_compound_tab_container) {
// Container should be attached before TabDragContext so that dragged
// views can be atop container.
auto* brave_tab_container =
AddChildViewAt(std::make_unique<BraveCompoundTabContainer>(
*this, hover_card_controller_.get(),
GetDragContext(), *this, this),
0);
auto* drag_context = GetDragContext();
auto* brave_tab_container = AddChildViewAt(
std::make_unique<BraveCompoundTabContainer>(
*this, hover_card_controller_.get(), drag_context, *this, this),
0);
tab_container_ = *brave_tab_container;
layout_lock =
base::ScopedClosureRunner(brave_tab_container->LockLayout());

if (using_sticky_pinned_tabs) {
brave_tab_container->SetScrollEnabled(using_vertical_tabs);

// Make dragged views on top of container's layer.
drag_context->SetPaintToLayer();
drag_context->layer()->SetFillsBoundsOpaquely(false);
drag_context->parent()->ReorderChildView(drag_context, -1);
}
} else {
// Container should be attached before TabDragContext so that dragged
// views can be atop container.
Expand All @@ -233,6 +246,10 @@ void BraveTabStrip::UpdateTabContainer() {
tab_container_ = *brave_tab_container;
layout_lock =
base::ScopedClosureRunner(brave_tab_container->LockLayout());

if (using_sticky_pinned_tabs) {
GetDragContext()->DestroyLayer();
}
}

// Resets TabSlotViews for the new TabContainer.
Expand Down Expand Up @@ -306,12 +323,14 @@ void BraveTabStrip::UpdateTabContainer() {
base::Unretained(vertical_region_view)));
}

tab_container_->SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kVertical)
.SetDefault(views::kFlexBehaviorKey,
views::FlexSpecification(
views::MinimumFlexSizeRule::kScaleToMinimumSnapToZero,
views::MaximumFlexSizeRule::kPreferred));
if (!using_sticky_pinned_tabs) {
tab_container_->SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kVertical)
.SetDefault(views::kFlexBehaviorKey,
views::FlexSpecification(
views::MinimumFlexSizeRule::kScaleToMinimumSnapToZero,
views::MaximumFlexSizeRule::kPreferred));
}
} else {
if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) {
auto* browser_view = static_cast<BraveBrowserView*>(
Expand Down