diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index 6da0657a8561..e3548937be26 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -798,6 +798,8 @@ source_set("ui") { "views/brave_actions/brave_shields_action_view.h", "views/frame/vertical_tab_strip_region_view.cc", "views/frame/vertical_tab_strip_region_view.h", + "views/frame/vertical_tab_strip_root_view.cc", + "views/frame/vertical_tab_strip_root_view.h", "views/frame/vertical_tab_strip_widget_delegate_view.cc", "views/frame/vertical_tab_strip_widget_delegate_view.h", "views/location_bar/brave_location_bar_view.cc", diff --git a/browser/ui/views/frame/vertical_tab_strip_root_view.cc b/browser/ui/views/frame/vertical_tab_strip_root_view.cc new file mode 100644 index 000000000000..80adb9eb83e9 --- /dev/null +++ b/browser/ui/views/frame/vertical_tab_strip_root_view.cc @@ -0,0 +1,71 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/ui/views/frame/vertical_tab_strip_root_view.h" + +#include "chrome/browser/ui/views/frame/browser_root_view.h" +#include "ui/base/metadata/metadata_impl_macros.h" +#include "ui/views/view_utils.h" + +VerticalTabStripRootView::VerticalTabStripRootView(BrowserView* browser_view, + views::Widget* widget) + : BrowserRootView(browser_view, widget) {} + +VerticalTabStripRootView::~VerticalTabStripRootView() = default; + +bool VerticalTabStripRootView::OnMousePressed(const ui::MouseEvent& event) { +#if defined(USE_AURA) + const bool result = RootView::OnMousePressed(event); + auto* focus_manager = GetFocusManager(); + DCHECK(focus_manager); + + // When vertical tab strip area is clicked, shortcut handling process + // could get broken on Windows. There are 2 paths where shortcut is handled. + // One is BrowserView::AcceleratorPressed(), and the other is + // BrowserView::PreHandleKeyboardEvent(). When web view has focus, the + // first doesn't deal with it and the latter is responsible for the + // shortcuts. when users click the vertical tab strip area with web view + // focused, both path don't handle it. This is because focused view state of + // views/ framework and focused native window state of Aura is out of sync. + // So as a workaround, resets the focused view state so that shortcuts can + // be handled properly. This shouldn't change the actually focused view, and + // is just reset the status. + // https://github.com/brave/brave-browser/issues/28090 + // https://github.com/brave/brave-browser/issues/27812 + if (auto* focused_view = focus_manager->GetFocusedView(); + focused_view && views::IsViewClass(focused_view)) { + focus_manager->ClearFocus(); + focus_manager->RestoreFocusedView(); + } + + return result; +#else + // On Mac, the parent widget doesn't get activated in this case. Then + // shortcut handling could malfunction. So activate it. + // https://github.com/brave/brave-browser/issues/29993 + auto* widget = GetWidget(); + DCHECK(widget); + widget = widget->GetTopLevelWidget(); + widget->Activate(); + + return RootView::OnMousePressed(event); +#endif +} + +bool VerticalTabStripRootView::OnMouseWheel(const ui::MouseWheelEvent& event) { + return views::internal::RootView::OnMouseWheel(event); +} + +void VerticalTabStripRootView::OnMouseExited(const ui::MouseEvent& event) { + views::internal::RootView::OnMouseExited(event); +} + +void VerticalTabStripRootView::PaintChildren( + const views::PaintInfo& paint_info) { + views::internal::RootView::PaintChildren(paint_info); +} + +BEGIN_METADATA(VerticalTabStripRootView, BrowserRootView) +END_METADATA diff --git a/browser/ui/views/frame/vertical_tab_strip_root_view.h b/browser/ui/views/frame/vertical_tab_strip_root_view.h new file mode 100644 index 000000000000..eb2b1b76153b --- /dev/null +++ b/browser/ui/views/frame/vertical_tab_strip_root_view.h @@ -0,0 +1,32 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#ifndef BRAVE_BROWSER_UI_VIEWS_FRAME_VERTICAL_TAB_STRIP_ROOT_VIEW_H_ +#define BRAVE_BROWSER_UI_VIEWS_FRAME_VERTICAL_TAB_STRIP_ROOT_VIEW_H_ + +#include "chrome/browser/ui/views/frame/browser_root_view.h" + +// `VerticalTabStripRootView` extends `BrowserRootView` to support link +// drag-and-drop feature. In order to avoid features other than that, replaces +// mouse event callbacks and bypass the `BrowserRootView`'s implementation. e.g. +// OnMouseWheel() +class VerticalTabStripRootView : public BrowserRootView { + public: + METADATA_HEADER(VerticalTabStripRootView); + + VerticalTabStripRootView(BrowserView* browser_view, views::Widget* widget); + + ~VerticalTabStripRootView() override; + + // BrowserRootView: + bool OnMousePressed(const ui::MouseEvent& event) override; + bool OnMouseWheel(const ui::MouseWheelEvent& event) override; + void OnMouseExited(const ui::MouseEvent& event) override; + + protected: + void PaintChildren(const views::PaintInfo& paint_info) override; +}; + +#endif // BRAVE_BROWSER_UI_VIEWS_FRAME_VERTICAL_TAB_STRIP_ROOT_VIEW_H_ diff --git a/browser/ui/views/frame/vertical_tab_strip_root_view_browsertest.cc b/browser/ui/views/frame/vertical_tab_strip_root_view_browsertest.cc new file mode 100644 index 000000000000..b6137d62e7cf --- /dev/null +++ b/browser/ui/views/frame/vertical_tab_strip_root_view_browsertest.cc @@ -0,0 +1,158 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "brave/browser/ui/browser_commands.h" +#include "brave/browser/ui/views/frame/brave_browser_view.h" +#include "brave/browser/ui/views/frame/vertical_tab_strip_region_view.h" +#include "brave/browser/ui/views/frame/vertical_tab_strip_root_view.h" +#include "brave/browser/ui/views/frame/vertical_tab_strip_widget_delegate_view.h" +#include "brave/browser/ui/views/tabs/vertical_tab_utils.h" +#include "chrome/browser/ui/views/frame/browser_non_client_frame_view.h" +#include "chrome/browser/ui/views/frame/browser_view.h" +#include "chrome/browser/ui/views/tabs/tab_strip.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "content/public/test/browser_test.h" +#include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/dragdrop/drop_target_event.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" +#include "ui/base/dragdrop/os_exchange_data.h" +#include "ui/compositor/layer_tree_owner.h" + +class VerticalTabStripRootViewBrowserTest : public InProcessBrowserTest { + public: + VerticalTabStripRootViewBrowserTest() = default; + VerticalTabStripRootViewBrowserTest( + const VerticalTabStripRootViewBrowserTest&) = delete; + VerticalTabStripRootViewBrowserTest& operator=( + const VerticalTabStripRootViewBrowserTest&) = delete; + ~VerticalTabStripRootViewBrowserTest() override = default; + + Tab* GetTabAt(int index) { return tab_strip()->tab_at(index); } + + BrowserView* browser_view() { + return BrowserView::GetBrowserViewForBrowser(browser()); + } + + TabStrip* tab_strip() { return browser_view()->tabstrip(); } + + VerticalTabStripRootView* vtab_strip_root_view() { + if (vtab_tab_strip_widget_delegate_view()) { + return static_cast( + vtab_tab_strip_widget_delegate_view()->GetWidget()->GetRootView()); + } + return nullptr; + } + + BrowserNonClientFrameView* browser_non_client_frame_view() { + return browser_view()->frame()->GetFrameView(); + } + + void ToggleVerticalTabStrip() { + brave::ToggleVerticalTabStrip(browser()); + browser_non_client_frame_view()->Layout(); + } + + VerticalTabStripWidgetDelegateView* vtab_tab_strip_widget_delegate_view() { + auto* browser_view = static_cast( + BrowserView::GetBrowserViewForBrowser(browser())); + if (browser_view) { + return browser_view->vertical_tab_strip_widget_delegate_view(); + } + return nullptr; + } +}; + +#if BUILDFLAG(IS_WIN) +// This test is flaky on Windows. +#define MAYBE_DragAfterCurrentTab DISABLED_DragAfterCurrentTab +#else +#define MAYBE_DragAfterCurrentTab DragAfterCurrentTab +#endif + +IN_PROC_BROWSER_TEST_F(VerticalTabStripRootViewBrowserTest, + MAYBE_DragAfterCurrentTab) { + ToggleVerticalTabStrip(); + + ASSERT_TRUE(tabs::utils::ShouldShowVerticalTabs(browser())); + + auto* tab_strip_model = browser()->tab_strip_model(); + EXPECT_EQ(tab_strip_model->count(), 1); + + ui::OSExchangeData data; + GURL url("https://brave.com/"); + data.SetURL(url, std::u16string()); + + Tab* current_tab = GetTabAt(0); + gfx::Point location; + views::View::ConvertPointToWidget(current_tab, &location); + + // To drag after current tab. + location.Offset(0, current_tab->height()); + ui::DropTargetEvent event(data, gfx::PointF(location), gfx::PointF(location), + ui::DragDropTypes::DRAG_COPY); + + VerticalTabStripRootView* root_view = vtab_strip_root_view(); + + EXPECT_NE(root_view, nullptr); + root_view->OnDragUpdated(event); + auto drop_cb = root_view->GetDropCallback(event); + ui::mojom::DragOperation output_drag_op = ui::mojom::DragOperation::kNone; + std::move(drop_cb).Run(event, output_drag_op, + /*drag_image_layer_owner=*/nullptr); + + EXPECT_EQ(output_drag_op, ui::mojom::DragOperation::kCopy); + EXPECT_EQ(tab_strip_model->count(), 2); + EXPECT_TRUE(browser() + ->tab_strip_model() + ->GetWebContentsAt(1) + ->GetURL() + .EqualsIgnoringRef(url)); +} + +#if BUILDFLAG(IS_WIN) +// This test is flaky on Windows. +#define MAYBE_DragOnCurrentTab DISABLED_DragOnCurrentTab +#else +#define MAYBE_DragOnCurrentTab DragOnCurrentTab +#endif +IN_PROC_BROWSER_TEST_F(VerticalTabStripRootViewBrowserTest, + MAYBE_DragOnCurrentTab) { + ToggleVerticalTabStrip(); + + ASSERT_TRUE(tabs::utils::ShouldShowVerticalTabs(browser())); + + auto* tab_strip_model = browser()->tab_strip_model(); + EXPECT_EQ(tab_strip_model->count(), 1); + + ui::OSExchangeData data; + GURL url("https://brave.com/"); + data.SetURL(url, std::u16string()); + + Tab* current_tab = GetTabAt(0); + gfx::Point location; + views::View::ConvertPointToWidget(current_tab, &location); + + // To drag on the same tab. + location.Offset(0, current_tab->height() / 2); + ui::DropTargetEvent event(data, gfx::PointF(location), gfx::PointF(location), + ui::DragDropTypes::DRAG_COPY); + + VerticalTabStripRootView* root_view = vtab_strip_root_view(); + + EXPECT_NE(root_view, nullptr); + root_view->OnDragUpdated(event); + auto drop_cb = root_view->GetDropCallback(event); + ui::mojom::DragOperation output_drag_op = ui::mojom::DragOperation::kNone; + std::move(drop_cb).Run(event, output_drag_op, + /*drag_image_layer_owner=*/nullptr); + + EXPECT_EQ(output_drag_op, ui::mojom::DragOperation::kCopy); + EXPECT_EQ(tab_strip_model->count(), 1); + EXPECT_TRUE(browser() + ->tab_strip_model() + ->GetWebContentsAt(0) + ->GetURL() + .EqualsIgnoringRef(url)); +} diff --git a/browser/ui/views/frame/vertical_tab_strip_widget_delegate_view.cc b/browser/ui/views/frame/vertical_tab_strip_widget_delegate_view.cc index 2fa7cc6599c1..33ad2f017143 100644 --- a/browser/ui/views/frame/vertical_tab_strip_widget_delegate_view.cc +++ b/browser/ui/views/frame/vertical_tab_strip_widget_delegate_view.cc @@ -10,10 +10,12 @@ #include "base/check.h" #include "brave/browser/ui/views/frame/brave_browser_view.h" #include "brave/browser/ui/views/frame/vertical_tab_strip_region_view.h" +#include "brave/browser/ui/views/frame/vertical_tab_strip_root_view.h" #include "brave/browser/ui/views/tabs/vertical_tab_utils.h" #include "chrome/browser/ui/browser_list.h" #include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h" #include "chrome/browser/ui/exclusive_access/fullscreen_controller.h" +#include "chrome/browser/ui/views/frame/browser_root_view.h" #include "chrome/browser/ui/views/frame/browser_view.h" #include "chrome/browser/ui/views/theme_copying_widget.h" #include "chrome/common/pref_names.h" @@ -27,66 +29,19 @@ namespace { -class VerticalTabStripRootView : public views::internal::RootView { - public: - METADATA_HEADER(VerticalTabStripRootView); - - using RootView::RootView; - ~VerticalTabStripRootView() override = default; - - // views::internal::RootView: - bool OnMousePressed(const ui::MouseEvent& event) override { -#if defined(USE_AURA) - const bool result = RootView::OnMousePressed(event); - auto* focus_manager = GetFocusManager(); - DCHECK(focus_manager); - - // When vertical tab strip area is clicked, shortcut handling process - // could get broken on Windows. There are 2 paths where shortcut is handled. - // One is BrowserView::AcceleratorPressed(), and the other is - // BrowserView::PreHandleKeyboardEvent(). When web view has focus, the - // first doesn't deal with it and the latter is responsible for the - // shortcuts. when users click the vertical tab strip area with web view - // focused, both path don't handle it. This is because focused view state of - // views/ framework and focused native window state of Aura is out of sync. - // So as a workaround, resets the focused view state so that shortcuts can - // be handled properly. This shouldn't change the actually focused view, and - // is just reset the status. - // https://github.com/brave/brave-browser/issues/28090 - // https://github.com/brave/brave-browser/issues/27812 - if (auto* focused_view = focus_manager->GetFocusedView(); - focused_view && views::IsViewClass(focused_view)) { - focus_manager->ClearFocus(); - focus_manager->RestoreFocusedView(); - } - - return result; -#else - // On Mac, the parent widget doesn't get activated in this case. Then - // shortcut handling could malfunction. So activate it. - // https://github.com/brave/brave-browser/issues/29993 - auto* widget = GetWidget(); - DCHECK(widget); - widget = widget->GetTopLevelWidget(); - widget->Activate(); - - return RootView::OnMousePressed(event); -#endif - } -}; - -BEGIN_METADATA(VerticalTabStripRootView, views::internal::RootView) -END_METADATA - class VerticalTabStripWidget : public ThemeCopyingWidget { public: - using ThemeCopyingWidget::ThemeCopyingWidget; + VerticalTabStripWidget(BrowserView* browser_view, views::Widget* widget) + : ThemeCopyingWidget(widget), browser_view_(browser_view) {} ~VerticalTabStripWidget() override = default; // ThemeCopyingWidget: views::internal::RootView* CreateRootView() override { - return new VerticalTabStripRootView(this); + return new VerticalTabStripRootView(browser_view_, this); } + + private: + raw_ptr browser_view_; }; } // namespace @@ -108,8 +63,8 @@ VerticalTabStripWidgetDelegateView* VerticalTabStripWidgetDelegateView::Create( // not get focus. params.activatable = views::Widget::InitParams::Activatable::kNo; - auto widget = - std::make_unique(browser_view->GetWidget()); + auto widget = std::make_unique( + browser_view, browser_view->GetWidget()); widget->Init(std::move(params)); #if defined(USE_AURA) widget->GetNativeView()->SetProperty(views::kHostViewKey, host_view); @@ -160,8 +115,9 @@ void VerticalTabStripWidgetDelegateView::AddedToWidget() { void VerticalTabStripWidgetDelegateView::ChildPreferredSizeChanged( views::View* child) { - if (!host_) + if (!host_) { return; + } // Setting minimum size for |host_| so that we can overlay vertical tabs over // the web view. @@ -179,13 +135,15 @@ void VerticalTabStripWidgetDelegateView::OnViewVisibilityChanged( views::View* observed_view, views::View* starting_view) { auto* widget = GetWidget(); - if (!widget || widget->IsVisible() == observed_view->GetVisible()) + if (!widget || widget->IsVisible() == observed_view->GetVisible()) { return; + } - if (observed_view->GetVisible()) + if (observed_view->GetVisible()) { widget->Show(); - else + } else { widget->Hide(); + } } void VerticalTabStripWidgetDelegateView::OnViewBoundsChanged( @@ -243,12 +201,14 @@ void VerticalTabStripWidgetDelegateView::OnWidgetBoundsChanged( } void VerticalTabStripWidgetDelegateView::UpdateWidgetBounds() { - if (!host_) + if (!host_) { return; + } auto* widget = GetWidget(); - if (!widget) + if (!widget) { return; + } // Convert coordinate system based on Browser's widget. gfx::Rect widget_bounds = host_->ConvertRectToWidget(host_->GetLocalBounds()); @@ -274,8 +234,9 @@ void VerticalTabStripWidgetDelegateView::UpdateWidgetBounds() { widget->Show(); } - if (need_to_call_layout) + if (need_to_call_layout) { Layout(); + } #if BUILDFLAG(IS_MAC) UpdateClip(); diff --git a/browser/ui/views/tabs/brave_compound_tab_container.cc b/browser/ui/views/tabs/brave_compound_tab_container.cc index 5b6690789508..4dc735227daf 100644 --- a/browser/ui/views/tabs/brave_compound_tab_container.cc +++ b/browser/ui/views/tabs/brave_compound_tab_container.cc @@ -191,8 +191,9 @@ void BraveCompoundTabContainer::TransferTabBetweenContainers( layout_dirty = true; } - if (layout_dirty) + if (layout_dirty) { Layout(); + } } void BraveCompoundTabContainer::Layout() { @@ -299,6 +300,38 @@ int BraveCompoundTabContainer::GetUnpinnedContainerIdealLeadingX() const { return 0; } +BrowserRootView::DropIndex BraveCompoundTabContainer::GetDropIndex( + const ui::DropTargetEvent& event) { + if (!ShouldShowVerticalTabs()) { + return CompoundTabContainer::GetDropIndex(event); + } + + TabContainer* sub_drop_target = GetTabContainerAt(event.location()); + CHECK(sub_drop_target); + CHECK(sub_drop_target->GetDropTarget( + ConvertPointToTarget(this, sub_drop_target, event.location()))); + + // Convert to `sub_drop_target`'s local coordinate space. + const gfx::Point loc_in_sub_target = ConvertPointToTarget( + this, sub_drop_target->GetViewForDrop(), event.location()); + const ui::DropTargetEvent adjusted_event = ui::DropTargetEvent( + event.data(), gfx::PointF(loc_in_sub_target), + gfx::PointF(loc_in_sub_target), event.source_operations()); + + if (sub_drop_target == base::to_address(pinned_tab_container_)) { + // Pinned tab container shares an index and coordinate space, so no + // adjustments needed. + return sub_drop_target->GetDropIndex(adjusted_event); + } else { + // For the unpinned container, we need to transform the output to the + // correct index space. + const BrowserRootView::DropIndex sub_target_index = + sub_drop_target->GetDropIndex(adjusted_event); + return {sub_target_index.value + NumPinnedTabs(), + sub_target_index.drop_before, sub_target_index.drop_in_group}; + } +} + BrowserRootView::DropTarget* BraveCompoundTabContainer::GetDropTarget( gfx::Point loc_in_local_coords) { if (!ShouldShowVerticalTabs()) { @@ -312,7 +345,11 @@ BrowserRootView::DropTarget* BraveCompoundTabContainer::GetDropTarget( return nullptr; } - return GetTabContainerAt(loc_in_local_coords); + if (GetTabContainerAt(loc_in_local_coords)) { + return this; + } + + return nullptr; } void BraveCompoundTabContainer::OnThemeChanged() { diff --git a/browser/ui/views/tabs/brave_compound_tab_container.h b/browser/ui/views/tabs/brave_compound_tab_container.h index 921b46b651de..b23b3633c079 100644 --- a/browser/ui/views/tabs/brave_compound_tab_container.h +++ b/browser/ui/views/tabs/brave_compound_tab_container.h @@ -48,14 +48,18 @@ class BraveCompoundTabContainer : public CompoundTabContainer { gfx::Point point_in_local_coords) const override; gfx::Rect ConvertUnpinnedContainerIdealBoundsToLocal( gfx::Rect ideal_bounds) const override; - 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 prev_active_index, absl::optional new_active_index) override; + // BrowserRootView::DropTarget + BrowserRootView::DropTarget* GetDropTarget( + gfx::Point loc_in_local_coords) override; + BrowserRootView::DropIndex GetDropIndex( + const ui::DropTargetEvent& event) override; + private: bool ShouldShowVerticalTabs() const; diff --git a/browser/ui/views/tabs/brave_tab_container.cc b/browser/ui/views/tabs/brave_tab_container.cc index 1abe01de79b2..9ad4b399f548 100644 --- a/browser/ui/views/tabs/brave_tab_container.cc +++ b/browser/ui/views/tabs/brave_tab_container.cc @@ -6,10 +6,15 @@ #include "brave/browser/ui/views/tabs/brave_tab_container.h" #include +#include #include #include "base/check_is_test.h" +#include "base/containers/flat_map.h" #include "brave/browser/ui/tabs/brave_tab_prefs.h" +#include "brave/browser/ui/tabs/features.h" +#include "brave/browser/ui/views/frame/brave_browser_view.h" +#include "brave/browser/ui/views/frame/vertical_tab_strip_widget_delegate_view.h" #include "brave/browser/ui/views/tabs/brave_tab_group_header.h" #include "brave/browser/ui/views/tabs/brave_tab_strip.h" #include "brave/browser/ui/views/tabs/vertical_tab_utils.h" @@ -19,8 +24,12 @@ #include "chrome/browser/ui/tabs/tab_style.h" #include "chrome/browser/ui/ui_features.h" #include "chrome/browser/ui/views/tabs/tab_drag_controller.h" +#include "chrome/grit/theme_resources.h" #include "ui/base/metadata/metadata_impl_macros.h" +#include "ui/display/screen.h" #include "ui/gfx/canvas.h" +#include "ui/gfx/image/image_skia_operations.h" +#include "ui/gfx/skbitmap_operations.h" #include "ui/views/view_utils.h" BraveTabContainer::BraveTabContainer( @@ -35,7 +44,8 @@ BraveTabContainer::BraveTabContainer( tab_slot_controller, scroll_contents_view), drag_context_(static_cast(drag_context)), - tab_style_(TabStyle::Get()) { + tab_style_(TabStyle::Get()), + controller_(controller) { auto* browser = tab_slot_controller_->GetBrowser(); if (!browser) { CHECK_IS_TEST(); @@ -84,8 +94,9 @@ base::OnceClosure BraveTabContainer::LockLayout() { gfx::Size BraveTabContainer::CalculatePreferredSize() const { // Note that we check this before checking currently we're in vertical tab // strip mode. We might be in the middle of changing orientation. - if (layout_locked_) + if (layout_locked_) { return {}; + } if (!tabs::utils::ShouldShowVerticalTabs( tab_slot_controller_->GetBrowser())) { @@ -105,8 +116,9 @@ gfx::Size BraveTabContainer::CalculatePreferredSize() const { // When closing trailing tabs, the last tab's current bottom could be // greater than ideal bounds bottom. Note that closing tabs are not in // tabs_view_model_ so we have to check again here. - for (auto* tab : closing_tabs_) + for (auto* tab : closing_tabs_) { height = std::max(height, tab->bounds().bottom()); + } } const auto slots_bounds = layout_helper_->CalculateIdealBounds( @@ -185,8 +197,9 @@ bool BraveTabContainer::ShouldTabBeVisible(const Tab* tab) const { void BraveTabContainer::StartInsertTabAnimation(int model_index) { // Note that we check this before checking currently we're in vertical tab // strip mode. We might be in the middle of changing orientation. - if (layout_locked_) + if (layout_locked_) { return; + } if (!tabs::utils::ShouldShowVerticalTabs( tab_slot_controller_->GetBrowser())) { @@ -229,8 +242,9 @@ void BraveTabContainer::OnTabCloseAnimationCompleted(Tab* tab) { TabContainerImpl::OnTabCloseAnimationCompleted(tab); // we might have to hide this container entirely - if (!tabs_view_model_.view_size()) + if (!tabs_view_model_.view_size()) { PreferredSizeChanged(); + } } void BraveTabContainer::UpdateLayoutOrientation() { @@ -252,8 +266,9 @@ void BraveTabContainer::OnUnlockLayout() { void BraveTabContainer::CompleteAnimationAndLayout() { // Note that we check this before checking currently we're in vertical tab // strip mode. We might be in the middle of changing orientation. - if (layout_locked_) + if (layout_locked_) { return; + } TabContainerImpl::CompleteAnimationAndLayout(); @@ -293,5 +308,294 @@ void BraveTabContainer::PaintChildren(const views::PaintInfo& paint_info) { } } +BrowserRootView::DropIndex BraveTabContainer::GetDropIndex( + const ui::DropTargetEvent& event) { + if (!tabs::utils::ShouldShowVerticalTabs( + tab_slot_controller_->GetBrowser())) { + return TabContainerImpl::GetDropIndex(event); + } + + // Force animations to stop, otherwise it makes the index calculation tricky. + CompleteAnimationAndLayout(); + + const int x = GetMirroredXInView(event.x()); + const int y = event.y(); + + std::vector views = layout_helper_->GetTabSlotViews(); + + // Loop until we find a tab or group header that intersects |event|'s + // location. + for (TabSlotView* view : views) { + const int max_y = view->y() + view->height(); + const int max_x = view->x() + view->width(); + if (y >= max_y) { + continue; + } + + if (view->GetTabSlotViewType() == TabSlotView::ViewType::kTab) { + Tab* const tab = static_cast(view); + + // Closing tabs should be skipped. + if (tab->closing()) { + continue; + } + + const int model_index = GetModelIndexOf(tab).value(); + + const bool is_tab_pinned = tab->data().pinned; + + // When dropping text or links onto pinned tabs, we need to take the + // x-axis position into consideration. + if (is_tab_pinned && x >= max_x) { + continue; + } + + const bool first_in_group = + tab->group().has_value() && + model_index == controller_->GetFirstTabInGroup(tab->group().value()); + + const int hot_height = tab->height() / 4; + const int hot_width = tab->width() / 4; + + if (is_tab_pinned ? x >= (max_x - hot_width) + : y >= (max_y - hot_height)) { + return {model_index + 1, true /* drop_before */, + false /* drop_in_group */}; + } + + if (is_tab_pinned ? x < tab->x() + hot_width + : y < tab->y() + hot_height) { + return {model_index, true /* drop_before */, first_in_group}; + } + + return {model_index, false /* drop_before */, false /* drop_in_group */}; + } else { + TabGroupHeader* const group_header = static_cast(view); + const int first_tab_index = + controller_->GetFirstTabInGroup(group_header->group().value()) + .value(); + return {first_tab_index, true /* drop_before */, + y >= max_y - group_header->height() / 2 /* drop_in_group */}; + } + } + + // The drop isn't over a tab, add it to the end. + return {GetTabCount(), true, false}; +} + +// BraveTabContainer::DropArrow: +// ---------------------------------------------------------- +BraveTabContainer::DropArrow::DropArrow(const BrowserRootView::DropIndex& index, + Position position, + bool beneath, + views::Widget* context) + : index_(index), position_(position), beneath_(beneath) { + arrow_window_ = new views::Widget; + views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); + params.z_order = ui::ZOrderLevel::kFloatingUIElement; + params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; + params.accept_events = false; + + // All drop images has the same size. + const gfx::ImageSkia* drop_image = + GetDropArrowImage(Position::Horizontal, false); + params.bounds = gfx::Rect(drop_image->width(), drop_image->height()); + + params.context = context->GetNativeWindow(); + arrow_window_->Init(std::move(params)); + arrow_view_ = + arrow_window_->SetContentsView(std::make_unique()); + arrow_view_->SetImage(GetDropArrowImage(position_, beneath_)); + scoped_observation_.Observe(arrow_window_.get()); + + arrow_window_->Show(); +} + +BraveTabContainer::DropArrow::~DropArrow() { + // Close eventually deletes the window, which deletes arrow_view too. + if (arrow_window_) { + arrow_window_->Close(); + } +} + +void BraveTabContainer::DropArrow::SetBeneath(bool beneath) { + if (beneath_ == beneath) { + return; + } + + beneath_ = beneath; + arrow_view_->SetImage(GetDropArrowImage(position_, beneath)); +} + +void BraveTabContainer::DropArrow::SetWindowBounds(const gfx::Rect& bounds) { + arrow_window_->SetBounds(bounds); +} + +void BraveTabContainer::DropArrow::OnWidgetDestroying(views::Widget* widget) { + DCHECK(scoped_observation_.IsObservingSource(arrow_window_.get())); + scoped_observation_.Reset(); + arrow_window_ = nullptr; +} + +void BraveTabContainer::HandleDragUpdate( + const absl::optional& index) { + if (!tabs::utils::ShouldShowVerticalTabs( + tab_slot_controller_->GetBrowser())) { + TabContainerImpl::HandleDragUpdate(index); + return; + } + SetDropArrow(index); +} + +void BraveTabContainer::HandleDragExited() { + if (!tabs::utils::ShouldShowVerticalTabs( + tab_slot_controller_->GetBrowser())) { + TabContainerImpl::HandleDragExited(); + return; + } + SetDropArrow({}); +} + +gfx::Rect BraveTabContainer::GetDropBounds(int drop_index, + bool drop_before, + bool drop_in_group, + bool* is_beneath) { + DCHECK_NE(drop_index, -1); + + // The center is determined along the x-axis if it's pinned, or along the + // y-axis if not. + int center = -1; + + if (GetTabCount() == 0) { + // If the tabstrip is empty, it doesn't matter where the drop arrow goes. + // The tabstrip can only be transiently empty, e.g. during shutdown. + return gfx::Rect(); + } + + Tab* tab = GetTabAtModelIndex(std::min(drop_index, GetTabCount() - 1)); + + const bool is_tab_pinned = tab->data().pinned; + + const bool first_in_group = + drop_index < GetTabCount() && tab->group().has_value() && + GetModelIndexOf(tab) == + controller_->GetFirstTabInGroup(tab->group().value()); + + if (!drop_before || !first_in_group || drop_in_group) { + // Dropping between tabs, or between a group header and the group's first + // tab. + center = is_tab_pinned ? tab->x() : tab->y(); + const int length = is_tab_pinned ? tab->width() : tab->height(); + if (drop_index < GetTabCount()) { + center += drop_before ? -(tabs::kVerticalTabsSpacing / 2) : (length / 2); + } else { + center += length + (tabs::kVerticalTabsSpacing / 2); + } + } else { + // Dropping before a group header. + TabGroupHeader* const header = group_views_[tab->group().value()]->header(); + // Since there is no tab group in pinned tabs, there is no need to consider + // the x-axis. + center = header->y() + tabs::kVerticalTabsSpacing / 2; + } + + // Since all drop indicator images are the same size, we will use the right + // arrow image to determine the height and width. + const gfx::ImageSkia* drop_image = GetDropArrowImage( + BraveTabContainer::DropArrow::Position::Horizontal, false); + + // Determine the screen bounds. + gfx::Point drop_loc(is_tab_pinned ? center - drop_image->width() / 2 : 0, + is_tab_pinned ? tab->y() - drop_image->height() + : center - drop_image->height() / 2); + ConvertPointToScreen(this, &drop_loc); + gfx::Rect drop_bounds(drop_loc.x(), drop_loc.y(), drop_image->width(), + drop_image->height()); + + // If the rect doesn't fit on the monitor, push the arrow to the bottom. + display::Screen* screen = display::Screen::GetScreen(); + display::Display display = screen->GetDisplayMatching(drop_bounds); + *is_beneath = !display.bounds().Contains(drop_bounds); + + if (*is_beneath) { + drop_bounds.Offset( + is_tab_pinned ? 0 : drop_bounds.width() + tab->width(), + is_tab_pinned ? drop_bounds.height() + tab->height() : 0); + } + + return drop_bounds; +} + +gfx::ImageSkia* BraveTabContainer::GetDropArrowImage( + BraveTabContainer::DropArrow::Position pos, + bool beneath) { + using Position = BraveTabContainer::DropArrow::Position; + using RotationAmount = SkBitmapOperations::RotationAmount; + static base::NoDestructor< + base::flat_map, gfx::ImageSkia>> + drop_images([] { + gfx::ImageSkia* top_arrow_image = + ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( + IDR_TAB_DROP_UP); + + base::flat_map, gfx::ImageSkia> + position_to_images; + + position_to_images.emplace(std::make_pair(Position::Vertical, true), + *top_arrow_image); + position_to_images.emplace( + std::make_pair(Position::Horizontal, false), + gfx::ImageSkiaOperations::CreateRotatedImage( + *top_arrow_image, RotationAmount::ROTATION_90_CW)); + position_to_images.emplace( + std::make_pair(Position::Vertical, false), + gfx::ImageSkiaOperations::CreateRotatedImage( + *top_arrow_image, RotationAmount::ROTATION_180_CW)); + position_to_images.emplace( + std::make_pair(Position::Horizontal, true), + gfx::ImageSkiaOperations::CreateRotatedImage( + *top_arrow_image, RotationAmount::ROTATION_270_CW)); + return position_to_images; + }()); + return &drop_images->find(std::make_pair(pos, beneath))->second; +} + +void BraveTabContainer::SetDropArrow( + const absl::optional& index) { + if (!index) { + controller_->OnDropIndexUpdate(absl::nullopt, false); + drop_arrow_.reset(); + return; + } + + // Let the controller know of the index update. + controller_->OnDropIndexUpdate(index->value, index->drop_before); + + if (drop_arrow_ && (index == drop_arrow_->index())) { + return; + } + + bool is_beneath = false; + gfx::Rect drop_bounds = GetDropBounds(index->value, index->drop_before, + index->drop_in_group, &is_beneath); + + if (!drop_arrow_) { + DropArrow::Position position = DropArrow::Position::Vertical; + if (GetTabCount() > 0) { + Tab* tab = GetTabAtModelIndex(0); + position = tab->data().pinned ? DropArrow::Position::Vertical + : DropArrow::Position::Horizontal; + } + drop_arrow_ = + std::make_unique(*index, position, is_beneath, GetWidget()); + } else { + drop_arrow_->set_index(*index); + drop_arrow_->SetBeneath(is_beneath); + } + + // Reposition the window. + drop_arrow_->SetWindowBounds(drop_bounds); +} + BEGIN_METADATA(BraveTabContainer, TabContainerImpl) END_METADATA diff --git a/browser/ui/views/tabs/brave_tab_container.h b/browser/ui/views/tabs/brave_tab_container.h index eca56a075128..1c4b18e31d98 100644 --- a/browser/ui/views/tabs/brave_tab_container.h +++ b/browser/ui/views/tabs/brave_tab_container.h @@ -6,6 +6,8 @@ #ifndef BRAVE_BROWSER_UI_VIEWS_TABS_BRAVE_TAB_CONTAINER_H_ #define BRAVE_BROWSER_UI_VIEWS_TABS_BRAVE_TAB_CONTAINER_H_ +#include + #include "chrome/browser/ui/views/tabs/tab_container_impl.h" #include "chrome/browser/ui/tabs/tab_style.h" @@ -44,11 +46,68 @@ class BraveTabContainer : public TabContainerImpl { void OnPaintBackground(gfx::Canvas* canvas) override; void PaintChildren(const views::PaintInfo& paint_info) override; + // BrowserRootView::DropTarget + BrowserRootView::DropIndex GetDropIndex( + const ui::DropTargetEvent& event) override; + void HandleDragUpdate( + const absl::optional& index) override; + void HandleDragExited() override; + private: + class DropArrow : public views::WidgetObserver { + public: + enum class Position { Vertical, Horizontal }; + + DropArrow(const BrowserRootView::DropIndex& index, + Position position, + bool beneath, + views::Widget* context); + DropArrow(const DropArrow&) = delete; + DropArrow& operator=(const DropArrow&) = delete; + ~DropArrow() override; + + void set_index(const BrowserRootView::DropIndex& index) { index_ = index; } + BrowserRootView::DropIndex index() const { return index_; } + + void SetBeneath(bool beneath); + bool beneath() const { return beneath_; } + + void SetWindowBounds(const gfx::Rect& bounds); + + // views::WidgetObserver: + void OnWidgetDestroying(views::Widget* widget) override; + + private: + // Index of the tab to drop on. + BrowserRootView::DropIndex index_; + + Position position_ = Position::Vertical; + + bool beneath_ = false; + + // Renders the drop indicator. + raw_ptr arrow_window_ = nullptr; + + raw_ptr arrow_view_ = nullptr; + + base::ScopedObservation + scoped_observation_{this}; + }; + void UpdateLayoutOrientation(); + static gfx::ImageSkia* GetDropArrowImage( + BraveTabContainer::DropArrow::Position pos, + bool beneath); + void OnUnlockLayout(); + void SetDropArrow(const absl::optional& index); + gfx::Rect GetDropBounds(int drop_index, + bool drop_before, + bool drop_in_group, + bool* is_beneath); + base::flat_set closing_tabs_; raw_ptr drag_context_; @@ -56,6 +115,10 @@ class BraveTabContainer : public TabContainerImpl { // A pointer storing the global tab style to be used. const raw_ptr tab_style_; + const raw_ref controller_; + + std::unique_ptr drop_arrow_; + BooleanPrefMember show_vertical_tabs_; BooleanPrefMember vertical_tabs_floating_mode_enabled_; BooleanPrefMember vertical_tabs_collapsed_; diff --git a/test/BUILD.gn b/test/BUILD.gn index f07cc1ecad54..83d90cae8060 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -1084,6 +1084,7 @@ test("brave_browser_tests") { "//brave/browser/ui/views/brave_shields/cookie_list_opt_in_browsertest.cc", "//brave/browser/ui/views/frame/brave_non_client_hit_test_helper_browsertest.cc", "//brave/browser/ui/views/frame/brave_tabs_search_button_browsertest.cc", + "//brave/browser/ui/views/frame/vertical_tab_strip_root_view_browsertest.cc", "//brave/browser/ui/views/omnibox/brave_omnibox_view_views_browsertest.cc", "//brave/browser/ui/views/omnibox/omnibox_autocomplete_browsertest.cc", "//brave/browser/ui/views/profiles/brave_profile_menu_view_browsertest.cc",