blob: ab68d066b54b11c6fff95d6f21207b296222a36f [file]
// Copyright 2025 The Chromium Authors
// 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/vertical/vertical_split_tab_view.h"
#include <numeric>
#include <vector>
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/views/tabs/tab/glow_hover_controller.h"
#include "chrome/browser/ui/views/tabs/vertical/tab_collection_animating_layout_manager.h"
#include "chrome/browser/ui/views/tabs/vertical/tab_collection_node.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_tab_drag_handler.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_tab_view.h"
#include "components/tabs/public/tab_collection.h"
#include "components/tabs/public/tab_interface.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/border.h"
#include "ui/views/layout/delegating_layout_manager.h"
#include "ui/views/layout/proposed_layout.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
VerticalSplitTabView::VerticalSplitTabView(TabCollectionNode* collection_node)
: collection_node_(collection_node),
hover_controller_(gfx::Animation::ShouldRenderRichAnimation()
? std::make_unique<GlowHoverController>(this)
: nullptr) {
SetLayoutManager(std::make_unique<views::DelegatingLayoutManager>(this));
collection_node->set_detach_child_from_node(
base::BindRepeating(&VerticalSplitTabView::RemoveChildViewForReparenting,
base::Unretained(this)));
node_destroyed_subscription_ =
collection_node_->RegisterWillDestroyCallback(base::BindOnce(
&VerticalSplitTabView::ResetCollectionNode, base::Unretained(this)));
// Ensures this view gets mouse events as well its children.
SetNotifyEnterExitOnChild(true);
}
VerticalSplitTabView::~VerticalSplitTabView() = default;
void VerticalSplitTabView::OnThemeChanged() {
views::View::OnThemeChanged();
UpdateBorder();
}
void VerticalSplitTabView::AddedToWidget() {
paint_as_active_subscription_ =
GetWidget()->RegisterPaintAsActiveChangedCallback(base::BindRepeating(
&VerticalSplitTabView::UpdateBorder, base::Unretained(this)));
OnDataChanged();
UpdateHovered(IsMouseHovered());
}
void VerticalSplitTabView::RemovedFromWidget() {
paint_as_active_subscription_ = {};
}
void VerticalSplitTabView::OnMouseEntered(const ui::MouseEvent& event) {
UpdateHovered(true);
}
void VerticalSplitTabView::OnMouseExited(const ui::MouseEvent& event) {
UpdateHovered(false);
}
void VerticalSplitTabView::OnMouseMoved(const ui::MouseEvent& event) {
// Linux enter/leave events are sometimes flaky, so we don't want to "miss"
// an enter event and fail to hover the tab.
UpdateHovered(true);
}
void VerticalSplitTabView::OnPaint(gfx::Canvas* canvas) {
if (pinned_) {
const std::vector<views::View*> children =
collection_node_ ? collection_node_->GetDirectChildren()
: std::vector<views::View*>();
std::optional<SkColor> background_color =
!children.empty() ? views::AsViewClass<VerticalTabView>(children[0])
->GetBackgroundColor()
: std::nullopt;
if (background_color.has_value()) {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(background_color.value());
const float corner_radius =
GetLayoutConstant(LayoutConstant::kVerticalTabCornerRadius) -
GetInsets().top() / 2.0;
canvas->DrawRoundRect(GetContentsBounds(), corner_radius, flags);
}
}
views::View::OnPaint(canvas);
}
views::ProposedLayout VerticalSplitTabView::CalculateProposedLayout(
const views::SizeBounds& size_bounds) const {
views::ProposedLayout layouts;
int width = 0;
int height = 0;
const std::vector<views::View*> children =
collection_node_ ? collection_node_->GetDirectChildren()
: std::vector<views::View*>();
if (children.size() != 2) {
layouts.host_size = gfx::Size(0, 0);
return layouts;
}
const int border_thickness =
pinned_
? GetLayoutConstant(LayoutConstant::kVerticalTabPinnedBorderThickness)
: 0;
// Layout children in order. Children will have their preferred height and
// fill available width. If unbounded or both children fit on one row they
// will share it, otherwise they will be stacked vertically.
if (!size_bounds.width().is_bounded() ||
size_bounds.width().value() >=
static_cast<int>(
GetLayoutConstant(LayoutConstant::kVerticalTabMinWidth) *
children.size())) {
int x = 0;
for (auto* child : children) {
gfx::Rect bounds = gfx::Rect(child->GetPreferredSize());
bounds.set_x(x);
// Fill available width if bounded.
if (size_bounds.width().is_bounded()) {
bounds.set_width(
x == 0 ? (std::floor(size_bounds.width().value() +
2 * border_thickness - kSplitViewGap) /
2)
: size_bounds.width().value() - x);
}
x += bounds.width() - 2 * border_thickness + (x == 0 ? kSplitViewGap : 0);
height = std::max(height, bounds.height());
layouts.child_layouts.emplace_back(child, child->GetVisible(), bounds);
}
width = x;
} else {
int y = 0;
for (auto* child : children) {
gfx::Rect bounds = gfx::Rect(child->GetPreferredSize());
bounds.set_y(y);
bounds.set_width(size_bounds.width().value());
bounds.set_height(bounds.height());
y +=
bounds.height() - 2 * border_thickness + (y == 0 ? kSplitViewGap : 0);
layouts.child_layouts.emplace_back(child, child->GetVisible(), bounds);
}
width = size_bounds.width().value();
height = y + 2 * border_thickness;
}
layouts.host_size = gfx::Size(width, height);
return layouts;
}
std::optional<BrowserRootView::DropIndex>
VerticalSplitTabView::GetLinkDropIndex(const gfx::Point& loc_in_view) {
if (!collection_node_ || !collection_node_->GetController()) {
return std::nullopt;
}
CHECK_EQ(collection_node_->children().size(), 2ul);
const auto& start_node = collection_node_->children()[0];
gfx::Point start_loc = views::View::ConvertPointToTarget(
start_node->view(), this,
start_node->view()->GetLocalBounds().CenterPoint());
const auto& end_node = collection_node_->children()[1];
gfx::Point end_loc = views::View::ConvertPointToTarget(
end_node->view(), this, end_node->view()->GetLocalBounds().CenterPoint());
VerticalTabDragHandler& drag_handler =
collection_node_->GetController()->GetDragHandler();
// Links can't be dropped between tabs in a split view so just determine the
// closest tab to the drop point.
if ((start_loc - loc_in_view).LengthSquared() <
(end_loc - loc_in_view).LengthSquared()) {
return drag_handler.GetLinkDropIndexForNode(*start_node, std::nullopt);
} else {
return drag_handler.GetLinkDropIndexForNode(*end_node, std::nullopt);
}
}
double VerticalSplitTabView::GetHoverAnimationValue() const {
if (!hover_controller_) {
return hovered_ ? 1.0 : 0.0;
}
return hover_controller_->GetAnimationValue();
}
void VerticalSplitTabView::ResetCollectionNode() {
collection_node_ = nullptr;
}
void VerticalSplitTabView::OnDataChanged() {
const tabs::TabCollection* tab_collection =
std::get<const tabs::TabCollection*>(collection_node_->GetNodeData());
const std::vector<tabs::TabInterface*> tabs =
tab_collection->GetTabsRecursive();
pinned_ = tabs[0]->IsPinned();
UpdateBorder();
}
void VerticalSplitTabView::UpdateBorder() {
if (pinned_) {
const bool is_frame_active =
GetWidget() ? GetWidget()->ShouldPaintAsActive() : true;
SetBorder(views::CreateRoundedRectBorder(
GetLayoutConstant(LayoutConstant::kVerticalTabPinnedBorderThickness),
GetLayoutConstant(LayoutConstant::kVerticalTabCornerRadius),
is_frame_active ? kColorTabDividerFrameActive
: kColorTabDividerFrameInactive));
} else if (GetBorder()) {
SetBorder(nullptr);
}
}
void VerticalSplitTabView::UpdateHovered(bool hovered) {
if (hovered_ == hovered) {
return;
}
hovered_ = hovered;
float radial_highlight_opacity = 1.0f;
for (views::View* child : children()) {
if (auto* tab_view = views::AsViewClass<VerticalTabView>(child)) {
tab_view->UpdateHovered(hovered_);
radial_highlight_opacity = tab_view->radial_highlight_opacity();
}
}
if (hover_controller_) {
if (hovered_) {
hover_controller_->SetSubtleOpacityScale(radial_highlight_opacity);
hover_controller_->Show(TabStyle::ShowHoverStyle::kSubtle);
} else {
hover_controller_->Hide(TabStyle::HideHoverStyle::kGradual);
}
}
SchedulePaint();
}
std::unique_ptr<views::View>
VerticalSplitTabView::RemoveChildViewForReparenting(views::View* child_view) {
DCHECK(std::ranges::contains(children(), child_view));
CHECK(collection_node_);
auto children = collection_node_->GetDirectChildren();
auto source_layout_info = std::make_unique<
TabCollectionAnimatingLayoutManager::SourceLayoutInfo>(
TabCollectionAnimatingLayoutManager::SourceLayoutInfo{
.animation_axis =
TabCollectionAnimatingLayoutManager::AnimationAxis::kHorizontal,
// Note: Tabs are removed from the split view collection from the
// front first so it is necessary to test the number of children
// in the collection when computing the animation direction.
.animation_direction =
(children.size() == 2 && children[0] == child_view)
? TabCollectionAnimatingLayoutManager::AnimationDirection::
kEndToStart
: TabCollectionAnimatingLayoutManager::AnimationDirection::
kStartToEnd,
});
// Ensure we remove the child view before setting source layout info to
// prevent the manager from clearing the metadata.
auto removed_child_view = RemoveChildViewT(child_view);
TabCollectionAnimatingLayoutManager::SetSourceLayoutInfo(
child_view, std::move(source_layout_info));
return removed_child_view;
}
BEGIN_METADATA(VerticalSplitTabView)
END_METADATA