blob: 040c73e6291570103683b99a6323fc0a70ebad1b [file]
// Copyright 2026 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_dragged_tabs_container.h"
#include "base/callback_list.h"
#include "base/containers/flat_set.h"
#include "base/i18n/rtl.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "base/types/to_address.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/tabs/dragging/drag_session_data.h"
#include "chrome/browser/ui/views/tabs/dragging/tab_drag_target.h"
#include "chrome/browser/ui/views/tabs/tab_slot_view.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_group_view.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 "chrome/browser/ui/views/tabs/vertical/vertical_unpinned_tab_container_view.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point3_f.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/layout/proposed_layout.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
namespace {
// Calculates the offset of the source dragged view (i.e. the main view being
// dragged) from the mouse.
gfx::Vector2d GetSourceViewOffsetFromMouse(
views::View& source_dragged_view,
const DragSessionData& session_data) {
views::View* source_slot_view =
session_data.source_view_drag_data()->attached_view;
// The view that initiated the drag may not be the same as the view that
// is being dragged (e.g. dragging a tab group header).
gfx::Vector2d slot_view_offset_to_source =
views::View::ConvertPointToTarget(source_slot_view, &source_dragged_view,
source_slot_view->bounds().origin())
.OffsetFromOrigin();
gfx::Vector2d dragged_view_bounds_offset_from_bounds;
dragged_view_bounds_offset_from_bounds -= slot_view_offset_to_source;
dragged_view_bounds_offset_from_bounds -=
{static_cast<int>(session_data.mouse_offset_to_size_ratios.x() *
source_slot_view->width()),
static_cast<int>(session_data.mouse_offset_to_size_ratios.y() *
source_slot_view->height())};
return dragged_view_bounds_offset_from_bounds;
}
} // namespace
VerticalDraggedTabsContainer::VerticalDraggedTabsContainer(
views::View& host_view,
TabCollectionNode* collection_node,
DragAxes drag_axes,
DragLayout drag_layout)
: host_view_(host_view),
collection_node_(collection_node),
drag_start_animation_(this),
drag_axes_(drag_axes),
drag_layout_(drag_layout) {
host_view_observation_.Observe(&host_view);
node_destroyed_subscription_ = collection_node_->RegisterWillDestroyCallback(
base::BindOnce(&VerticalDraggedTabsContainer::ResetCollectionNode,
base::Unretained(this)));
}
VerticalDraggedTabsContainer::~VerticalDraggedTabsContainer() {
on_will_destroy_callback_list_.Notify();
}
VerticalDraggedTabsContainer& VerticalDraggedTabsContainer::GetTabDragTarget(
const gfx::Point& point_in_screen) {
return *this;
}
TabDragContext* VerticalDraggedTabsContainer::OnTabDragUpdated(
TabDragTarget::DragController& drag_controller,
const gfx::Point& point_in_screen) {
last_drag_point_in_screen_ = point_in_screen;
if (drag_controller.GetAttachedContext() !=
GetDragHandler().GetDragContext()) {
// Do nothing until the drag attaches to this window's context.
return GetDragHandler().GetDragContext();
}
// Used to determine whether the layout should snap into position without
// animating at the end of this drag cycle.
if (dragging_views_.empty()) {
HandleTabDragEnteredContainer();
InitializeDragState(drag_controller);
}
ApplyUpdatesForDragPositionChange();
return GetDragHandler().GetDragContext();
}
void VerticalDraggedTabsContainer::ApplyUpdatesForDragPositionChange() {
gfx::Point point_in_container = views::View::ConvertPointFromScreen(
base::to_address(host_view_), last_drag_point_in_screen_);
gfx::Rect dragged_bounds_in_container =
GetDraggingViewsBoundsAtPoint(point_in_container);
auto* scroll_view = GetScrollViewForContainer();
CHECK(scroll_view);
scroll_handler_.OnDraggedTabPositionUpdated(
*scroll_view, views::View::ConvertRectToTarget(
base::to_address(host_view_), scroll_view,
dragged_bounds_in_container));
HandleTabDragInContainer(dragged_bounds_in_container);
UpdateDraggingViewTransforms(point_in_container);
}
void VerticalDraggedTabsContainer::OnTabDragExited(
const gfx::Point& point_in_screen) {
ResetDragState();
scroll_handler_.StopScrolling();
}
void VerticalDraggedTabsContainer::OnTabDragEnded() {
ResetDragState();
scroll_handler_.StopScrolling();
}
bool VerticalDraggedTabsContainer::CanDropTab() {
return true;
}
void VerticalDraggedTabsContainer::HandleTabDragEnteredContainer() {
CHECK(collection_node_);
GetDragHandler().HandleDraggedTabsIntoNode(*collection_node_);
// We don't need to snap any views here, since the layout manager already
// skips animating dragged views.
// The target layout just needs to be recalculated so that nodes added to this
// container will haver their views tracked in the layout manager.
UpdateTargetLayoutForDrag({});
}
base::CallbackListSubscription
VerticalDraggedTabsContainer::RegisterWillDestroyCallback(
base::OnceClosure callback) {
return on_will_destroy_callback_list_.Add(std::move(callback));
}
void VerticalDraggedTabsContainer::OnViewBoundsChanged(
views::View* observed_view) {
CHECK_EQ(observed_view, base::to_address(host_view_));
// The transformation coordinates are relative to the host view's coordinates,
// so they must be updated as the bounds change to ensure the dragged tabs
// remain at the same point in the screen.
if (IsHandlingDrag()) {
UpdateDraggingViewTransforms(views::View::ConvertPointFromScreen(
base::to_address(host_view_), last_drag_point_in_screen_));
}
}
void VerticalDraggedTabsContainer::AnimationProgressed(
const gfx::Animation* animation) {
CHECK_EQ(animation, &drag_start_animation_);
UpdateDraggingViewTransforms(views::View::ConvertPointFromScreen(
base::to_address(host_view_), last_drag_point_in_screen_));
}
void VerticalDraggedTabsContainer::AnimationEnded(
const gfx::Animation* animation) {
CHECK_EQ(animation, &drag_start_animation_);
host_view_->InvalidateLayout();
}
void VerticalDraggedTabsContainer::InitializeDragState(
TabDragTarget::DragController& controller) {
CHECK(dragging_views_.empty());
auto* scroll_view = GetScrollViewForContainer();
CHECK(scroll_view);
on_scrolled_subscription_ =
scroll_view->AddContentsScrolledCallback(base::BindRepeating(
&VerticalDraggedTabsContainer::ApplyUpdatesForDragPositionChange,
base::Unretained(this)));
tab_strip_padding_ = GetLayoutConstant(
IsTabStripCollapsed()
? LayoutConstant::kVerticalTabStripCollapsedPadding
: LayoutConstant::kVerticalTabStripUncollapsedPadding);
const auto& session_data = controller.GetSessionData();
BuildDragLayout(session_data);
InitializeDragStartAnimation(controller);
}
void VerticalDraggedTabsContainer::InitializeDragStartAnimation(
const TabDragTarget::DragController& controller) {
CHECK(animating_views_start_offsets_.empty());
// Duration of the animation for dragged views to become contiguous.
static constexpr base::TimeDelta kDragStartAnimationDuration =
base::Milliseconds(200);
base::TimeDelta drag_start_animation_duration =
gfx::Animation::RichAnimationDuration(kDragStartAnimationDuration);
static constexpr gfx::Tween::Type kStartDragAnimationTweenType =
gfx::Tween::Type::EASE_IN_OUT;
const auto& drag_handler = GetDragHandler();
base::TimeDelta drag_time_elapsed =
base::TimeTicks::Now() - controller.GetSessionData().drag_start_time;
if (drag_time_elapsed >= drag_start_animation_duration) {
return;
}
gfx::Point point_in_container = views::View::ConvertPointFromScreen(
base::to_address(host_view_), last_drag_point_in_screen_);
gfx::Rect dragged_bounds_in_container =
GetDraggingViewsBoundsAtPointClamped(point_in_container);
const auto* source_dragged_view = drag_handler.ViewFromTabSlot(
controller.GetSessionData().source_view_drag_data()->attached_view);
const auto source_view_target_position = GetDraggingViewPositionForBounds(
source_dragged_view, dragged_bounds_in_container,
dragging_views_.at(source_dragged_view).offset);
// Build the map of dragging views to their offset between their position at
// the start of the drag, and the position they're expected to be at this
// moment.
const bool should_compute_x_offset = IsHorizontalDragSupported();
for (const auto& [dragging_view, visual_data] : dragging_views_) {
auto start_offset_from_source =
drag_handler.GetOffsetFromSourceAtDragStart(dragging_view);
auto target_offset_from_source =
GetDraggingViewPositionForBounds(
dragging_view, dragged_bounds_in_container, visual_data.offset) -
source_view_target_position;
auto animation_offset =
*start_offset_from_source - target_offset_from_source;
if (!should_compute_x_offset) {
animation_offset.set_x(0);
}
if (animation_offset != gfx::Vector2d()) {
animating_views_start_offsets_.insert({dragging_view, animation_offset});
}
}
// Don't start the animation if there are no offsets to animate.
if (animating_views_start_offsets_.empty()) {
return;
}
drag_start_animation_.SetTweenType(kStartDragAnimationTweenType);
drag_start_animation_.SetSlideDuration(drag_start_animation_duration);
// Set the animations value to be proportional according to the time that
// elapsed since the drag started. This makes the transition between
// containers smoother.
drag_start_animation_.Reset(gfx::Tween::CalculateValue(
kStartDragAnimationTweenType,
drag_time_elapsed.InMillisecondsF() /
drag_start_animation_duration.InMilliseconds()));
drag_start_animation_.Show();
}
void VerticalDraggedTabsContainer::BuildDragLayout(
const DragSessionData& session_data) {
auto* source_dragged_view = GetDragHandler().ViewFromTabSlot(
session_data.source_view_drag_data()->attached_view);
CHECK(source_dragged_view);
dragging_views_bounds_ = gfx::Rect();
dragging_views_bounds_.Offset(
GetSourceViewOffsetFromMouse(*source_dragged_view, session_data));
const auto& target_layout = GetLayoutForDrag();
for (auto* attached_view : session_data.attached_views()) {
auto* dragging_view = GetDragHandler().ViewFromTabSlot(attached_view);
CHECK(dragging_view);
if (dragging_view->parent() != base::to_address(host_view_)) {
continue;
}
if (dragging_views_.contains(dragging_view)) {
// It's possible that multiple dragged tabs map to the same dragged view
// (e.g., split tabs). Skip the duplicates.
continue;
}
const bool is_source_view = dragging_view == source_dragged_view;
const auto* dragging_view_layout =
target_layout.GetLayoutFor(dragging_view);
CHECK(dragging_view_layout);
switch (drag_layout_) {
case DragLayout::kVertical:
CHECK(!IsHorizontalDragSupported());
AddViewToVerticalDragLayout(dragging_view, dragging_view_layout->bounds,
is_source_view);
break;
case DragLayout::kSquash:
AddViewToSquashedDragLayout(dragging_view, dragging_view_layout->bounds,
is_source_view);
break;
default:
NOTREACHED();
}
}
}
void VerticalDraggedTabsContainer::AddViewToVerticalDragLayout(
views::View* dragging_view,
const gfx::Rect& view_bounds,
bool is_source_dragged_view) {
gfx::Rect bounds = view_bounds;
bounds.set_y(dragging_views_bounds_.height());
dragging_views_.insert(
{dragging_view, {.offset = bounds.OffsetFromOrigin()}});
static constexpr int kDraggedViewVerticalPadding = 2;
dragging_views_bounds_.set_height(dragging_views_bounds_.height() +
bounds.height() +
kDraggedViewVerticalPadding);
if (is_source_dragged_view) {
dragging_views_bounds_.Offset({-1 * bounds.x(), -1 * bounds.y()});
}
}
void VerticalDraggedTabsContainer::AddViewToSquashedDragLayout(
views::View* dragging_view,
const gfx::Rect& view_bounds,
bool is_source_dragged_view) {
if (is_source_dragged_view) {
dragging_views_bounds_.set_size(view_bounds.size());
}
dragging_views_.insert(
{dragging_view,
{.offset = gfx::Vector2d(), .should_hide = !is_source_dragged_view}});
}
void VerticalDraggedTabsContainer::ResetDragState() {
// Don't immediately clear `dragging_views_` so that the host view has a
// chance to lay the dragged views out at their expected positions rather
// than relying on `VerticalDraggedTabsContainer` to lay them out with
// transforms.
for (auto& [view, visual_data] : dragging_views_) {
view->SetTransform(gfx::Transform());
// The next layout update should allow the view to be shown by the host.
visual_data.should_hide = false;
}
// The dragged view's bounds need to be snapped. While dragging, the bounds
// are set to (0,0), but afterward the drag, the bounds must be updated to
// the actual position, without animating.
UpdateTargetLayoutForDrag(GetDraggingViews());
dragging_views_.clear();
animating_views_start_offsets_.clear();
drag_start_animation_.Reset(0.0);
dragging_views_bounds_ = gfx::Rect();
on_scrolled_subscription_.reset();
}
void VerticalDraggedTabsContainer::UpdateDraggingViewTransforms(
const gfx::Point& point_in_container) {
const gfx::Rect bounding_box_for_point =
GetDraggingViewsBoundsAtPointClamped(point_in_container);
for (auto& [dragged_view, visual_data] : dragging_views_) {
if (visual_data.should_hide && !drag_start_animation_.is_animating()) {
continue;
}
// Use a transformation to render the dragged views, offset from the
// container's origin.
gfx::Transform transform;
transform.Translate(GetDraggingViewPositionForBounds(
dragged_view, bounding_box_for_point, visual_data.offset));
dragged_view->SetTransform(transform);
}
}
gfx::Rect VerticalDraggedTabsContainer::GetDraggingViewsBoundsAtPoint(
const gfx::Point& point_in_container) const {
gfx::Rect bounding_box_for_point = dragging_views_bounds_;
bounding_box_for_point.Offset(point_in_container.OffsetFromOrigin());
return bounding_box_for_point;
}
gfx::Rect VerticalDraggedTabsContainer::GetDraggingViewsBounds() const {
gfx::Rect box_for_point = dragging_views_bounds_;
box_for_point.Offset(
views::View::ConvertPointFromScreen(base::to_address(host_view_),
last_drag_point_in_screen_)
.OffsetFromOrigin());
return box_for_point;
}
gfx::Vector2d VerticalDraggedTabsContainer::GetDraggingViewPositionForBounds(
const views::View* dragging_view,
const gfx::Rect& dragging_views_bounding_box,
const gfx::Vector2d& target_offset) const {
gfx::Vector2d target(IsHorizontalDragSupported()
? dragging_views_bounding_box.x() + target_offset.x()
: 0,
dragging_views_bounding_box.y() + target_offset.y());
double value = drag_start_animation_.GetCurrentValue();
if (drag_start_animation_.is_animating()) {
if (auto it = animating_views_start_offsets_.find(dragging_view);
it != animating_views_start_offsets_.end()) {
target += {gfx::Tween::IntValueBetween(value, it->second.x(), 0),
gfx::Tween::IntValueBetween(value, it->second.y(), 0)};
// If applying an offset for the drag-start animation, ensure we clamp
// the offsets to the scroll view bounds.
const auto* scroll_view = GetScrollViewForContainer();
CHECK(scroll_view);
gfx::Rect clamping_bounds = views::View::ConvertRectToTarget(
scroll_view, base::to_address(host_view_),
scroll_view->GetLocalBounds());
gfx::Rect view_bounds(gfx::PointAtOffsetFromOrigin(target),
dragging_view->size());
view_bounds.AdjustToFit(clamping_bounds);
target = view_bounds.OffsetFromOrigin();
}
}
return target;
}
gfx::Rect VerticalDraggedTabsContainer::GetDraggingViewsBoundsAtPointClamped(
const gfx::Point& point_in_container) const {
gfx::Rect bounding_box_for_point =
GetDraggingViewsBoundsAtPoint(point_in_container);
const auto* scroll_view = GetScrollViewForContainer();
CHECK(scroll_view);
gfx::Rect clamping_bounds = views::View::ConvertRectToTarget(
scroll_view, base::to_address(host_view_), scroll_view->GetLocalBounds());
clamping_bounds.set_width(clamping_bounds.width() - tab_strip_padding_);
bounding_box_for_point.AdjustToFit(clamping_bounds);
return bounding_box_for_point;
}
std::optional<VerticalDraggedTabsContainer::DraggedViewVisualData>
VerticalDraggedTabsContainer::GetVisualDataForDraggedView(
const views::View& view) const {
auto it = dragging_views_.find(&view);
if (it == dragging_views_.end()) {
return std::nullopt;
}
// Views that should be hidden are still shown while animating into position,
// but are set to "float" so that surrounding views may also animate into
// the end position.
const bool should_hide =
it->second.should_hide && !drag_start_animation_.is_animating();
const bool should_float =
it->second.should_hide && drag_start_animation_.is_animating();
if (view.GetTransform().IsIdentity()) {
// If a drag recently ended the child will still be in
// `dragging_views_` but will not have a transformation, which let's
// the tab view animate into its correct slot.
const gfx::Point point_in_container = views::View::ConvertPointFromScreen(
base::to_address(host_view_), last_drag_point_in_screen_);
const gfx::Rect bounding_box_for_point =
GetDraggingViewsBoundsAtPointClamped(point_in_container);
return std::make_optional(DraggedViewVisualData{
.offset = GetDraggingViewPositionForBounds(
&view, bounding_box_for_point, it->second.offset),
.should_hide = should_hide,
.should_float = should_float,
});
}
// If the tab is being dragged, then it is rendered using
// transformations, offset from the container's origin.
return DraggedViewVisualData{
.offset = gfx::Vector2d(),
.should_hide = should_hide,
.should_float = should_float,
};
}
views::View* VerticalDraggedTabsContainer::GetViewForDragBounds(
const views::ProposedLayout& layout,
const gfx::Rect& dragged_tab_bounds) {
gfx::Rect logical_drag_bounds = dragged_tab_bounds;
if (base::i18n::IsRTL()) {
logical_drag_bounds.set_x(
host_view_->GetMirroredXForRect(logical_drag_bounds));
}
for (const auto& child_layout : layout.child_layouts) {
if (!child_layout.visible ||
GetDragHandler().IsViewDragging(*child_layout.child_view)) {
continue;
}
// The percentage overlap between dragged tabs and the view at its
// target position to be considered the view over current drag bounds.
constexpr float kEntryThreshold = 0.6f;
if (HasMinimumOverlap(logical_drag_bounds, child_layout.bounds,
IsHorizontalDragSupported()
? std::make_optional(child_layout.bounds.width() *
kEntryThreshold)
: std::nullopt,
child_layout.bounds.height() * kEntryThreshold)) {
return child_layout.child_view;
}
}
return nullptr;
}
bool VerticalDraggedTabsContainer::IsHorizontalDragSupported() const {
return drag_axes_ != DragAxes::kVerticalOnly;
}
bool VerticalDraggedTabsContainer::HasMinimumOverlap(
const gfx::Rect& a,
const gfx::Rect& b,
std::optional<int> min_x_overlap,
std::optional<int> min_y_overlap) const {
if (min_y_overlap) {
gfx::RangeF vertical_overlap =
gfx::RangeF(a.y(), a.bottom())
.Intersect(gfx::RangeF(b.y(), b.bottom()));
if (vertical_overlap.length() < *min_y_overlap) {
return false;
}
}
if (min_x_overlap) {
gfx::RangeF horizontal_overlap =
gfx::RangeF(a.x(), a.right()).Intersect(gfx::RangeF(b.x(), b.right()));
if (horizontal_overlap.length() < *min_x_overlap) {
return false;
}
}
// Neither checks failed, so return `true`.
return true;
}
bool VerticalDraggedTabsContainer::IsHandlingDrag() const {
return !dragging_views_.empty();
}
VerticalTabDragHandler& VerticalDraggedTabsContainer::GetDragHandler() {
return const_cast<VerticalTabDragHandler&>(
std::as_const(*this).GetDragHandler());
}
const VerticalTabDragHandler& VerticalDraggedTabsContainer::GetDragHandler()
const {
CHECK(collection_node_);
CHECK(collection_node_->GetController());
return collection_node_->GetController()->GetDragHandler();
}
bool VerticalDraggedTabsContainer::IsTabStripCollapsed() const {
CHECK(collection_node_);
const auto* controller = collection_node_->GetController();
return controller && controller->IsCollapsed();
}
void VerticalDraggedTabsContainer::ResetCollectionNode() {
collection_node_ = nullptr;
}
std::vector<const views::View*> VerticalDraggedTabsContainer::GetDraggingViews()
const {
std::vector<const views::View*> views;
views.reserve(dragging_views_.size());
std::transform(dragging_views_.begin(), dragging_views_.end(),
std::back_inserter(views),
[](const auto& entry) { return entry.first; });
return views;
}