// Copyright 2012 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/dragging/tab_drag_controller.h"

#include <algorithm>
#include <memory>
#include <optional>
#include <ranges>
#include <set>
#include <utility>
#include <variant>

#include "base/auto_reset.h"
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_auto_reset.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_init_state.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window/public/create_browser_window.h"
#include "chrome/browser/ui/sad_tab_helper.h"
#include "chrome/browser/ui/tabs/split_tab_util.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/dragging/drag_session_data.h"
#include "chrome/browser/ui/views/tabs/dragging/tab_drag_context.h"
#include "chrome/browser/ui/views/tabs/dragging/tab_drag_target.h"
#include "chrome/browser/ui/views/tabs/dragging/window_finder.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_slot_view.h"
#include "chrome/browser/ui/waap/initial_webui_window_metrics_manager.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
#include "chrome/browser/ui/web_applications/web_app_tabbed_utils.h"
#include "chrome/browser/ui/window_feature_controller/window_feature_controller.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tabs/public/split_tab_data.h"
#include "components/tabs/public/tab_group.h"
#include "components/tabs/public/tab_interface.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/dragdrop/os_exchange_data_provider_factory.h"
#include "ui/base/mojom/window_show_state.mojom.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/compositor/presentation_time_recorder.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/gestures/gesture_recognizer.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/range/range.h"
#include "ui/views/drag_utils.h"
#include "ui/views/event_monitor.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/view.h"
#include "ui/views/view_tracker.h"
#include "ui/views/widget/widget.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/public/cpp/window_properties.h"  // nogncheck
#include "chrome/browser/ash/boca/on_task/on_task_locked_controller.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"  // nogncheck
#endif

#if BUILDFLAG(IS_MAC)
#include "base/debug/dump_without_crashing.h"
#include "base/strings/string_number_conversions.h"
#include "components/crash/core/common/crash_key.h"
#include "components/remote_cocoa/browser/window.h"
#endif

#if defined(USE_AURA)
#include "ui/aura/env.h"                            // nogncheck
#include "ui/aura/window.h"                         // nogncheck
#include "ui/wm/core/window_modality_controller.h"  // nogncheck
#endif

#if BUILDFLAG(IS_LINUX)
#include "ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.h"
#endif

using content::OpenURLParams;
using content::WebContents;

// If non-null there is a drag underway.
static TabDragController* g_tab_drag_controller = nullptr;

namespace {

// A dragged window is forced to be a bit smaller than maximized bounds during a
// drag. This prevents the dragged browser widget from getting maximized at
// creation and makes it easier to drag tabs out of a restored window that had
// maximized size.
constexpr int kMaximizedWindowInset = 10;  // DIPs.

class VisibilityWaiter : public views::WidgetObserver {
 public:
  explicit VisibilityWaiter(views::Widget* widget) {
    observation_.Observe(widget);
  }
  void Wait() { run_loop_.Run(); }

 private:
  void OnWidgetVisibilityChanged(views::Widget* widget, bool visible) override {
    if (visible) {
      run_loop_.Quit();
    }
  }
  void OnWidgetDestroying(views::Widget* widget) override {
    observation_.Reset();
    run_loop_.Quit();
  }

  // Required to cooperatively halt the OS drag session while awaiting
  // WebUI paint without blocking the UI thread.
  base::RunLoop run_loop_{base::RunLoop::Type::kNestableTasksAllowed};
  base::ScopedObservation<views::Widget, views::WidgetObserver> observation_{
      this};
};

constexpr char kTabDraggingPresentationTimeHistogram[] =
    "Browser.TabDragging.PresentationTime";
constexpr char kTabDraggingPresentationTimeMaxHistogram[] =
    "Browser.TabDragging.PresentationTimeMax";
constexpr char kDragToNewBrowserPresentationTimeHistogram[] =
    "Browser.TabDragging.DragToNewBrowserPresentationTime";

#if BUILDFLAG(IS_CHROMEOS)

// Returns the aura::Window which stores the window properties for tab-dragging.
aura::Window* GetWindowForTabDraggingProperties(const TabDragContext* context) {
  return context ? context->GetWidget()->GetNativeWindow() : nullptr;
}

// Returns true if `context` browser window is snapped.
bool IsSnapped(const TabDragContext* context) {
  DCHECK(context);
  chromeos::WindowStateType type =
      GetWindowForTabDraggingProperties(context)->GetProperty(
          chromeos::kWindowStateTypeKey);
  return type == chromeos::WindowStateType::kPrimarySnapped ||
         type == chromeos::WindowStateType::kSecondarySnapped;
}

#else

bool IsSnapped(const TabDragContext* context) {
  return false;
}

#endif  // BUILDFLAG(IS_CHROMEOS)

gfx::Rect GetTabstripScreenBounds(const TabDragContext* context) {
  const views::View* view = context;
  gfx::Point view_topleft;
  views::View::ConvertPointToScreen(view, &view_topleft);
  gfx::Rect view_screen_bounds = view->GetLocalBounds();
  view_screen_bounds.Offset(view_topleft.x(), view_topleft.y());
  return view_screen_bounds;
}

// Returns true if `bounds` contains the y-coordinate `y`. The y-coordinate
// of `bounds` is adjusted by `vertical_adjustment`.
bool DoesRectContainVerticalPointExpanded(const gfx::Rect& bounds,
                                          int vertical_adjustment,
                                          int y) {
  int upper_threshold = bounds.bottom() + vertical_adjustment;
  int lower_threshold = bounds.y() - vertical_adjustment;
  return y >= lower_threshold && y <= upper_threshold;
}

// Adds `x_offset` to all the rectangles in `rects`.
void OffsetX(int x_offset, std::vector<gfx::Rect>* rects) {
  if (x_offset == 0) {
    return;
  }

  for (auto& rect : *rects) {
    rect.set_x(rect.x() + x_offset);
  }
}

// Gets the source browser view during a tab dragging. Returns nullptr if there
// is none.
BrowserView* GetBrowserViewForContext(const TabDragContext* context) {
  if (!context) {
    return nullptr;
  }
  gfx::NativeWindow source_window = context->GetWidget()->GetNativeWindow();
  if (source_window) {
    return BrowserView::GetBrowserViewForNativeWindow(source_window);
  }
  return nullptr;
}

void UpdateSystemDnDDragImage(TabDragContext* attached_context,
                              const gfx::ImageSkia& image) {
#if BUILDFLAG(IS_LINUX)
  VLOG(1) << __func__ << " image size=" << image.size().ToString();
  aura::Window* root_window =
      attached_context->GetWidget()->GetNativeWindow()->GetRootWindow();
  if (aura::client::GetDragDropClient(root_window)) {
    aura::client::GetDragDropClient(root_window)
        ->UpdateDragImage(image, {image.width() / 2, image.height() / 2});
  }
#endif  // BUILDFLAG(IS_LINUX)
}

class DefaultTabDragPointResolver : public TabDragPointResolver {
 public:
  DefaultTabDragPointResolver() = default;
  ~DefaultTabDragPointResolver() override = default;

  TabDragTarget* GetDragTarget(BrowserView& browser_view,
                               const gfx::Point& point_in_screen) override {
    return browser_view.GetTabDragTarget(point_in_screen);
  }
};

TabDragPointResolver* g_tab_drag_point_resolver_ = nullptr;

TabDragPointResolver* GetTabDragPointResolver() {
  if (!g_tab_drag_point_resolver_) {
    static DefaultTabDragPointResolver resolver;
    g_tab_drag_point_resolver_ = &resolver;
  }
  return g_tab_drag_point_resolver_;
}

}  // namespace

// EventTracker installs an event monitor and ends the drag when it receives any
// key event, or a mouse release event during a system DnD session.
class EventTracker : public ui::EventObserver {
 public:
  EventTracker(base::OnceClosure end_drag_callback,
               base::OnceClosure revert_drag_callback,
               gfx::NativeWindow context)
      : end_drag_callback_(std::move(end_drag_callback)),
        revert_drag_callback_(std::move(revert_drag_callback)) {
    event_monitor_ = views::EventMonitor::CreateApplicationMonitor(
        this, context,
        {ui::EventType::kKeyPressed, ui::EventType::kMouseReleased});
  }
  EventTracker(const EventTracker&) = delete;
  EventTracker& operator=(const EventTracker&) = delete;
  ~EventTracker() override = default;

 private:
  // ui::EventObserver:
  void OnEvent(const ui::Event& event) override {
    if (event.IsKeyEvent()) {
      if (event.AsKeyEvent()->key_code() == ui::VKEY_ESCAPE &&
          revert_drag_callback_) {
        std::move(revert_drag_callback_).Run();
      } else if (event.AsKeyEvent()->key_code() != ui::VKEY_ESCAPE &&
                 end_drag_callback_) {
        std::move(end_drag_callback_).Run();
      }
    } else {
      CHECK(event.type() == ui::EventType::kMouseReleased);
      if (TabDragController::IsSystemDnDSessionRunning()) {
        TabDragController::OnSystemDnDEnded();
      }
    }
  }

  base::OnceClosure end_drag_callback_;
  base::OnceClosure revert_drag_callback_;
  std::unique_ptr<views::EventMonitor> event_monitor_;
};

class TabDragController::SourceTabStripEmptinessTracker
    : public TabStripModelObserver {
 public:
  explicit SourceTabStripEmptinessTracker(TabStripModel* tabstrip,
                                          TabDragController* parent)
      : tab_strip_(tabstrip), parent_(parent) {
    tab_strip_->AddObserver(this);
  }

 private:
  void TabStripEmpty() override {
    tab_strip_->RemoveObserver(this);
    parent_->OnSourceTabStripEmpty();
  }

  const raw_ptr<TabStripModel, AcrossTasksDanglingUntriaged> tab_strip_;
  const raw_ptr<TabDragController, AcrossTasksDanglingUntriaged> parent_;
};

class TabDragController::DraggedTabsClosedTracker
    : public TabStripModelObserver {
 public:
  DraggedTabsClosedTracker(TabStripModel* tabstrip, TabDragController* parent)
      : parent_(parent) {
    tabstrip->AddObserver(this);
  }

  void OnTabStripModelChanged(
      TabStripModel* model,
      const TabStripModelChange& change,
      const TabStripSelectionChange& selection) override {
    switch (change.type()) {
      case TabStripModelChange::Type::kRemoved:
        for (const auto& contents : change.GetRemove()->contents) {
          parent_->OnActiveStripWebContentsRemoved(contents.contents);
        }
        break;
      case TabStripModelChange::Type::kReplaced:
        parent_->OnActiveStripWebContentsReplaced(
            change.GetReplace()->old_contents,
            change.GetReplace()->new_contents);
        break;
      default:
        break;
    }
  }

 private:
  const raw_ptr<TabDragController, DanglingUntriaged> parent_;
};

///////////////////////////////////////////////////////////////////////////////
// TabDragController, public:

// static
const int TabDragController::kTouchVerticalDetachMagnetism = 50;

// static
const int TabDragController::kVerticalDetachMagnetism = 15;

TabDragController::TabDragController()
    : source_context_(nullptr),
      attached_context_(nullptr),
      can_release_capture_(true),
      old_focused_view_tracker_(std::make_unique<views::ViewTracker>()),
      detach_behavior_(DetachBehavior::kDetachable),
      is_dragging_new_browser_(false),
      was_source_maximized_(false),
      was_source_fullscreen_(false),
      did_restore_window_(false),
      tab_strip_to_attach_to_after_exit_(nullptr),
      move_loop_widget_(nullptr),
      is_mutating_(false) {
  g_tab_drag_controller = this;
}

TabDragController::~TabDragController() {
  if (g_tab_drag_controller == this) {
    g_tab_drag_controller = nullptr;
  }

  widget_observation_.Reset();

  if (current_state_ == DragState::kDraggingWindow) {
    VLOG(1) << "EndMoveLoop in TabDragController dtor";
    GetAttachedBrowserWidget()->EndMoveLoop();
  }

  if (event_source_ == ui::mojom::DragEventSource::kTouch) {
    TabDragContext* capture_context =
        attached_context_ ? attached_context_.get() : source_context_.get();
    capture_context->GetWidget()->ReleaseCapture();
  }
  CHECK(!IsInObserverList());

  DCHECK(!expect_stay_alive_)
      << "TabDragController was destroyed when it shouldn't have been. Check "
         "up the stack for reentrancy.";
}

TabDragController::Liveness TabDragController::Init(
    TabDragContext* source_context,
    TabSlotView* source_view,
    const std::vector<TabSlotView*>& dragging_views,
    const gfx::Point& offset_from_source_view,
    ui::ListSelectionModel initial_selection_model,
    ui::mojom::DragEventSource event_source) {
  DCHECK(!dragging_views.empty());
  DCHECK(std::ranges::contains(dragging_views, source_view));

  // There's some rare condition on Windows where we are destroying ourselves
  // during Init() at some point. It's unclear how this happens, so we'll take
  // the nuke it from orbit approach and use a weak pointer throughout Init().
  // TODO(b/324993098): Revert this and use weak pointers in more targeted ways
  // once we know where the self-destruction is happening.
  base::WeakPtr<TabDragController> ref(weak_factory_.GetWeakPtr());

  ref->source_context_ = source_context;
  ref->was_source_maximized_ = source_context->GetWidget()->IsMaximized();
  ref->was_source_fullscreen_ = source_context->GetWidget()->IsFullscreen();
  ref->presentation_time_recorder_ =
      ui::CreatePresentationTimeHistogramRecorder(
          source_context->GetWidget()->GetCompositor(),
          kTabDraggingPresentationTimeHistogram,
          kTabDraggingPresentationTimeMaxHistogram,
          ui::PresentationTimeRecorder::BucketParams::CreateWithMaximum(
              base::Seconds(10)));
  // Do not release capture when transferring capture between widgets on:
  // - Desktop Linux
  //     Mouse capture is not synchronous on desktop Linux. Chrome makes
  //     transferring capture between widgets without releasing capture appear
  //     synchronous on desktop Linux, so use that.
  // - ChromeOS Ash
  //     Releasing capture on Ash cancels gestures so avoid it.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
  ref->can_release_capture_ = false;
#endif
  ref->start_point_in_screen_ =
      views::View::ConvertPointToScreen(source_view, offset_from_source_view);
  ref->event_source_ = event_source;
  ref->last_point_in_screen_ = start_point_in_screen_;
#if BUILDFLAG(IS_MAC)
  // Tracked for Mac only to enable resizing when dragging windows across
  // displays.
  ref->last_sized_display_id_ =
      display::Screen::Get()
          ->GetDisplayNearestPoint(start_point_in_screen_)
          .id();
#endif
  // Detachable tabs are not supported on Mac if the window is an out-of-process
  // (remote_cocoa) window, i.e. a PWA window.
  // TODO(crbug.com/40128833): Make detachable tabs work in PWAs on Mac.
#if BUILDFLAG(IS_MAC)
  if (ref->source_context_->GetWidget() &&
      remote_cocoa::IsWindowRemote(
          ref->source_context_->GetWidget()->GetNativeWindow())) {
    ref->detach_behavior_ = DetachBehavior::kNotDetachable;
  }
#else
#if BUILDFLAG(IS_CHROMEOS)
  // Tabs should not be detachable from the window if any of the following are
  // true:
  // 1. The app window is locked for OnTask. Not applicable for web browser
  //    scenarios.
  // 2. The dragged tab strip exists in a PWA, and any of the dragged views
  //    are the Pinned Home tab.
  BrowserWindowInterface* source_browser =
      BrowserView::GetBrowserViewForNativeWindow(
          source_context->GetWidget()->GetNativeWindow())
          ->browser();
  if (ash::boca::OnTaskLockedController::From(source_browser)
          ->is_locked_for_on_task()) {
    ref->detach_behavior_ = DetachBehavior::kNotDetachable;
  }
#endif  // BUILDFLAG(IS_CHROMEOS)
  for (TabSlotView* dragging_view : dragging_views) {
    if (!source_context->IsTabDetachable(dragging_view)) {
      ref->detach_behavior_ = DetachBehavior::kNotDetachable;
    }
  }
#endif  // BUILDFLAG(IS_MAC)

  ref->source_context_emptiness_tracker_ =
      std::make_unique<SourceTabStripEmptinessTracker>(
          ref->source_context_->GetTabStripModel(), this);

  const TabStripModel* tab_strip_model =
      ref->source_context_->GetTabStripModel();
  if (source_view->GetTabSlotViewType() ==
      TabSlotView::ViewType::kTabGroupHeader) {
    const tab_groups::TabGroupId group_id = source_view->group().value();
    const std::optional<tab_groups::TabGroupId> active_group_id =
        tab_strip_model->GetActiveTab()->GetGroup();
    gfx::Range group_range =
        tab_strip_model->group_model()->GetTabGroup(group_id)->ListTabs();
    const int active_tab_index_within_group =
        (active_group_id.has_value() && active_group_id.value() == group_id &&
         !group_range.is_empty())
            ? tab_strip_model->active_index() - group_range.GetMin()
            : 0;
    ref->drag_data_.group_header_drag_data_ =
        std::make_optional<GroupHeaderDragData>(group_id,
                                                active_tab_index_within_group);
  }

  for (TabSlotView* dragging_view : dragging_views) {
    ref->drag_data_.tab_drag_data_.emplace_back(source_context_, dragging_view);
    if (dragging_view->GetTabSlotViewType() ==
        TabSlotView::ViewType::kTabGroupHeader) {
      ref->drag_data_.dragging_groups.insert(*dragging_view->group());
    }
  }

  ref->drag_data_.source_view_index_ =
      std::ranges::find(dragging_views, source_view) - dragging_views.begin();

  // Listen for Esc key presses and mouse releases.
  ref->event_tracker_ = std::make_unique<EventTracker>(
      base::BindOnce(&TabDragController::EndDrag, base::Unretained(this),
                     EndDragReason::kComplete),
      base::BindOnce(&TabDragController::EndDrag, base::Unretained(this),
                     EndDragReason::kCancel),
      ref->source_context_->GetWidget()->GetNativeWindow());

  if (source_view->width() > 0) {
    ref->drag_data_.mouse_offset_to_size_ratios.set_x(
        static_cast<float>(
            source_view->GetMirroredXInView(offset_from_source_view.x())) /
        source_view->width());
  }
  if (source_view->height() > 0) {
    ref->drag_data_.mouse_offset_to_size_ratios.set_y(
        static_cast<float>(offset_from_source_view.y()) /
        source_view->height());
  }
  ref->initial_selection_model_ = std::move(initial_selection_model);

  ref->window_finder_ = std::make_unique<WindowFinder>();

  // Start listening for tabs to be closed or replaced in `source_context_`, in
  // case this happens before the mouse is moved enough to fully start the drag.
  // See crbug.com/40912628.
  ref->attached_context_tabs_closed_tracker_ =
      std::make_unique<DraggedTabsClosedTracker>(
          ref->source_context_->GetTabStripModel(), this);

  // To support immersive fullscreen on macOS, get the top-level widget, which
  // is the browser widget. In immersive fullscreen there are other
  // intermediate widgets that host the toolbar and tab strip; getting their
  // restored bounds is not helpful. Getting the top-level widget in the
  // non-immersive fullscreen case is a essentially a NOP, so use it
  // unconditionally here for all platforms.
  const views::Widget* source_top_level_widget =
      ref->source_context_->GetWidget()->GetTopLevelWidget();
  CHECK(source_top_level_widget);
  ref->initial_window_size_ =
      source_top_level_widget->GetRestoredBounds().size();

  ///////// DO NOT ADD INITIALIZATION CODE BELOW THIS LINE ////////
  // We need to be initialized at this point because SetCapture may reenter this
  // TabDragController and we do not want to deal with a partially-uninitialized
  // object at that point. This is already too complicated.

  // Gestures don't automatically do a capture. We don't allow multiple drags at
  // the same time, so we explicitly capture.
  if (event_source == ui::mojom::DragEventSource::kTouch) {
    // Taking capture involves OS calls which may reentrantly call back into
    // Chrome. This may result in the drag ending, destroying `this`. This
    // behavior has been observed on Windows in https://crbug.com/41459475 and
    // ChromeOS in https://crbug.com/40902272.
    if (SetCapture(ref->source_context_) == Liveness::kDeleted) {
      return Liveness::kDeleted;
    }
  }

  CHECK(ref);
  return Liveness::kAlive;
}

// static
bool TabDragController::IsAttachedTo(const TabDragContext* context) {
  return (g_tab_drag_controller && g_tab_drag_controller->active() &&
          g_tab_drag_controller->attached_context() == context);
}

// static
bool TabDragController::IsActive() {
  return g_tab_drag_controller && g_tab_drag_controller->active();
}

// static
bool TabDragController::IsSystemDnDSessionRunning() {
  return g_tab_drag_controller &&
         g_tab_drag_controller->system_drag_and_drop_session_running_;
}

// static
void TabDragController::OnSystemDnDUpdated(const ui::DropTargetEvent& event) {
  CHECK(IsSystemDnDSessionRunning());
  VLOG(2) << __func__ << " event=" << event.ToString();
  // It is important to use the event's root location instead of its location.
  // The latter may have been transformed to be relative to a child window, e.g.
  // Aura's clipping window. But we need the location relative to the browser
  // window, i.e. the root location.
  // ignore the Liveness; it's fine if `g_tab_drag_controller` dies here.
  std::ignore = g_tab_drag_controller->Drag(event.root_location());
}

// static
void TabDragController::OnSystemDnDExited() {
  CHECK(IsSystemDnDSessionRunning());
  VLOG(1) << __func__;
  // Call Drag() with a location that is definitely out of the tab strip.
  // ignore the Liveness; it's fine if `g_tab_drag_controller` dies here.
  std::ignore = g_tab_drag_controller->Drag(
      {g_tab_drag_controller->last_point_in_screen_.x(),
       g_tab_drag_controller->GetOutOfBoundsYCoordinate()});
}

// static
void TabDragController::OnSystemDnDEnded() {
  CHECK(IsSystemDnDSessionRunning());
  VLOG(1) << __func__;
  // Set this to prevent us from cancelling the drag again. The platform might
  // not have finished processing the drag end, and requesting to cancel the
  // drag might put us in an infinite loop of being notified about the drag end,
  // requesting to cancel the drag, being notified again, and so on.
  g_tab_drag_controller->system_drag_and_drop_session_running_ = false;
  g_tab_drag_controller->EndDrag(EndDragReason::kComplete);
}

// static
TabDragContext* TabDragController::GetSourceContext() {
  return g_tab_drag_controller ? g_tab_drag_controller->source_context_.get()
                               : nullptr;
}

void TabDragController::SetGroupHeaderWasCollapsedFromDrag(
    bool was_collapsed_from_drag) {
  if (drag_data_.group_header_drag_data_.has_value()) {
    drag_data_.group_header_drag_data_->was_collapsed_from_drag =
        was_collapsed_from_drag;
  }
}

void TabDragController::TabWasAdded() {
  // Stop dragging when a new tab is added and dragging a window, unless we're
  // doing so ourselves (e.g. while attaching to a new browser). Doing otherwise
  // results in a confusing state if the user attempts to reattach. We could
  // allow this and update ourselves during the add, but this comes up
  // infrequently enough that it's not worth the complexity.
  // Note: When we're in the kDraggingUsingSystemDnD state, this method being
  // called means a tab was added to the source window of the drag. We don't
  // need to cancel the drag in that case.
  if (current_state_ == DragState::kDraggingWindow && !is_mutating_) {
    EndDrag(EndDragReason::kComplete);
  }
}

void TabDragController::OnTabWillBeRemoved(content::WebContents* contents) {
  // End the drag before we remove a tab that's being dragged, to avoid
  // complex special cases that could result.
  if (!CanRemoveTabDuringDrag(contents)) {
    EndDrag(EndDragReason::kComplete);
  }
}

bool TabDragController::CanRemoveTabDuringDrag(
    content::WebContents* contents) const {
  // Tab removal can happen without interrupting dragging only if either a) the
  // tab isn't part of the drag or b) we're doing the removal ourselves.
  return !IsDraggingTab(contents) || is_mutating_;
}

bool TabDragController::CanRestoreFullscreenWindowDuringDrag() const {
#if BUILDFLAG(IS_MAC)
  // On macOS in immersive fullscreen mode, the horizontal tab strip is hosted
  // in a separate overlay widget. Restoring the window during drag moves the
  // tab strip back to the browser widget, breaking tab drag assumptions.
  //
  // This does not apply to vertical tab strips, which always remain in the
  // main browser widget regardless of fullscreen state. For vertical tabs,
  // allow restoration so that dragging all tabs in fullscreen correctly exits
  // fullscreen and enters the move loop.
  BrowserView* browser_view = GetBrowserViewForContext(source_context_);
  if (browser_view && browser_view->ShouldDrawVerticalTabStrip()) {
    return true;
  }
  return false;
#else
  return true;
#endif
}

TabDragController::Liveness TabDragController::Drag(
    const gfx::Point& point_in_screen) {
  TRACE_EVENT1("views", "TabDragController::Drag", "point_in_screen",
               point_in_screen.ToString());

  // If we're in `kWaitingToExitRunLoop` or `kWaitingToDragTabs`, then we have
  // asked to exit the nested run loop, but are still in it. We should ignore
  // any events until we actually exit the nested run loop, to avoid potentially
  // starting another one (see https://crbug.com/41493121). Similarly, if we're
  // kStopped but haven't been destroyed yet, or if we are waiting for the
  // detached window to show, we should ignore events.
  if (current_state_ == DragState::kWaitingToExitRunLoop ||
      current_state_ == DragState::kWaitingToDragTabs ||
      current_state_ == DragState::kStopped ||
      current_state_ == DragState::kWaitingForWindowToShow) {
    return Liveness::kAlive;
  }

  bring_to_front_timer_.Stop();

  if (current_state_ == DragState::kNotStarted) {
    if (!CanStartDrag(point_in_screen)) {
      return Liveness::kAlive;  // User hasn't dragged far enough yet.
    }

    // If any of the tabs have disappeared (e.g. closed or discarded), cancel
    // the drag session. See crbug.com/40912628.
    if (GetViewsMatchingDraggedContents(source_context_).empty()) {
      EndDrag(EndDragReason::kCancel);
      return Liveness::kDeleted;
    }

    // On windows SaveFocus() may trigger a capture lost, which destroys us.
    {
      const Liveness alive = SaveFocus();
      if (alive == Liveness::kDeleted) {
        return Liveness::kDeleted;
      }
    }

    StartDrag();

    TabStripModel* const model = source_context_->GetTabStripModel();
    const bool is_dragging_all_tabs =
        drag_data_.num_dragging_tabs() == model->count();

    // In focus mode, dragging the focused group header should move the window,
    // as the focused group represents all visible unpinned tabs.
    const bool is_dragging_focused_group_header =
        model->GetFocusedGroup().has_value() &&
        drag_data_.group_header_id() == model->GetFocusedGroup();

    if (is_dragging_all_tabs || is_dragging_focused_group_header) {
      if (ShouldDragWindowUsingSystemDnD()) {
        return StartSystemDnDSessionIfNecessary(attached_context_,
                                                point_in_screen);
      }

      did_restore_window_ =
          was_source_maximized_ ||
          (was_source_fullscreen_ && CanRestoreFullscreenWindowDuringDrag());
      if (did_restore_window_) {
        RestoreAttachedWindowForDrag();
      }

      // Drag the window relative to `start_point_in_screen_` to pretend that
      // this was the plan all along.
      const gfx::Vector2d drag_offset =
          start_point_in_screen_ -
          attached_context_->GetWidget()->GetWindowBoundsInScreen().origin();
      return RunMoveLoop(point_in_screen, drag_offset);
    }

    current_state_ = DragState::kDraggingTabs;
    StartDraggingTabsSession(true, point_in_screen);
  }

  return ContinueDragging(point_in_screen);
}

void TabDragController::EndDrag(EndDragReason reason) {
  TRACE_EVENT0("views", "TabDragController::EndDrag");
  presentation_time_recorder_.reset();

  if (!active()) {
    return;
  }

  // Some drags need to react to the model being mutated before the model can
  // change its state.
  if (reason == EndDragReason::kModelAddedTab) {
    // If a group is being dragged, we must place the
    // drag at the current position in the tabstrip or else we will be
    // re-entering into tabstrip mutation code.
    bool group_dragged = false;

    for (const TabDragData& tab_drag_datum : drag_data_.tab_drag_data_) {
      if (tab_drag_datum.view_type == TabSlotView::ViewType::kTabGroupHeader) {
        group_dragged = true;
      }
    }

    if (group_dragged) {
      EndDragImpl(source_context_ == attached_context_ ? EndDragType::kCanceled
                                                       : EndDragType::kNormal);
    }
    return;
  }

  // If we're dragging a window ignore capture lost since it'll ultimately
  // trigger the move loop to end and we'll revert the drag when RunMoveLoop()
  // finishes.
  if (reason == EndDragReason::kCaptureLost &&
      current_state_ == DragState::kDraggingWindow) {
    return;
  }

  // We always lose capture when hiding `attached_context_`, just ignore it.
  if (reason == EndDragReason::kCaptureLost &&
      current_state_ == DragState::kDraggingUsingSystemDnD) {
    return;
  }

  // End the move loop if we're in one. Note that the drag will end (just below)
  // before the move loop actually exits.
  if (current_state_ == DragState::kDraggingWindow && in_move_loop_) {
    VLOG(1) << "EndMoveLoop in EndDrag";
    GetAttachedBrowserWidget()->EndMoveLoop();
  }

  if (reason == EndDragReason::kComplete && drag_start_time_.has_value()) {
    // If we support a mixture of dragging between VT and HT, then update this
    // metric accordingly.
    base::UmaHistogramTimes(
        absl::StrFormat("TabStrip.%s.TabDragDuration", GetTabStripMode()),
        base::TimeTicks::Now() - drag_start_time_.value());
  }
  drag_start_time_.reset();

  EndDragImpl(reason != EndDragReason::kComplete && source_context_
                  ? EndDragType::kCanceled
                  : EndDragType::kNormal);
}

void TabDragController::SetDragLoopDoneCallbackForTesting(
    base::OnceClosure callback) {
  drag_loop_done_callback_ = std::move(callback);
}

std::unique_ptr<tabs::TabModel> TabDragController::DetachTabAtForInsertion(
    int drag_idx) {
  CHECK_EQ(current_state_, DragState::kStopped)
      << "Detaching is only supported after the drag is stopped";
  CHECK(attached_context_);

  TabDragData& tab_data = drag_data_.tab_drag_data_[drag_idx];
  // We can't move the tab if `contents` was destroyed during the drag, or if
  // this is a group header.
  CHECK(tab_data.contents);
  tab_data.attached_view->set_detached();
  tab_data.attached_view = nullptr;

  const int from_idx =
      attached_context_->GetTabStripModel()->GetIndexOfWebContents(
          tab_data.contents);
  CHECK_NE(from_idx, TabStripModel::kNoTab);

  base::AutoReset<bool> setter(&is_mutating_, true);
  base::AutoReset<bool> is_removing_last_tab_setter(&is_moving_last_tab_, true);
  std::unique_ptr<tabs::TabModel> detached_tab =
      attached_context_->GetTabStripModel()->DetachTabAtForInsertion(from_idx);
  OnContextDraggedTabsDetached();

  return detached_tab;
}

const DragSessionData& TabDragController::GetSessionData() const {
  return drag_data_;
}

const TabDragContext* TabDragController::GetAttachedContext() const {
  return attached_context();
}

///////////////////////////////////////////////////////////////////////////////
// TabDragController, private:

void TabDragController::OnWidgetBoundsChanged(views::Widget* widget,
                                              const gfx::Rect& new_bounds) {
  TRACE_EVENT1("views", "TabDragController::OnWidgetBoundsChanged",
               "new_bounds", new_bounds.ToString());
#if defined(USE_AURA)
  aura::Env* env = aura::Env::GetInstance();
  // WidgetBoundsChanged happens as a step of ending a drag, but Drag() doesn't
  // have to be called -- GetCursorScreenPoint() may return an incorrect
  // location in such case and causes a weird effect. See
  // https://crbug.com/40606256 for the details.
  if (!env->IsMouseButtonDown() && !env->is_touch_down()) {
    return;
  }
#endif
  // ignore the Liveness; it's fine if Drag destroys `this`.
  std::ignore = Drag(GetCursorScreenPoint());

  // !! N.B. `this` may be deleted here !!
}

void TabDragController::OnWidgetDestroyed(views::Widget* widget) {
  widget_observation_.Reset();
}

void TabDragController::OnSourceTabStripEmpty() {
  // NULL out source_context_ so that we don't attempt to add back to it (in
  // the case of a revert).
  source_context_ = nullptr;
}

void TabDragController::OnActiveStripWebContentsRemoved(
    content::WebContents* contents) {
  // Mark closed tabs as destroyed so we don't try to manipulate them later.
  for (auto& drag_datum : drag_data_.tab_drag_data_) {
    if (drag_datum.contents == contents) {
      drag_datum.contents = nullptr;
      break;
    }
  }
}

void TabDragController::OnActiveStripWebContentsReplaced(
    content::WebContents* previous,
    content::WebContents* next) {
  for (auto& drag_datum : drag_data_.tab_drag_data_) {
    if (drag_datum.contents == previous) {
      drag_datum.contents = next;
      break;
    }
  }
}

TabDragController::Liveness TabDragController::SaveFocus() {
  base::WeakPtr<TabDragController> ref(weak_factory_.GetWeakPtr());
  DCHECK(source_context_);

  old_focused_view_tracker_->SetView(
      source_context_->GetWidget()->GetFocusManager()->GetFocusedView());
  source_context_->GetWidget()->GetFocusManager()->ClearFocus();

  // WARNING: we may have been deleted.
  return ref ? Liveness::kAlive : Liveness::kDeleted;
}

void TabDragController::RestoreFocus() {
  if (attached_context_ != source_context_) {
    if (is_dragging_new_browser_) {
      content::WebContents* active_contents =
          drag_data_.source_dragged_contents();
      if (active_contents) {
        // If the tab is split, make the last active split tab stay active
        // instead of the drag source.
        const tabs::TabInterface* tab =
            tabs::TabInterface::GetFromContents(active_contents);

        if (tab->IsSplit()) {
          TabStripModel* tab_strip_model =
              attached_context_->GetTabStripModel();
          active_contents = tab_strip_model->GetWebContentsAt(
              split_tabs::GetIndexOfLastActiveTab(tab_strip_model,
                                                  tab->GetSplit().value()));
        }

        if (!active_contents->FocusLocationBarByDefault()) {
          active_contents->Focus();
        }
      }
    }
    return;
  }

  views::View* old_focused_view = old_focused_view_tracker_->view();
  if (!old_focused_view || !old_focused_view->GetFocusManager()) {
    return;
  }

  old_focused_view->GetFocusManager()->SetFocusedView(old_focused_view);
}

bool TabDragController::CanStartDrag(const gfx::Point& point_in_screen) const {
  // Determine if the mouse has moved beyond a minimum elasticity distance in
  // any direction from the starting point.
  static const int kMinimumDragDistance = 10;
  int x_offset = abs(point_in_screen.x() - start_point_in_screen_.x());
  int y_offset = abs(point_in_screen.y() - start_point_in_screen_.y());
  return sqrt(pow(static_cast<float>(x_offset), 2) +
              pow(static_cast<float>(y_offset), 2)) > kMinimumDragDistance;
}

TabDragController::Liveness TabDragController::ContinueDragging(
    const gfx::Point& point_in_screen) {
  TRACE_EVENT1("views", "TabDragController::ContinueDragging",
               "point_in_screen", point_in_screen.ToString());
  if (presentation_time_recorder_) {
    presentation_time_recorder_->RequestNext();
  }
  last_point_in_screen_ = point_in_screen;

  DCHECK(attached_context_);

  TabDragContext* target_context = source_context_;
  if (detach_behavior_ == DetachBehavior::kDetachable) {
    auto [alive, context, drop_target] = GetDragTargetForPoint(point_in_screen);
    if (alive == Liveness::kDeleted) {
      return Liveness::kDeleted;
    }
    target_context = context;
    UpdateDragTarget(drop_target, point_in_screen);
    if (current_drag_target_) {
      TabDragContext* delegate_context =
          current_drag_target_->OnTabDragUpdated(*this, point_in_screen);
      if (!delegate_context && current_state_ == DragState::kDraggingWindow) {
        delegate_context = attached_context_;
      }
      target_context = delegate_context;
    }
  }

  if (target_context != attached_context_) {
    is_dragging_new_browser_ = false;
    did_restore_window_ = false;

    return DragBrowserToNewTabStrip(target_context, point_in_screen);
  }

  if (current_state_ == DragState::kDraggingWindow) {
#if BUILDFLAG(IS_MAC)
    // On Mac, resizing the window during dragging ensures that it fits within
    // the target display's work area. This is not enabled or tested on other
    // desktop platforms because their interactive UI test environments do not
    // support mocking multi-display layouts.
    display::Display current_display =
        display::Screen::Get()->GetDisplayNearestPoint(point_in_screen);
    if (current_display.id() != last_sized_display_id_) {
      last_sized_display_id_ = current_display.id();
      gfx::Size new_size = CalculateDraggedWindowSize(attached_context_);
      views::Widget* browser_widget = GetAttachedBrowserWidget();
      gfx::Rect bounds = browser_widget->GetWindowBoundsInScreen();
      if (bounds.size() != new_size) {
        browser_widget->GetRootView()->SetSize(new_size);
        browser_widget->LayoutRootViewIfNecessary();
        const gfx::Vector2d drag_offset = CalculateWindowDragOffset();
        bounds.set_origin(point_in_screen - drag_offset);
        bounds.set_size(new_size);
        browser_widget->SetBounds(bounds);
      }
    }
#endif

    bring_to_front_timer_.Start(
        FROM_HERE, base::Milliseconds(750),
        base::BindOnce(&TabDragController::BringWindowUnderPointToFront,
                       base::Unretained(this), point_in_screen));
  }

  if (current_state_ == DragState::kDraggingTabs &&
      attached_context_->GetPositioningDelegate()) {
    dragging_tabs_session_->MoveAttached(point_in_screen);
  }
  return Liveness::kAlive;
}

TabDragController::Liveness TabDragController::DragBrowserToNewTabStrip(
    TabDragContext* target_context,
    const gfx::Point& point_in_screen) {
  TRACE_EVENT1("views", "TabDragController::DragBrowserToNewTabStrip",
               "point_in_screen", point_in_screen.ToString());

  base::WeakPtr<TabDragController> weak_this = weak_factory_.GetWeakPtr();

  dragging_tabs_session_ = nullptr;

  if (!target_context) {
    return DetachIntoNewBrowserAndRunMoveLoop(point_in_screen);
  }

#if defined(USE_AURA)
  // Only Aura windows are gesture consumers.
  gfx::NativeView attached_native_view =
      GetAttachedBrowserWidget()->GetNativeView();
  GetAttachedBrowserWidget()->GetGestureRecognizer()->TransferEventsTo(
      attached_native_view, target_context->GetWidget()->GetNativeView(),
      ui::TransferTouchesBehavior::kDontCancel);

  CHECK(weak_this);
#endif

  if (current_state_ == DragState::kDraggingWindow) {
    // ReleaseCapture() is going to result in calling back to us (because it
    // results in a move). That'll cause all sorts of problems.  Reset the
    // observer so we don't get notified and process the event.
#if BUILDFLAG(IS_CHROMEOS)
    widget_observation_.Reset();
    move_loop_widget_ = nullptr;
#endif  // BUILDFLAG(IS_CHROMEOS)
    views::Widget* browser_widget = GetAttachedBrowserWidget();
    // Disable animations so that we don't see a close animation on aero.
    browser_widget->SetVisibilityChangedAnimationsEnabled(false);
    if (can_release_capture_) {
      browser_widget->ReleaseCapture();
    } else {
      CHECK_EQ(SetCapture(target_context), Liveness::kAlive);
    }

#if !BUILDFLAG(IS_LINUX)
    const bool is_dragging_all_tabs =
        source_context_ && drag_data_.num_dragging_tabs() ==
                               source_context_->GetTabStripModel()->count();
    if (is_dragging_new_browser_ || is_dragging_all_tabs) {
      // EndMoveLoop is going to snap the window back to its original location.
      // Hide it so users don't see this. Hiding a window in Linux aura causes
      // it to lose capture so skip it.
      browser_widget->Hide();
    }
#endif
    // Does not immediately exit the move loop - that only happens when control
    // returns to the event loop. The rest of this method will complete before
    // control returns to RunMoveLoop().
    VLOG(1) << "EndMoveLoop in DragBrowserToNewTabStrip";
    browser_widget->EndMoveLoop();
    CHECK(weak_this);

    // Ideally we would always swap the tabs now, but on non-ash Windows, it
    // seems that running the move loop implicitly activates the window when
    // done, leading to all sorts of flicker. So, on non-ash Windows, instead
    // we process the move after the loop completes. But on ChromeOS Ash, we
    // can do tab swapping now to avoid the tab flashing issue
    // (crbug.com/40740059).
    if (can_release_capture_) {
      tab_strip_to_attach_to_after_exit_ = target_context;
      current_state_ = DragState::kWaitingToDragTabs;
    } else {
      // We already transferred ownership of `this` above, before we released
      // capture.
      DetachAndAttachToNewContext(ReleaseCapture::kDontReleaseCapture,
                                  target_context);
      CHECK(weak_this);

      // Enter kWaitingToExitRunLoop until we actually have exited the nested
      // run loop. Otherwise, we might attempt to start another nested run loop,
      // which will CHECK. See https://crbug.com/41493121.
      current_state_ = DragState::kWaitingToExitRunLoop;

      // Move the tabs into position.
      StartDraggingTabsSession(false, point_in_screen);
      attached_context_->GetWidget()->Activate();
    }

    return Liveness::kAlive;
  }

  if (current_state_ == DragState::kDraggingUsingSystemDnD) {
    current_state_ = DragState::kDraggingTabs;
    // Hide the drag image while attached.
    UpdateSystemDnDDragImage(attached_context_, {});
  }

  // In this case we're either:
  // - inserting into a new tabstrip directly from another, without going
  // through any intervening stage of dragging a window. This is possible if the
  // cursor goes over the target tabstrip but isn't far enough from the attached
  // tabstrip to trigger dragging a window;
  // - or the platform does not support RunMoveLoop() and this is the normal
  // behaviour.
  DetachAndAttachToNewContext(ReleaseCapture::kDontReleaseCapture,
                              target_context);
  CHECK(weak_this);

  StartDraggingTabsSession(false, point_in_screen);
  attached_context_->GetWidget()->Activate();
  return Liveness::kAlive;
}

bool TabDragController::ShouldDragWindowUsingSystemDnD() {
  return !GetAttachedBrowserWidget()->IsMoveLoopSupported();
}

void TabDragController::RequestTabThumbnail() {
  WebContents* contents =
      source_context_->GetTabStripModel()->GetActiveWebContents();
  CHECK(contents);
  CHECK(IsDraggingTab(contents));

  content::RenderWidgetHostView* rwhv = contents->GetRenderWidgetHostView();
  if (rwhv) {
    float scale = rwhv->GetDeviceScaleFactor();
    // Passing an empty source rect means copying the whole surface, an
    // empty target size means no scaling (as we don't know the surface
    // size, it's easier to scale the bitmap to the correct size later).
    rwhv->CopyFromSurface(
        gfx::Rect(), gfx::Size(), base::TimeDelta(),
        base::BindOnce(&TabDragController::OnTabThumbnailAvailable,
                       weak_factory_.GetWeakPtr(), scale));
  }
}

void TabDragController::OnTabThumbnailAvailable(
    float window_scale,
    const content::CopyFromSurfaceResult& result) {
  const SkBitmap& thumbnail = result.has_value() ? result->bitmap : SkBitmap();
  VLOG(1) << __func__ << " " << thumbnail.width() << "x" << thumbnail.height();
  constexpr size_t kTargetHeightDip = 200;
  constexpr int kRoundedCornerRadius = 4;

  // Exit early if CopyFromSurface() failed, e.g. because the tab was deleted
  // after the drag started.
  if (thumbnail.drawsNothing()) {
    return;
  }

  const float scale = static_cast<float>(kTargetHeightDip) / thumbnail.height();
  drag_image_ = gfx::ImageSkia::CreateFromBitmap(thumbnail, window_scale);
  const gfx::Size target_size =
      gfx::ScaleToCeiledSize(drag_image_.size(), scale);
  drag_image_ = gfx::ImageSkiaOperations::CreateResizedImage(
      drag_image_, skia::ImageOperations::ResizeMethod::RESIZE_GOOD,
      target_size);
  drag_image_ = gfx::ImageSkiaOperations::CreateImageWithRoundRectClip(
      kRoundedCornerRadius, drag_image_);
  if (current_state_ == DragState::kDraggingUsingSystemDnD) {
    UpdateSystemDnDDragImage(attached_context_, drag_image_);
  }
}

TabDragController::Liveness TabDragController::StartSystemDnDSessionIfNecessary(
    TabDragContext* context,
    gfx::Point point_in_screen) {
  CHECK(ShouldDragWindowUsingSystemDnD());
  CHECK(ui::ResourceBundle::HasSharedInstance());
  current_state_ = DragState::kDraggingUsingSystemDnD;

  if (system_drag_and_drop_session_running_) {
    // Show the drag image again.
    UpdateSystemDnDDragImage(attached_context_, drag_image_);

    return Liveness::kAlive;
  }

  system_drag_and_drop_session_running_ = true;

  if (attached_context_ == source_context_ &&
      drag_data_.num_dragging_tabs() ==
          source_context_->GetTabStripModel()->count()) {
    // When dragging all of a window's tabs, we just hide that window instead of
    // creating a new hidden one. On some platforms (e.g. Wayland) the drag and
    // drop session must be started before hiding the window, so defer until the
    // drag has started.
    drag_started_callback_ = base::BindOnce(
        &TabDragController::HideAttachedContext, weak_factory_.GetWeakPtr());
  }

  auto data_provider = ui::OSExchangeDataProviderFactory::CreateProvider();
  // Set data in a format that is accepted by TabStrip so that a drop can
  // happen.
  base::Pickle pickle;
  data_provider->SetPickledData(
      ui::ClipboardFormatType::CustomPlatformType(ui::kMimeTypeWindowDrag),
      pickle);

  // If the drag image is already available, set it right away. Else we'll get a
  // call to OnTabThumbnailAvailable() and will update it after starting the DnD
  // session.
  if (!drag_image_.isNull()) {
    VLOG(1) << __func__ << " setting drag image";
    data_provider->SetDragImage(
        drag_image_, {drag_image_.width() / 2, drag_image_.height() / 2});
  }

  // Pull into a local to avoid use-after-free if RunDragDropLoop deletes
  // `this`.
  base::OnceClosure drag_loop_done_callback =
      std::move(drag_loop_done_callback_);

  VLOG(1) << __func__ << " Starting system DnD session";

#if defined(USE_AURA)
  aura::Window* root_window =
      context->GetWidget()->GetNativeWindow()->GetRootWindow();
  aura::client::DragDropClient* client =
      aura::client::GetDragDropClient(root_window);
  if (client) {
    drag_drop_client_observation_.Observe(client);
  }
#endif  // defined(USE_AURA)

  base::WeakPtr<TabDragController> ref(weak_factory_.GetWeakPtr());
  context->GetWidget()->RunDragDropLoop(
      context, std::make_unique<ui::OSExchangeData>(std::move(data_provider)),
      point_in_screen, static_cast<int>(ui::mojom::DragOperation::kMove),
      ui::mojom::DragEventSource::kMouse);

  VLOG(1) << __func__ << " RunDragDropLoop returned";

  // `RunDragDropLoop()` may return if we drag all of a window's tabs into
  // another window's tab strip, because that destroys the window. The DnD
  // session is still running and functional, though.

  // If we're still alive and haven't updated our state yet, this means the drag
  // session ended while we were dragging all of the only window's tabs. We need
  // to end the drag session ourselves.
  if (ref && current_state_ == DragState::kDraggingUsingSystemDnD) {
    VLOG(1) << __func__ << " Ending drag";
    EndDrag(EndDragReason::kComplete);
  }

  if (drag_loop_done_callback) {
    std::move(drag_loop_done_callback).Run();
  }

  return ref ? Liveness::kAlive : Liveness::kDeleted;
}

void TabDragController::HideAttachedContext() {
  CHECK_EQ(current_state_, DragState::kDraggingUsingSystemDnD);
  CHECK(GetAttachedBrowserWidget()->IsVisible());

  // See the comment in StartSystemDnDSessionIfNecessary() where we start the
  // drag session.
  GetAttachedBrowserWidget()->Hide();

  // Stop observing, as we're only interested in knowing when the drag started.
#if defined(USE_AURA)
  drag_drop_client_observation_.Reset();
#endif  // defined(USE_AURA)
}

std::tuple<TabDragController::Liveness, TabDragContext*, TabDragTarget*>
TabDragController::GetDragTargetForPoint(gfx::Point point_in_screen) {
  TRACE_EVENT1("views", "TabDragController::GetTargetTabStripForPoint",
               "point_in_screen", point_in_screen.ToString());

  gfx::NativeWindow local_window;
  const Liveness state = GetLocalProcessWindow(
      point_in_screen, current_state_ == DragState::kDraggingWindow,
      &local_window);
  if (state == Liveness::kDeleted) {
    return std::tuple(Liveness::kDeleted, nullptr, nullptr);
  }

  if (local_window && CanAttachTo(local_window)) {
    BrowserView* browser_view =
        BrowserView::GetBrowserViewForNativeWindow(local_window);
    TabDragContext* destination_tab_strip =
        browser_view->tab_strip_view()->GetDragContext();
    if (destination_tab_strip) {
      if (DoesTabStripContain(destination_tab_strip, point_in_screen)) {
        return std::tuple(Liveness::kAlive, destination_tab_strip, nullptr);
      } else if (TabDragTarget* candidate =
                     GetTabDragPointResolver()->GetDragTarget(
                         *browser_view, point_in_screen)) {
        // The delegate will provide the context to attach to.
        return std::tuple(Liveness::kAlive, nullptr, candidate);
      }
    }
  }

  return std::tuple(Liveness::kAlive,
                    current_state_ == DragState::kDraggingWindow
                        ? attached_context_.get()
                        : nullptr,
                    nullptr);
}

bool TabDragController::DoesTabStripContain(
    TabDragContext* context,
    const gfx::Point& point_in_screen) const {
  TabDragPositioningDelegate* positioning_delegate =
      context->GetPositioningDelegate();
  if (!positioning_delegate) {
    return false;
  }
  // Make sure the specified screen point is actually within the bounds of the
  // specified context...
  gfx::Rect tabstrip_bounds = GetTabstripScreenBounds(context);
  const int x_in_strip = point_in_screen.x() - tabstrip_bounds.x();
  return (x_in_strip >= positioning_delegate->TabDragAreaBeginX()) &&
         (x_in_strip < positioning_delegate->TabDragAreaEndX()) &&
         DoesRectContainVerticalPointExpanded(
             tabstrip_bounds, kVerticalDetachMagnetism, point_in_screen.y());
}

void TabDragController::StartDrag() {
  // `source_context_` already owns `this` (it created us, even), so no need
  // to hand off ownership.
  CHECK_EQ(source_context_->GetDragController(), this);
  CHECK(!current_drag_target_);
  attached_context_ = source_context_;

  AttachImpl();

  drag_start_time_ = base::TimeTicks::Now();
  drag_data_.drag_start_time = *drag_start_time_;

  // Request a thumbnail to use as drag image if we'll use fallback tab
  // dragging.
  if (ShouldDragWindowUsingSystemDnD()) {
    RequestTabThumbnail();
  }
}

void TabDragController::AttachToNewContext(
    TabDragContext* attached_context,
    std::unique_ptr<TabDragController> controller,
    std::vector<std::variant<std::unique_ptr<DetachedTab>,
                             std::unique_ptr<DetachedTabCollection>>>
        owned_tabs_and_collections) {
  // We should already have detached by the time we get here.
  CHECK(!attached_context_);
  attached_context_ = attached_context;

  // `this` is owned by `controller`, and not by `attached_context`.
  CHECK_EQ(this, controller.get());
  CHECK(!attached_context_->GetDragController());

  // Transitioning from detached to attached to a new context. Add tabs to
  // the new model.
  CHECK(GetViewsMatchingDraggedContents(attached_context_).empty());

  TabStripModel* tab_strip_model = attached_context_->GetTabStripModel();
  selection_model_before_attach_ =
      tab_strip_model->selection_model().GetListSelectionModel();

  // Insert at any valid index in the tabstrip. We'll fix up the insertion
  // index in MoveAttached() later, if we're transitioning to kDraggingTabs;
  // if we're transitioning to kDraggingWindow this is the correct index, 0.
  size_t index = tab_strip_model->IndexOfFirstNonPinnedTab();
  // When focus mode is active, initial insertion of attached dragged unpinned
  // tabs should target the focused group's range to prevent index desync.
  // Pinned tabs cannot be inserted into tab groups.
  const std::optional<tab_groups::TabGroupId> focused_group =
      tab_strip_model->GetFocusedGroup();
  if (focused_group.has_value() &&
      !drag_data_.group_header_drag_data_.has_value() &&
      !std::ranges::any_of(drag_data_.tab_drag_data_, &TabDragData::pinned)) {
    const TabGroup* group =
        tab_strip_model->group_model()->GetTabGroup(*focused_group);
    if (group && group->ListTabs().length() > 0) {
      index = group->ListTabs().start();
    }
  }

  base::AutoReset<bool> setter(&is_mutating_, true);

  const auto update_sad_tab = base::BindRepeating(
      [](TabStripModel* model, size_t sad_index) {
        // If a sad tab is showing, the SadTabView needs to be updated.
        SadTabHelper* const sad_tab_helper =
            SadTabHelper::From(model->GetTabAtIndex(sad_index));
        if (sad_tab_helper) {
          sad_tab_helper->ReinstallInWebView();
        }
      },
      attached_context_->GetTabStripModel());

  for (auto& tab_or_collection : owned_tabs_and_collections) {
    if (auto* tab =
            std::get_if<std::unique_ptr<DetachedTab>>(&tab_or_collection)) {
      const WebContents* web_contents = tab->get()->tab->GetContents();
      // If it's a tab - we add it to the tabstrip.
      int add_types = AddTabTypes::ADD_NONE;
      auto it = std::find_if(drag_data_.tab_drag_data_.begin(),
                             drag_data_.tab_drag_data_.end(),
                             [web_contents](TabDragData& tab_data) {
                               return web_contents == tab_data.contents;
                             });
      CHECK(it != drag_data_.tab_drag_data_.end());
      TabDragData& tab_data = *it;
      std::optional<tab_groups::TabGroupId> group_for_tab = std::nullopt;
      if (tab_data.pinned) {
        add_types |= AddTabTypes::ADD_PINNED;
      } else {
        group_for_tab = focused_group;
      }

      const size_t inserted_index =
          attached_context_->GetTabStripModel()->InsertDetachedTabAt(
              index, std::move(tab->get()->tab), add_types, group_for_tab);
      CHECK_EQ(inserted_index, index);
      update_sad_tab.Run(index);
      index++;
    } else {
      gfx::Range collection_indices;
      auto* detached_tab_collection =
          std::get_if<std::unique_ptr<DetachedTabCollection>>(
              &tab_or_collection);

      const bool pinned = detached_tab_collection->get()->pinned_;

      if (std::holds_alternative<std::unique_ptr<tabs::TabGroupTabCollection>>(
              detached_tab_collection->get()->collection_)) {
        collection_indices =
            attached_context_->GetTabStripModel()->InsertDetachedTabGroupAt(
                std::move(*detached_tab_collection), index);
      } else {
        collection_indices =
            attached_context_->GetTabStripModel()->InsertDetachedSplitTabAt(
                std::move(*detached_tab_collection), index, pinned);
      }

      CHECK_EQ(collection_indices.start(), index);
      index += collection_indices.length();

      for (size_t sad_index = collection_indices.start();
           sad_index < collection_indices.end(); sad_index++) {
        update_sad_tab.Run(sad_index);
      }
    }
  }

  // If we're dragging a saved group, resume tracking now that the group is
  // re-attached.
  MaybeResumeTrackingSavedTabGroup();

  attached_context_->OwnDragController(std::move(controller));

  AttachImpl();
}

void TabDragController::AttachImpl() {
  const std::vector<TabSlotView*> views =
      GetViewsMatchingDraggedContents(attached_context_);

  DCHECK_EQ(views.size(), drag_data_.tab_drag_data_.size());
  for (size_t i = 0; i < drag_data_.tab_drag_data_.size(); ++i) {
    drag_data_.tab_drag_data_[i].attached_view = views[i];
  }

  ResetSelection(attached_context_->GetTabStripModel());

  // This should be called after ResetSelection() in order to generate
  // bounds correctly. http://crbug.com/40573140
  OnContextStartedDragging(views);

  // Make sure the window has capture. This is important so that if activation
  // changes the drag isn't prematurely canceled.
  CHECK_EQ(SetCapture(attached_context_), Liveness::kAlive);

  attached_context_tabs_closed_tracker_ =
      std::make_unique<DraggedTabsClosedTracker>(
          attached_context_->GetTabStripModel(), this);
}

std::tuple<std::unique_ptr<TabDragController>,
           std::vector<std::variant<std::unique_ptr<DetachedTab>,
                                    std::unique_ptr<DetachedTabCollection>>>>
TabDragController::Detach(ReleaseCapture release_capture) {
  TRACE_EVENT1("views", "TabDragController::Detach", "release_capture",
               release_capture);

  attached_context_tabs_closed_tracker_.reset();

  // Detaching may trigger the Widget bounds to change. Such bounds changes
  // should be ignored as they may lead to reentrancy and bad things happening.
  widget_observation_.Reset();

  // Release ownership of the drag controller and mouse capture. When we
  // reattach ownership is transferred.
  std::unique_ptr<TabDragController> me =
      attached_context_->ReleaseDragController();
  DCHECK_EQ(me.get(), this);

  if (release_capture == ReleaseCapture::kReleaseCapture) {
    attached_context_->GetWidget()->ReleaseCapture();
  }

  TabStripModel* attached_model = attached_context_->GetTabStripModel();

  // If we're dragging a saved tab group, suspend tracking between Detach and
  // Attach. Otherwise, the group will get emptied out as we close all the tabs.
  MaybePauseTrackingSavedTabGroup();

  for (TabDragData& tab_drag_datum : drag_data_.tab_drag_data_) {
    // Marking the view as detached tells the TabStrip to not animate its
    // closure, as it's actually being moved.
    // TODO(tbergquist): Is this the right path for this bit to take? Would it
    // make more sense for the model notification to have this information, and
    // for the tabstrip to use that instead?
    tab_drag_datum.attached_view->set_detached();
    // Detaching may end up deleting the view, drop references to it.
    tab_drag_datum.attached_view = nullptr;
  }

  std::vector<int> dragged_indices;
  for (const auto& data : drag_data_.tab_drag_data_) {
    if (data.contents) {
      const int index = attached_model->GetIndexOfWebContents(data.contents);
      if (index != TabStripModel::kNoTab) {
        dragged_indices.push_back(index);
      }
    }
  }
  std::ranges::sort(dragged_indices);

  int next_active_index = TabStripModel::kNoTab;
  if (drag_data_.group_header_drag_data_.has_value() &&
      drag_data_.group_header_drag_data_->was_collapsed_from_drag &&
      !dragged_indices.empty()) {
    const int last_dragged_index = dragged_indices.back();
    if (last_dragged_index + 1 < attached_model->count()) {
      next_active_index =
          (last_dragged_index + 1) - static_cast<int>(dragged_indices.size());
    } else if (dragged_indices.front() > 0) {
      next_active_index = dragged_indices.front() - 1;
    }
  }

  const std::vector<tab_groups::TabGroupId> groups_to_move =
      attached_model->GetGroupsDestroyedFromRemovingIndices(dragged_indices);

  // If we are detaching from the source tabstrip, make sure the tab that was
  // initially active is selected. This ensures that when the currently active
  // dragged tab is removed, TabStripModel will fall back to activating the
  // initially active tab, preventing a brief flash of an incorrect active tab.
  if (attached_context_ == source_context_ &&
      !initial_selection_model_.empty() &&
      initial_selection_model_.active().has_value()) {
    const int initial_active_index = initial_selection_model_.active().value();
    if (attached_model->ContainsIndex(initial_active_index) &&
        std::ranges::find(dragged_indices, initial_active_index) ==
            dragged_indices.end()) {
      ui::ListSelectionModel selection =
          attached_model->selection_model().GetListSelectionModel();
      selection.AddIndexToSelection(initial_active_index);
      UpdateSelectionModel(attached_model, selection);
    }
  }

  std::vector<std::variant<std::unique_ptr<DetachedTab>,
                           std::unique_ptr<DetachedTabCollection>>>
      owned_tabs_and_collections =
          attached_model->DetachTabsAndCollectionsForInsertion(dragged_indices);

  // If we've removed the last Tab from the TabDragContext, hide the
  // frame now.
  if (!attached_model->empty()) {
    if (!selection_model_before_attach_.empty() &&
        selection_model_before_attach_.active().has_value() &&
        selection_model_before_attach_.active().value() <
            static_cast<size_t>(attached_model->count())) {
      // Restore the selection.
      UpdateSelectionModel(attached_model, selection_model_before_attach_);
    } else if (attached_context_ == source_context_ &&
               !initial_selection_model_.empty()) {
      RestoreInitialSelection();
    }

    if (next_active_index != TabStripModel::kNoTab) {
      attached_model->ActivateTabAt(
          next_active_index,
          TabStripUserGestureDetails(
              TabStripUserGestureDetails::GestureType::kOther));
    }
  }

  OnContextDraggedTabsDetached();
  attached_context_ = nullptr;

  return std::make_tuple(std::move(me), std::move(owned_tabs_and_collections));
}

void TabDragController::DetachAndAttachToNewContext(
    ReleaseCapture release_capture,
    TabDragContext* target_context) {
  auto [me, owned_tabs] = Detach(release_capture);
  AttachToNewContext(target_context, std::move(me), std::move(owned_tabs));
}

TabDragController::Liveness
TabDragController::DetachIntoNewBrowserAndRunMoveLoop(
    gfx::Point point_in_screen) {
  if (attached_context_->GetTabStripModel()->count() ==
      drag_data_.num_dragging_tabs()) {
    // All the tabs in a browser are being dragged but all the tabs weren't
    // initially being dragged. For this to happen the user would have to
    // start dragging a set of tabs, the other tabs close, then detach.

    if (ShouldDragWindowUsingSystemDnD()) {
      return StartSystemDnDSessionIfNecessary(attached_context_,
                                              point_in_screen);
    }

    const bool restore_window =
        attached_context_->GetWidget()->IsMaximized() ||
        (attached_context_->GetWidget()->IsFullscreen() &&
         CanRestoreFullscreenWindowDuringDrag());
    if (attached_context_ == source_context_) {
      did_restore_window_ = restore_window;
    }
    if (restore_window) {
      RestoreAttachedWindowForDrag();
    }

    const gfx::Vector2d drag_offset =
        point_in_screen -
        attached_context_->GetWidget()->GetWindowBoundsInScreen().origin();
    return RunMoveLoop(point_in_screen, drag_offset);
  }

  const gfx::Size new_size = CalculateDraggedWindowSize(attached_context_);

  int previous_tab_area_width;
  int first_tab_leading_x;
  std::vector<gfx::Rect> drag_bounds;
  if (TabDragPositioningDelegate* positioning_delegate =
          attached_context_->GetPositioningDelegate()) {
    previous_tab_area_width = positioning_delegate->GetTabDragAreaWidth();
    first_tab_leading_x = GetTabOffsetForDetachedWindow(point_in_screen);
    drag_bounds = positioning_delegate->CalculateBoundsForDraggedViews(
        drag_data_.attached_views());
  }

  BrowserWindowInterface* browser =
      CreateBrowserForDrag(attached_context_, new_size);

  BrowserView* const dragged_browser_view =
      BrowserView::GetBrowserViewForBrowser(browser);
  views::Widget* const dragged_widget = dragged_browser_view->GetWidget();

#if defined(USE_AURA)
  // Only Aura windows are gesture consumers.
  {
    auto ref = weak_factory_.GetWeakPtr();
    base::WeakAutoReset<TabDragController, bool> reentrant_destruction_guard(
        ref, &TabDragController::expect_stay_alive_, true);

    views::Widget* const attached_widget = attached_context_->GetWidget();
    // Unlike DragBrowserToNewTabStrip, this does not have to special-handle
    // IsUsingWindowServices(), since DesktopWIndowTreeHostMus takes care of it.
    attached_widget->GetGestureRecognizer()->TransferEventsTo(
        attached_widget->GetNativeView(), dragged_widget->GetNativeView(),
        ui::TransferTouchesBehavior::kDontCancel);

    // If `attached_context_` received a gesture end event, it will have ended
    // the drag, destroying `this`. This shouldn't ever happen (preventing this
    // scenario is why we pass kDontCancel above) - https://crbug.com/40060514.
    CHECK(ref) << "Drag session was ended as part of transferring events to "
                  "the new browser. This should not happen.";
  }
#endif

#if BUILDFLAG(IS_CHROMEOS)
  dragged_widget->GetNativeWindow()->SetProperty(
      ash::kTabDraggingSourceWindowKey,
      attached_context_->GetWidget()->GetNativeWindow()->GetWeakPtrAsWindow());

  // On ChromeOS, Detach should release capture; `can_release_capture_` is
  // false on ChromeOS because it can cancel touches, but for this cases
  // the touches are already transferred, so releasing is fine. Without
  // releasing, the capture remains and further touch events can be sent to a
  // wrong target.
  const ReleaseCapture release_capture = ReleaseCapture::kReleaseCapture;
#else
  const ReleaseCapture release_capture =
      can_release_capture_ ? ReleaseCapture::kReleaseCapture
                           : ReleaseCapture::kDontReleaseCapture;
#endif
  TabDragContext* new_context =
      attached_context_->GetContextForNewBrowser(dragged_browser_view);
  DetachAndAttachToNewContext(release_capture, new_context);

  if (ShouldDragWindowUsingSystemDnD()) {
    // Keep the new window hidden and start a system DnD session.
    //
    // We need to pass `source_context_` here, because the new context we're
    // attached to is hidden and thus can't start the drag session.
    return StartSystemDnDSessionIfNecessary(source_context_, point_in_screen);
  }

  if (attached_context_->GetPositioningDelegate()) {
    AdjustTabBoundsForDrag(previous_tab_area_width, first_tab_leading_x,
                           drag_bounds);
  }

  const gfx::Vector2d drag_offset = CalculateWindowDragOffset();
#if (!BUILDFLAG(IS_MAC))
  // Set the window origin before making it visible, to avoid flicker on
  // Windows. See https://crbug.com/394529650
  dragged_widget->SetBounds(
      gfx::Rect(point_in_screen - drag_offset, dragged_widget->GetSize()));
#else
  const gfx::Size widget_size = dragged_widget->GetSize();
#endif

  dragged_widget->SetVisibilityChangedAnimationsEnabled(false);
  browser->GetWindow()->Show();
  dragged_widget->SetVisibilityChangedAnimationsEnabled(true);

  // When InitialWebUI is enabled, the asynchronous loading of WebUI might cause
  // the detached browser window to be momentarily invisible during creation. To
  // prevent the mouse drag from racing ahead of the window rendering and
  // dispatching inputs to the wrong target, we wait for the widget to properly
  // show itself before starting the nested move loop.
  if (base::FeatureList::IsEnabled(features::kInitialWebUI)) {
    if (!dragged_widget->IsVisible()) {
      current_state_ = DragState::kWaitingForWindowToShow;
      VisibilityWaiter waiter(dragged_widget);

#if BUILDFLAG(IS_LINUX)
      // VisibilityWaiter runs a kNestableTasksAllowed loop while the user holds
      // the pointer button, before the move loop's own suppression scope is
      // established; suppress data drags for its duration as well.
      auto suppress_data_drag =
          views::DesktopDragDropClientOzone::ScopedSuppressForWindowMove();
#endif
      base::WeakPtr<TabDragController> ref(weak_factory_.GetWeakPtr());
      waiter.Wait();
      if (!ref) {
        return Liveness::kDeleted;
      }
      current_state_ = DragState::kDraggingWindow;
    }
  }

#if BUILDFLAG(IS_MAC)
  // Set the window origin after making it visible, to avoid child windows (such
  // as the find bar) being misplaced on Mac. See https://crbug.com/403129048
  dragged_widget->SetBounds(
      gfx::Rect(point_in_screen - drag_offset, widget_size));
  last_sized_display_id_ =
      display::Screen::Get()->GetDisplayNearestPoint(point_in_screen).id();
#endif

  // Activate may trigger a focus loss, destroying us.
  {
    base::WeakPtr<TabDragController> ref(weak_factory_.GetWeakPtr());
    browser->GetWindow()->Activate();
    if (!ref) {
      return Liveness::kDeleted;
    }
  }
  return RunMoveLoop(point_in_screen, drag_offset);
}

TabDragController::Liveness TabDragController::RunMoveLoop(
    gfx::Point point_in_screen,
    gfx::Vector2d drag_offset) {
  CHECK(!ShouldDragWindowUsingSystemDnD());

  move_loop_widget_ = GetAttachedBrowserWidget();
  DCHECK(move_loop_widget_);
#if !BUILDFLAG(IS_CHROMEOS)
  // In ChromeOS, `SetBounds` is not used to avoid accidentally moving the
  // window to a different display. `drag_offset` is used to calculate initial
  // location in ToplevelWindowEventHandler.
  // TODO(crbug.com/508016410): Verify if this is necessary and remove it.
  move_loop_widget_->SetBounds(
      gfx::Rect(point_in_screen - drag_offset, move_loop_widget_->GetSize()));
#endif  //! BUILDFLAG(IS_CHROMEOS)
  // RunMoveLoop can be called reentrantly from within another RunMoveLoop,
  // in which case the observation is already established.
  widget_observation_.Reset();
  widget_observation_.Observe(move_loop_widget_.get());
  current_state_ = DragState::kDraggingWindow;
  base::WeakPtr<TabDragController> ref(weak_factory_.GetWeakPtr());
  if (can_release_capture_) {
    // Running the move loop releases mouse capture, which triggers destroying
    // the drag loop. Release mouse capture now while the DragController is not
    // owned by the TabDragContext.
    std::unique_ptr<TabDragController> me =
        attached_context_->ReleaseDragController();
    DCHECK_EQ(me.get(), this);
    attached_context_->GetWidget()->ReleaseCapture();
    attached_context_->OwnDragController(std::move(me));
  }
  const views::Widget::MoveLoopSource move_loop_source =
      event_source_ == ui::mojom::DragEventSource::kMouse
          ? views::Widget::MoveLoopSource::kMouse
          : views::Widget::MoveLoopSource::kTouch;
  const views::Widget::MoveLoopEscapeBehavior escape_behavior =
      is_dragging_new_browser_
          ? views::Widget::MoveLoopEscapeBehavior::kHide
          : views::Widget::MoveLoopEscapeBehavior::kDontHide;

  // Pull into a local to avoid use-after-free if RunMoveLoop deletes `this`.
  base::OnceClosure drag_loop_done_callback =
      std::move(drag_loop_done_callback_);

  // This code isn't set up to handle nested run loops. Nested run loops may
  // lead to all sorts of interesting crashes, and generally indicate a bug
  // lower in the stack. This is a CHECK() as there may be security
  // implications to attempting a nested run loop.

  CHECK(!in_move_loop_);
  in_move_loop_ = true;
  views::Widget::MoveLoopResult result = move_loop_widget_->RunMoveLoop(
      drag_offset, move_loop_source, escape_behavior);
  // Note: `this` can be deleted here!

  if (drag_loop_done_callback) {
    std::move(drag_loop_done_callback).Run();
  }

  if (!ref) {
    return Liveness::kDeleted;
  }

  in_move_loop_ = false;
  widget_observation_.Reset();
  move_loop_widget_ = nullptr;

  if (current_state_ == DragState::kWaitingToExitRunLoop) {
    current_state_ = DragState::kDraggingTabs;
  } else if (current_state_ == DragState::kWaitingToDragTabs) {
    DCHECK(tab_strip_to_attach_to_after_exit_);
    DetachAndAttachToNewContext(ReleaseCapture::kDontReleaseCapture,
                                tab_strip_to_attach_to_after_exit_);
    current_state_ = DragState::kDraggingTabs;

    // Move the tabs into position.
    StartDraggingTabsSession(false, GetCursorScreenPoint());
    attached_context_->GetWidget()->Activate();
    // Activate may trigger a focus loss, destroying us.
    if (!ref) {
      return Liveness::kDeleted;
    }
    tab_strip_to_attach_to_after_exit_ = nullptr;
  } else if (current_state_ == DragState::kDraggingWindow) {
    EndDrag(result == views::Widget::MoveLoopResult::kCanceled
                ? EndDragReason::kCancel
                : EndDragReason::kComplete);
    return Liveness::kDeleted;
  }

  return Liveness::kAlive;
}

std::vector<TabSlotView*> TabDragController::GetViewsMatchingDraggedContents(
    TabDragContext* context) {
  const TabStripModel* const model = context->GetTabStripModel();
  std::vector<TabSlotView*> views;
  for (const TabDragData& tab_drag_datum : drag_data_.tab_drag_data_) {
    // TODO(crbug.com/477230662): Ideally, split tabs would also be directly
    // compatible with `TabDragController`, rather than relying on the tab
    // selection model to achieve expected behavior. As is, this is incompatible
    // with the vertical tab strip because the split tabs are contained within
    // a single dedicated `SplitTabView`.
    if (tab_drag_datum.view_type == TabSlotView::ViewType::kTab) {
      TabSlotView* tab_view =
          context->GetTabForContents(tab_drag_datum.contents);
      if (!tab_view) {
        return {};
      }
      views.push_back(tab_view);
    } else {
      // Return empty vector if the group is not present in the model.
      if (!model->group_model()->ContainsTabGroup(
              tab_drag_datum.tab_group_data->group_id)) {
        return {};
      }

      TabSlotView* header_view =
          context->GetTabGroupHeader(tab_drag_datum.tab_group_data->group_id);
      CHECK(header_view);
      views.push_back(header_view);
    }
  }
  return views;
}

void TabDragController::EndDragImpl(EndDragType type) {
  VLOG(1) << __func__ << " type=" << static_cast<int>(type)
          << " state=" << static_cast<int>(current_state_);

  DragState previous_state = current_state_;
  current_state_ = DragState::kStopped;
  attached_context_tabs_closed_tracker_.reset();

  bring_to_front_timer_.Stop();

  // Before clearing the tab drag data check to see if there was a tab that was
  // dragged from out of a tab group into a tab group.
  NotifyEventIfTabAddedToGroup();

  if (type != EndDragType::kTabDestroyed) {
    // We only finish up the drag if we were actually dragging. If start_drag_
    // is false, the user just clicked and released and didn't move the mouse
    // enough to trigger a drag.
    if (previous_state != DragState::kNotStarted) {
      // After the drag ends, sometimes it shouldn't restore the focus, because
      // - Some dragging gesture (like fling down) minimizes the window, but the
      //   window activation cancels minimized status. See
      //   https://crbug.com/40601420
      if (!attached_context_->GetWidget()->IsMinimized()) {
        RestoreFocus();
      }

      GetAttachedBrowserWidget()->SetCanAppearInExistingFullscreenSpaces(false);
      if (type == EndDragType::kCanceled) {
        RevertDrag();
      } else {
        if (previous_state == DragState::kDraggingUsingSystemDnD) {
// `views::CancelDragDropLoop()` is only available on Aura, and on all non-Aura
// platforms `IsMoveLoopSupported()` returns true anyways.
#if defined(USE_AURA)
          // Make sure the drag session ends.
          if (system_drag_and_drop_session_running_) {
            // We need to pass `allow_widget_mismatch=true` here, because
            // without it we're not allowed to cancel drags initiated by a
            // different widget; but the initiating widget (the one belonging to
            // `source_context_`) might have been destroyed during the drag (for
            // example when dragging all tabs of a window into another window).
            gfx::NativeView view = GetAttachedBrowserWidget()->GetNativeView();
            views::CancelDragDropLoop(view, /*allow_widget_mismatch=*/true);
          }
#endif  // defined(USE_AURA)

          // Make the hidden window containing the dragged tabs visible.
          GetAttachedBrowserWidget()->Show();
        }
        CompleteDrag();
      }
    }
  } else if (drag_data_.tab_drag_data_.size() > 1) {
    initial_selection_model_.Clear();
    if (previous_state != DragState::kNotStarted) {
      RevertDrag();
    }
  }  // else case the only tab we were dragging was deleted. Nothing to do.

  // Clear out drag data so we don't attempt to do anything with it.
  drag_data_.tab_drag_data_.clear();
  if (current_drag_target_) {
    current_drag_target_->OnTabDragEnded();
    ResetDragTarget();
  }

  TabDragContext* owning_context =
      attached_context_ ? attached_context_.get() : source_context_.get();
  owning_context->DestroyDragController();
}

void TabDragController::RevertDrag() {
  CHECK(attached_context_);
  CHECK(source_context_);

  dragging_tabs_session_ = nullptr;

  // If we're dragging a saved tab group, suspend tracking during the revert.
  // Otherwise, the group will get emptied out as we revert all the tabs.
  MaybePauseTrackingSavedTabGroup();

  base::AutoReset<bool> is_mutating_setter(&is_mutating_, true);
  base::AutoReset<bool> is_removing_last_tab_setter(&is_moving_last_tab_, true);

  // If we support a mixture of dragging between VT and HT, then update this
  // metric accordingly.
  base::UmaHistogramEnumeration(
      absl::StrFormat("TabStrip.%s.TabDragDestination", GetTabStripMode()),
      TabDraggingDestination::kAbandoned);

  if (attached_context_ != source_context_) {
    for (TabDragData& tab_datum : drag_data_.tab_drag_data_) {
      tab_datum.attached_view->set_detached();
      tab_datum.attached_view = nullptr;
    }
  }

  // Revert each tab, split and group. We manually increment `i` because
  // each group or split has multiple entries in `tab_drag_data_`.
  for (size_t i = 0; i < drag_data_.tab_drag_data_.size();) {
    const TabDragData tab_data = drag_data_.tab_drag_data_[i];
    if (tab_data.view_type == TabSlotView::ViewType::kTabGroupHeader) {
      RevertGroupAt(i);
      // Skip all the tabs in the group too.
      i += source_context_->GetTabStripModel()
               ->group_model()
               ->GetTabGroup(tab_data.tab_group_data->group_id)
               ->tab_count() +
           1;
    } else {
      CHECK(tab_data.contents);

      const tabs::TabInterface* tab =
          tabs::TabInterface::GetFromContents(tab_data.contents);

      if (tab->IsSplit()) {
        split_tabs::SplitTabId split_id = tab->GetSplit().value();
        RevertSplitAt(i);
        i += source_context_->GetTabStripModel()
                 ->GetSplitData(split_id)
                 ->ListTabs()
                 .size();
      } else {
        RevertTabAt(i);
        i++;
      }
    }
  }
  MaybeResumeTrackingSavedTabGroup();

  if (did_restore_window_) {
    MaximizeAttachedWindow();
  }
  if (attached_context_ == source_context_) {
    OnContextStoppedDragging();
  } else {
    OnContextDraggedTabsDetached();
  }

  // If tabs were closed during this drag, the initial selection might include
  // indices that are out of bounds for the tabstrip now. Reset the selection to
  // include the still-existing currently dragged WebContentses.
  for (int selection : initial_selection_model_.selected_indices()) {
    if (!source_context_->GetTabStripModel()->ContainsIndex(selection)) {
      initial_selection_model_.Clear();
      break;
    }
  }

  if (initial_selection_model_.empty()) {
    ResetSelection(source_context_->GetTabStripModel());
  } else {
    UpdateSelectionModel(source_context_->GetTabStripModel(),
                         initial_selection_model_);
  }

  source_context_->GetWidget()->Activate();
}

void TabDragController::ResetSelection(TabStripModel* model) {
  DCHECK(model);
  ui::ListSelectionModel selection_model;
  bool has_one_valid_tab = false;
  for (size_t i = 0; i < drag_data_.tab_drag_data_.size(); ++i) {
    // `contents` is NULL if a tab was deleted out from under us.
    if (drag_data_.tab_drag_data_[i].contents) {
      int index =
          model->GetIndexOfWebContents(drag_data_.tab_drag_data_[i].contents);
      DCHECK_GE(index, 0);
      selection_model.AddIndexToSelection(static_cast<size_t>(index));
      // Set this tab as active if:
      // a) we don't have an active tab yet
      // b) this was the source view for the drag
      // c) we're in a header drag, and this tab was active before the drag
      if (!has_one_valid_tab || i == drag_data_.source_view_index_ ||
          (drag_data_.group_header_drag_data_.has_value() &&
           (drag_data_.group_header_drag_data_.value()
                .active_tab_index_within_group +
            1) == static_cast<int>(i))) {
        // Reset the active/lead to the first tab. If the source tab is still
        // valid we'll reset these again later on.
        selection_model.set_active(static_cast<size_t>(index));
        selection_model.set_anchor(static_cast<size_t>(index));
        has_one_valid_tab = true;
      }
    }
  }
  if (!has_one_valid_tab) {
    return;
  }

  UpdateSelectionModel(model, selection_model);
}

void TabDragController::RestoreInitialSelection() {
  // First time detaching from the source tabstrip. Reset selection model to
  // initial_selection_model_. Before resetting though we have to remove all
  // the tabs from initial_selection_model_ as it was created with the tabs
  // still there.
  ui::ListSelectionModel selection_model = initial_selection_model_;
  for (const TabDragData& data :
       std::views::reverse(drag_data_.tab_drag_data_)) {
    if (data.source_model_index.has_value()) {
      selection_model.DecrementFrom(data.source_model_index.value());
    }
  }
  // We may have cleared out the selection model. Only reset it if it
  // contains something.
  if (selection_model.empty()) {
    return;
  }

  // Tabs in `source_context_` may have closed since the drag began. In that
  // case, `initial_selection_model_` may include indices that are no longer
  // valid in `source_context_`. Abort restoring the selection if so.
  if (!source_context_->GetTabStripModel()->ContainsIndex(
          *(selection_model.selected_indices().rbegin()))) {
    return;
  }

  // The anchor/active may have been among the tabs that were dragged out. Force
  // the anchor/active to be valid.
  if (!selection_model.anchor().has_value()) {
    selection_model.set_anchor(*selection_model.selected_indices().begin());
  }
  if (!selection_model.active().has_value()) {
    selection_model.set_active(*selection_model.selected_indices().begin());
  }
  UpdateSelectionModel(source_context_->GetTabStripModel(), selection_model);
}

void TabDragController::RevertGroupAt(size_t drag_index) {
  const tab_groups::TabGroupId group_id =
      drag_data_.tab_drag_data_[drag_index].tab_group_data->group_id;
  const TabDragData first_tab_in_group =
      drag_data_.tab_drag_data_[drag_index + 1];
  int target_index = first_tab_in_group.source_model_index.value();
  if (attached_context_ != source_context_) {
    source_context_->GetTabStripModel()->InsertDetachedTabGroupAt(
        attached_context_->GetTabStripModel()->DetachTabGroupForInsertion(
            group_id),
        target_index);
    source_context_->GetTabStripModel()->ChangeTabGroupVisuals(
        group_id, first_tab_in_group.tab_group_data->group_visual_data);
    return;
  }

  const int index =
      attached_context_->GetTabStripModel()->GetIndexOfWebContents(
          first_tab_in_group.contents);
  CHECK_NE(index, TabStripModel::kNoTab);

  if (target_index > index) {
    for (size_t i = drag_index + 1; i < drag_data_.tab_drag_data_.size(); ++i) {
      const TabDragData other_tab = drag_data_.tab_drag_data_[i];
      // Ignore group headers, they don't have model indices to skip over.
      if (other_tab.view_type != TabSlotView::ViewType::kTab) {
        continue;
      }
      // Ignore other tabs in this group, they will get moved along with us
      // so we won't skip over them.
      if (other_tab.tab_group_data.has_value() &&
          other_tab.tab_group_data->group_id == group_id) {
        continue;
      }

      ++target_index;
    }
  }

  source_context_->GetTabStripModel()->MoveGroupTo(
      first_tab_in_group.tab_group_data->group_id, target_index);
}

void TabDragController::RevertTabAt(size_t drag_index) {
  CHECK_NE(current_state_, DragState::kNotStarted);
  CHECK(attached_context_);
  CHECK(source_context_);
  // We can't revert if `contents` was destroyed during the drag, or if this is
  // a group header.
  CHECK(drag_data_.tab_drag_data_[drag_index].contents);

  const TabDragData tab_data = drag_data_.tab_drag_data_[drag_index];

  // The tab will be reverted into its original group.
  const std::optional<tab_groups::TabGroupId> existing_group =
      tab_data.tab_group_data.has_value()
          ? std::make_optional(tab_data.tab_group_data->group_id)
          : std::nullopt;

  const int from_index =
      attached_context_->GetTabStripModel()->GetIndexOfWebContents(
          tab_data.contents);
  CHECK_NE(from_index, TabStripModel::kNoTab);
  int target_index = tab_data.source_model_index.value();

  if (tab_data.tab_group_data.has_value() &&
      attached_context_ != source_context_) {
    // If the tab is going back to its original group in a different context,
    // use the relative index to ensure it lands in a valid position within
    // the group's current range.
    const TabGroup* group =
        source_context_->GetTabStripModel()->group_model()->GetTabGroup(
            existing_group.value());
    if (group) {
      target_index =
          group->ListTabs().start() + tab_data.tab_group_data->index_in_group;
    }
  }

  if (attached_context_ != source_context_) {
    // The Tab was inserted into another TabDragContext. We need to
    // put it back into the original one.

    std::unique_ptr<tabs::TabModel> detached_tab =
        attached_context_->GetTabStripModel()->DetachTabAtForInsertion(
            from_index);
    source_context_->GetTabStripModel()->InsertDetachedTabAt(
        target_index, std::move(detached_tab),
        (tab_data.pinned ? AddTabTypes::ADD_PINNED : 0), existing_group);
  } else {
    // The Tab was moved within the TabDragContext where the drag
    // was initiated. Move it back to the starting location.

    // If the target index is to the right, then other unreverted tabs are
    // occupying indices between this tab and the target index. Those
    // unreverted tabs will later be reverted to the right of the target
    // index, so we skip those indices.
    if (target_index > from_index) {
      for (size_t i = drag_index + 1; i < drag_data_.tab_drag_data_.size();
           ++i) {
        if (drag_data_.tab_drag_data_[i].contents) {
          ++target_index;
        }
      }
    }

    source_context_->GetTabStripModel()->MoveWebContentsAt(
        from_index, target_index, false, existing_group);
  }
}

void TabDragController::RevertSplitAt(size_t drag_index) {
  CHECK_NE(current_state_, DragState::kNotStarted);
  CHECK(attached_context_);
  CHECK(source_context_);
  // We can't revert if `contents` was destroyed during the drag, or if this is
  // a group header.
  CHECK(drag_data_.tab_drag_data_[drag_index].contents);

  const TabDragData tab_data = drag_data_.tab_drag_data_[drag_index];
  const tabs::TabInterface* tab =
      tabs::TabInterface::GetFromContents(tab_data.contents);
  split_tabs::SplitTabId split_id = tab->GetSplit().value();

  // The split will be reverted into its original group.
  const std::optional<tab_groups::TabGroupId> existing_group =
      tab_data.tab_group_data.has_value()
          ? std::make_optional(tab_data.tab_group_data->group_id)
          : std::nullopt;

  const int from_index =
      attached_context_->GetTabStripModel()->GetIndexOfWebContents(
          tab_data.contents);
  CHECK_NE(from_index, TabStripModel::kNoTab);
  int target_index = tab_data.source_model_index.value();

  if (attached_context_ != source_context_) {
    // The Split was inserted into another TabDragContext. We need to
    // put it back into the original one.
    std::unique_ptr<DetachedTabCollection> detached_split =
        attached_context_->GetTabStripModel()->DetachSplitTabForInsertion(
            split_id);
    source_context_->GetTabStripModel()->InsertDetachedSplitTabAt(
        std::move(detached_split), target_index, tab_data.pinned,
        existing_group);
  } else {
    if (target_index > from_index) {
      for (size_t i = drag_index + 1; i < drag_data_.tab_drag_data_.size();
           ++i) {
        const TabDragData other_tab = drag_data_.tab_drag_data_[i];

        // Ignore group headers, they don't have model indices to skip over.
        if (other_tab.view_type != TabSlotView::ViewType::kTab) {
          continue;
        }

        tabs::TabInterface* other_tab_interface =
            tabs::TabInterface::GetFromContents(other_tab.contents);

        CHECK(other_tab_interface);

        // Ignore other tabs in this split, they will get moved along with us
        // so we won't skip over them.
        if (other_tab_interface->GetSplit() == split_id) {
          continue;
        }

        ++target_index;
      }
    }

    source_context_->GetTabStripModel()->MoveSplitTo(
        split_id, target_index, tab_data.pinned, existing_group);
  }
}

void TabDragController::CompleteDrag() {
  CHECK_NE(current_state_, DragState::kNotStarted);
  CHECK(attached_context_);

  TabDraggingDestination destination = TabDraggingDestination::kExistingWindow;
  if (is_dragging_new_browser_) {
    destination = TabDraggingDestination::kNewWindow;
  } else if (source_context_ == attached_context_) {
    destination = TabDraggingDestination::kSameWindow;
  }
  // If we support a mixture of dragging between VT and HT, then update this
  // metric accordingly.
  base::UmaHistogramEnumeration(
      absl::StrFormat("TabStrip.%s.TabDragDestination", GetTabStripMode()),
      destination);

  bool has_pinned = false;
  bool has_unpinned = false;
  for (const auto& data : drag_data_.tab_drag_data_) {
    if (data.view_type == TabSlotView::ViewType::kTabGroupHeader ||
        !data.pinned) {
      has_unpinned = true;
    } else if (data.pinned) {
      has_pinned = true;
    }
  }

  TabDragPinnedness pinnedness;
  if (has_pinned && has_unpinned) {
    pinnedness = TabDragPinnedness::kMixed;
  } else if (has_pinned) {
    pinnedness = TabDragPinnedness::kAllPinned;
  } else {
    pinnedness = TabDragPinnedness::kAllUnpinned;
  }

  base::UmaHistogramEnumeration(
      absl::StrFormat("TabStrip.%s.TabDragPinnedness", GetTabStripMode()),
      pinnedness);

  if (current_drag_target_ && current_drag_target_->CanDropTab()) {
    current_drag_target_->HandleTabDrop(*this);
    OnContextStoppedDragging();

    // The delegate is expected to handle all tab dragging finalization, and
    // therefore we return here.
    // The logic below is specific for dragging to a tabstrip, most of which
    // should be moved to tabstrip's `TabDragTarget` implementation. Some
    // functionality, such as restoring tab model selection, may be shared as
    // part of the `TabDragTarget::DragController` interface.
    return;
  }

  if (is_dragging_new_browser_ || did_restore_window_) {
    if (IsSnapped(attached_context_)) {
      was_source_maximized_ = false;
      was_source_fullscreen_ = false;
    }

    // If source window was maximized - maximize the new window as well.
#if !BUILDFLAG(IS_WIN) && !BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_MAC)
    // Keeping maximized state breaks snap to Grid on Windows when dragging
    // tabs from maximized windows. TODO:(crbug.com/41321838) Explore doing this
    // for other desktop OS's. kMaximizedStateRetainedOnTabDrag in
    // tab_drag_controller_interactive_uitest.cc will need to be initialized
    // to false on each desktop OS that changes this behavior.
    // macOS opts out since this maps maximize to fullscreen, which can
    // violate user expectations and interacts poorly with some window
    // management actions.
    if (was_source_maximized_ || was_source_fullscreen_) {
      MaximizeAttachedWindow();
    }
#endif  // !BUILDFLAG(IS_WIN) && !BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_MAC)
  }
  OnContextStoppedDragging();

  // Tabbed PWAs with a home tab should have a home tab in every window.
  // This means when dragging tabs out to create a new window, a home tab
  // needs to be added.
  if (is_dragging_new_browser_) {
    BrowserWindowInterface* new_browser =
        BrowserView::GetBrowserViewForNativeWindow(
            attached_context_->GetWidget()->GetNativeWindow())
            ->browser();

    auto* const app_controller =
        web_app::AppBrowserController::From(new_browser);
    if (app_controller && app_controller->has_tab_strip()) {
      web_app::MaybeAddPinnedHomeTab(new_browser, app_controller->app_id());
    }
  }

  if (drag_data_.group_header_drag_data_.has_value()) {
    // Manually reset the selection to just the active tab in the group.
    // For multi tab select keep the selection model as the user used it as the
    // original selection.
    TabStripModel* model = attached_context_
                               ? attached_context_->GetTabStripModel()
                               : source_context_->GetTabStripModel();
    ui::ListSelectionModel selection;
    // Offset by 1 to account for the group header.
    const int drag_data_index = 1 + drag_data_.group_header_drag_data_.value()
                                        .active_tab_index_within_group;
    const int index = model->GetIndexOfWebContents(
        drag_data_.tab_drag_data_[drag_data_index].contents);

    // The tabs in the group may have been closed during the drag.
    if (index != TabStripModel::kNoTab) {
      DCHECK_GE(index, 0);
      selection.AddIndexToSelection(static_cast<size_t>(index));
      selection.set_active(static_cast<size_t>(index));
      selection.set_anchor(static_cast<size_t>(index));
      UpdateSelectionModel(model, selection);
    }
  }
}

void TabDragController::MaximizeAttachedWindow() {
  GetAttachedBrowserWidget()->Maximize();
#if BUILDFLAG(IS_MAC)
  if (was_source_fullscreen_) {
    GetAttachedBrowserWidget()->SetFullscreen(true);
  }
#endif
#if BUILDFLAG(IS_CHROMEOS)
  if (was_source_fullscreen_) {
    // In fullscreen mode it is only possible to get here if the source
    // was in "immersive fullscreen" mode, so toggle it back on.
    BrowserView* browser_view = BrowserView::GetBrowserViewForNativeWindow(
        GetAttachedBrowserWidget()->GetNativeWindow());
    DCHECK(browser_view);
    if (!browser_view->IsFullscreen()) {
      chrome::ToggleFullscreenMode(browser_view->browser(),
                                   /*user_initiated=*/false);
    }
  }
#endif
}

void TabDragController::BringWindowUnderPointToFront(
    const gfx::Point& point_in_screen) {
  gfx::NativeWindow native_window;
  if (GetLocalProcessWindow(point_in_screen, true, &native_window) ==
      Liveness::kDeleted) {
    return;
  }

  if (!native_window) {
    return;
  }

  // Only bring browser windows to front - only windows with a
  // TabDragContext can be tab drag targets.
  if (!CanAttachTo(native_window)) {
    return;
  }

  views::Widget* widget_window =
      views::Widget::GetWidgetForNativeWindow(native_window);
  if (!widget_window) {
    return;
  }

#if BUILDFLAG(IS_CHROMEOS)
  // TODO(varkha): The code below ensures that the phantom drag widget
  // is shown on top of browser windows. The code should be moved to ash/
  // and the phantom should be able to assert its top-most state on its own.
  // One strategy would be for DragWindowController to
  // be able to observe stacking changes to the phantom drag widget's
  // siblings in order to keep it on top. One way is to implement a
  // notification that is sent to a window parent's observers when a
  // stacking order is changed among the children of that same parent.
  // Note that OnWindowStackingChanged is sent only to the child that is the
  // argument of one of the Window::StackChildX calls and not to all its
  // siblings affected by the stacking change.
  aura::Window* browser_window = widget_window->GetNativeView();
  // Find a topmost non-popup window and stack the recipient browser above
  // it in order to avoid stacking the browser window on top of the phantom
  // drag widget created by DragWindowController in a second display.
  for (aura::Window* window :
       std::views::reverse(browser_window->parent()->children())) {
    // If the iteration reached the recipient browser window then it is
    // already topmost and it is safe to return with no stacking change.
    if (window == browser_window) {
      return;
    }
    if (window->GetType() != aura::client::WINDOW_TYPE_POPUP) {
      widget_window->StackAbove(window);
      break;
    }
  }
#else
  widget_window->StackAtTop();
#endif

  // The previous call made the window appear on top of the dragged window,
  // move the dragged window to the front.
  if (current_state_ == DragState::kDraggingWindow) {
    attached_context_->GetWidget()->StackAtTop();
  }
}

TabDragController::Liveness TabDragController::SetCapture(
    TabDragContext* context) {
  auto ref = weak_factory_.GetWeakPtr();
  context->GetWidget()->SetCapture(context);
  return ref ? Liveness::kAlive : Liveness::kDeleted;
}

bool TabDragController::IsDraggingTab(content::WebContents* contents) const {
  for (auto& drag_data : drag_data_.tab_drag_data_) {
    if (drag_data.contents == contents) {
      return true;
    }
  }
  return false;
}

views::Widget* TabDragController::GetAttachedBrowserWidget() {
  return attached_context_->GetWidget();
}

void TabDragController::RestoreAttachedWindowForDrag() {
  const gfx::Size restored_size = CalculateDraggedWindowSize(attached_context_);

  views::Widget* widget = attached_context_->GetWidget();
  widget->SetVisibilityChangedAnimationsEnabled(false);
  widget->Restore();
  widget->SetVisibilityChangedAnimationsEnabled(true);

  widget->SetSize(restored_size);
}

gfx::Size TabDragController::CalculateDraggedWindowSize(
    TabDragContext* source) {
  gfx::Size new_size(initial_window_size_);

  // Limit the window size to the current display's size, less some insets.
  const gfx::Size work_area =
      display::Screen::Get()
          ->GetDisplayNearestPoint(last_point_in_screen_)
          .work_area()
          .size();
  if (new_size.width() > work_area.width()) {
    new_size.set_width(
        std::max(0, work_area.width() - 2 * kMaximizedWindowInset));
  }
  if (new_size.height() > work_area.height()) {
    new_size.set_height(
        std::max(0, work_area.height() - 2 * kMaximizedWindowInset));
  }

  if (source->GetWidget()->IsMaximized()) {
    // If the restore bounds is really small, we don't want to honor it
    // (dragging a really small window looks wrong), instead make sure the new
    // window is at least 50% the size of the old.
    const gfx::Size maximized_size(
        source->GetWidget()->GetWindowBoundsInScreen().size());
    new_size.set_width(std::max(maximized_size.width() / 2, new_size.width()));
    new_size.set_height(
        std::max(maximized_size.height() / 2, new_size.height()));
  }

  return new_size;
}

int TabDragController::GetTabOffsetForDetachedWindow(
    gfx::Point point_in_screen) {
  CHECK(attached_context_);
  TabDragPositioningDelegate* positioning_delegate =
      attached_context_->GetPositioningDelegate();
  CHECK(positioning_delegate);
  const gfx::Point attached_point =
      views::View::ConvertPointFromScreen(attached_context_, point_in_screen);
  if (attached_point.x() < positioning_delegate->TabDragAreaBeginX()) {
    // Detaching to the left; tabs should be at the beginning of the window.
    return 0;
  }
  if (attached_point.x() >= positioning_delegate->TabDragAreaEndX()) {
    // Detaching to the right; tabs should be at the beginning of the window.
    return 0;
  }

  // Detaching above or below; tabs should keep their current offset.
  return drag_data_.tab_drag_data_[0].attached_view->bounds().x();
}

void TabDragController::AdjustTabBoundsForDrag(
    int previous_tab_area_width,
    int first_tab_leading_x,
    std::vector<gfx::Rect> drag_bounds) {
  CHECK(!ShouldDragWindowUsingSystemDnD());
  TabDragPositioningDelegate* positioning_delegate =
      attached_context_->GetPositioningDelegate();
  CHECK(positioning_delegate);

  positioning_delegate->ForceLayout();
  const int current_tab_area_width =
      positioning_delegate->GetTabDragAreaWidth();

  // If the new tabstrip region is smaller than the old, resize and reposition
  // the tabs to provide a sense of continuity.
  if (current_tab_area_width < previous_tab_area_width) {
    // TODO(crbug.com/40839358): Fix the case where the source window
    // spans two monitors horizontally, and IsRTL is true.

    // `leading_ratio` is the proportion of the previous tab area width which is
    // ahead of the first dragged tab's previous position.
    const float leading_ratio =
        first_tab_leading_x / static_cast<float>(previous_tab_area_width);

    // If the tabs can fit within the new tab area with room to spare, align
    // them within it so the leading tab is in the same position as it was in
    // the previous tab area, proportionally speaking.
    if (drag_bounds.back().right() < current_tab_area_width) {
      // The tabs must stay within the tabstrip.
      const int maximum_tab_x =
          current_tab_area_width -
          (drag_bounds.back().right() - drag_bounds.front().x());
      const int leading_tab_x =
          std::min(static_cast<int>(leading_ratio * current_tab_area_width),
                   maximum_tab_x);
      OffsetX(leading_tab_x, &drag_bounds);
    }
  } else {
    OffsetX(first_tab_leading_x, &drag_bounds);
  }
  positioning_delegate->SetBoundsForDrag(drag_data_.attached_views(),
                                         drag_bounds);
}

std::optional<webapps::AppId> TabDragController::GetControllingAppForDrag(
    BrowserWindowInterface* browser) {
  content::WebContents* active_contents = drag_data_.source_dragged_contents();
  if (!base::FeatureList::IsEnabled(
          features::kTearOffWebAppTabOpensWebAppWindow) ||
      drag_data_.tab_drag_data_.size() != 1 || !active_contents) {
    return std::nullopt;
  }
  const web_app::WebAppProvider* provider =
      web_app::WebAppProvider::GetForWebApps(browser->GetProfile());
  const base::flat_map<webapps::AppId, std::string> all_controlling_apps =
      provider->registrar_unsafe().GetAllAppsControllingUrl(
          active_contents->GetLastCommittedURL());
  if (all_controlling_apps.size() != 1) {
    return std::nullopt;
  }

  return all_controlling_apps.begin()->first;
}

BrowserWindowInterface* TabDragController::CreateBrowserForDrag(
    TabDragContext* source,
    gfx::Size initial_size) {
  source->GetWidget()
      ->GetCompositor()
      ->RequestSuccessfulPresentationTimeForNextFrame(base::BindOnce(
          [](base::TimeTicks now,
             const viz::FrameTimingDetails& frame_timing_details) {
            base::TimeTicks presentation_timestamp =
                frame_timing_details.presentation_feedback.timestamp;
            UmaHistogramTimes(kDragToNewBrowserPresentationTimeHistogram,
                              presentation_timestamp - now);
          },
          base::TimeTicks::Now()));

  // Find if there's a controlling app, and thus we should open an app window.
  BrowserWindowInterface* from_browser =
      BrowserView::GetBrowserViewForNativeWindow(
          GetAttachedBrowserWidget()->GetNativeWindow())
          ->browser();

  const std::optional<webapps::AppId> controlling_app =
      GetControllingAppForDrag(from_browser);
  const bool open_as_web_app = controlling_app.has_value();

  BrowserWindowCreateParams create_params =
      open_as_web_app ? BrowserWindowCreateParams::CreateForApp(
                            web_app::GenerateApplicationNameFromAppId(
                                controlling_app.value()),
                            /* trusted_source=*/true, gfx::Rect(),
                            from_browser->GetProfile(),
                            /* user_gesture=*/true)
                      : BrowserInitState::From(from_browser)
                            ->browser_window_create_params()
                            .Clone();
  // Web app windows have their own initial size independent of the source
  // browser window.
  if (!open_as_web_app) {
    create_params.initial_bounds = gfx::Rect(initial_size);
  }
  create_params.from_user_gesture = true;
  create_params.in_tab_dragging = true;
#if BUILDFLAG(IS_CHROMEOS)
  // Do not copy attached window's restore id as this will cause Full Restore to
  // restore the newly created browser using the original browser's stored data.
  // See crbug.com/40181917 and crbug.com/40227947 for details.
  create_params.restore_id = BrowserWindowCreateParams::kDefaultRestoreId;

  // Open the window in the same display.
  display::Display display = display::Screen::Get()->GetDisplayNearestWindow(
      source->GetWidget()->GetNativeWindow());
  create_params.display_id = display.id();
#endif
  // Do not copy attached window's show state as the attached window might be a
  // maximized or fullscreen window and we do not want the newly created browser
  // window is a maximized or fullscreen window since it will prevent window
  // moving/resizing on Chrome OS. See crbug.com/40107205 for details.
  create_params.initial_show_state = ui::mojom::WindowShowState::kDefault;

  // Don't copy the initial workspace since the *current* workspace might be
  // different and copying the workspace will move the tab to the initial one.
  create_params.initial_workspace = std::string();

  // Don't copy the window name - the user's deliberately creating a new window,
  // which should default to its own auto-generated name, not the same name as
  // the previous window.
  create_params.user_title = std::string();

  base::TimeTicks now = base::TimeTicks::Now();
  BrowserWindowInterface* browser =
      CreateBrowserWindow(std::move(create_params));
  if (auto* manager = InitialWebUIWindowMetricsManager::From(browser)) {
    manager->SetWindowCreationInfo(
        waap::NewWindowCreationSource::kDragToNewWindow, now);
  }
  is_dragging_new_browser_ = true;
  BrowserView::GetBrowserViewForBrowser(browser)
      ->GetWidget()
      ->SetCanAppearInExistingFullscreenSpaces(true);

#if !BUILDFLAG(IS_CHROMEOS)
  // If the window is created maximized then the bounds we supplied are ignored.
  // We need to reset them again so they are honored. On ChromeOS, this is
  // handled in NativeWidgetAura.
  if (!open_as_web_app) {
    browser->GetWindow()->SetBounds(gfx::Rect(initial_size));
  }
#endif

  return browser;
}

gfx::Point TabDragController::GetCursorScreenPoint() {
#if BUILDFLAG(IS_CHROMEOS)
  views::Widget* widget = GetAttachedBrowserWidget();
  DCHECK(widget);
  aura::Window* widget_window = widget->GetNativeWindow();
  DCHECK(widget_window->GetRootWindow());
  return aura::Env::GetInstance()->GetLastPointerPoint(
      event_source_, widget_window, /*fallback=*/last_point_in_screen_);
#else
  return display::Screen::Get()->GetCursorScreenPoint();
#endif
}

gfx::Vector2d TabDragController::CalculateWindowDragOffset() {
  const views::View* source_view =
      drag_data_.attached_views()[drag_data_.source_view_index_];
  const gfx::Rect source_tab_bounds = source_view->bounds();
  const int cursor_x_offset_within_tab = base::ClampRound(
      source_tab_bounds.width() * drag_data_.mouse_offset_to_size_ratios.x());
  const int cursor_y_offset_within_tab = base::ClampRound(
      source_tab_bounds.height() * drag_data_.mouse_offset_to_size_ratios.y());
  gfx::Point desired_cursor_pos_in_widget(
      attached_context_->GetMirroredXInView(source_tab_bounds.x() +
                                            cursor_x_offset_within_tab),
      source_tab_bounds.y() + cursor_y_offset_within_tab);
  desired_cursor_pos_in_widget = views::View::ConvertPointToTarget(
      source_view->parent(), attached_context_, desired_cursor_pos_in_widget);
  views::View::ConvertPointToWidget(attached_context_,
                                    &desired_cursor_pos_in_widget);
  return desired_cursor_pos_in_widget.OffsetFromOrigin();
}

TabDragController::Liveness TabDragController::GetLocalProcessWindow(
    const gfx::Point& screen_point,
    bool exclude_dragged_view,
    gfx::NativeWindow* window) {
  std::set<gfx::NativeWindow> exclude;
  if (exclude_dragged_view) {
    gfx::NativeWindow dragged_window =
        attached_context_->GetWidget()->GetNativeWindow();
    if (dragged_window) {
      exclude.insert(dragged_window);
    }
  }

#if BUILDFLAG(IS_LINUX)
  // Exclude windows which are pending deletion via Browser::TabStripEmpty().
  // These windows can be returned in the Linux Aura port because the browser
  // window which was used for dragging is not hidden once all of its tabs are
  // attached to another browser window in DragBrowserToNewTabStrip().
  // TODO(pkotwicz): Fix this properly (crbug.com/41098538)
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&exclude](BrowserWindowInterface* browser) {
        if (browser->GetTabStripModel()->empty()) {
          exclude.insert(browser->GetWindow()->GetNativeWindow());
        }
        return true;
      });
#endif
  base::WeakPtr<TabDragController> ref(weak_factory_.GetWeakPtr());
  *window = window_finder_->GetLocalProcessWindowAtPoint(screen_point, exclude);
  return ref ? Liveness::kAlive : Liveness::kDeleted;
}

bool TabDragController::CanAttachTo(gfx::NativeWindow window) {
  if (!window) {
    return false;
  }

  if (window == GetAttachedBrowserWidget()->GetNativeWindow()) {
    return true;
  }

#if BUILDFLAG(IS_CHROMEOS)
  // TODO(crbug.com/447246798): Don't allow dragging into an overview item as
  // the implementation is buggy. Triggering this appears to require drag by
  // touch, as drag by click causes the overview session to end immediately.
  if (window->GetProperty(chromeos::kIsShowingInOverviewKey)) {
    return false;
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  // Return false if `other_browser_view` is null or already closed. The latter
  // check is required since the widget may still alive on asynchronous
  // platforms such as Mac.
  BrowserView* other_browser_view =
      BrowserView::GetBrowserViewForNativeWindow(window);
  if (!other_browser_view || other_browser_view->GetWidget()->IsClosed()) {
    return false;
  }
  BrowserWindowInterface* other_browser = other_browser_view->browser();

  // Do not allow dragging into a window with a modal dialog, it causes a
  // weird behavior.  See crbug.com/40348569
#if defined(USE_AURA)
  if (wm::GetModalTransient(window)) {
    return false;
  }
#else
  TabStripModel* model = other_browser->GetTabStripModel();
  DCHECK(model);

  const int active_index = model->active_index();

#if BUILDFLAG(IS_MAC)
  // TODO(crbug.com/40890295): Remove DumpWithoutCrashing() if
  // Widget::IsClosed() check above works.
  if (!model->ContainsIndex(active_index)) {
    if (active_index == TabStripModel::kNoTab) {
      LOG(ERROR) << "TabStripModel of the browser tyring to attach to has no "
                    "active tab.";
    } else {
      LOG(ERROR)
          << "TabStripModel of the browser trying to attach to has invalid "
          << "active index: " << active_index;
    }

    // Avoid dumping too many times not to impact the performance as this may
    // be called multiple times for each mouse drag.
    static bool has_crash_reported = false;
    if (!has_crash_reported) {
      static crash_reporter::CrashKeyString<20> key("active_tab");
      key.Set(base::NumberToString(active_index));
      base::debug::DumpWithoutCrashing();
      has_crash_reported = true;
    }
    return false;
  }
#endif  // BUILDFLAG(IS_MAC)

  if (model->GetActiveTab()->IsBlocked()) {
    return false;
  }
#endif  // BUILDFLAG(USE_AURA)

  // We don't allow drops on windows that don't have tabstrips.
  if (!WindowFeatureController::From(other_browser)
           ->SupportsWindowFeature(
               WindowFeatureController::WindowFeature::kFeatureTabStrip)) {
    return false;
  }

  BrowserWindowInterface* browser =
      BrowserView::GetBrowserViewForNativeWindow(
          GetAttachedBrowserWidget()->GetNativeWindow())
          ->browser();

  // Profiles must be the same.
  if (other_browser->GetProfile() != browser->GetProfile()) {
    return false;
  }

  // Ensure that browser types and app names are the same.
  if (other_browser->GetType() != browser->GetType() ||
      (browser->GetType() == BrowserWindowInterface::Type::TYPE_APP &&
       BrowserInitState::From(browser)->create_params().app_name !=
           BrowserInitState::From(other_browser)->create_params().app_name)) {
    return false;
  }

  return true;
}

int TabDragController::GetOutOfBoundsYCoordinate() const {
  DCHECK(attached_context_);
  return attached_context_->GetBoundsInScreen().y() - kVerticalDetachMagnetism -
         1;
}

void TabDragController::NotifyEventIfTabAddedToGroup() {
  if (!source_context_ || source_context_->GetTabStripModel() == nullptr) {
    return;
  }

  const TabStripModel* source_model = source_context_->GetTabStripModel();
  for (const TabDragData& tab_drag_datum : drag_data_.tab_drag_data_) {
    // If the tab already had a group, skip it.
    if (tab_drag_datum.tab_group_data.has_value()) {
      continue;
    }

    // Get the tab group from the source model.
    std::optional<tab_groups::TabGroupId> group_id =
        source_model->GetTabGroupForTab(
            source_model->GetIndexOfWebContents(tab_drag_datum.contents));

    // If there was a tab group for that tab, then send the custom event for
    // adding a tab to a group.
    if (!group_id.has_value()) {
      continue;
    }

    if (source_context_->NotifyCustomEvent(kTabGroupedCustomEventId,
                                           tab_drag_datum.attached_view)) {
      break;
    }
  }
}

void TabDragController::MaybePauseTrackingSavedTabGroup() {
  if (!drag_data_.group_header_drag_data_.has_value()) {
    return;
  }

  BrowserWindowInterface* const browser =
      BrowserView::GetBrowserViewForNativeWindow(
          GetAttachedBrowserWidget()->GetNativeWindow())
          ->browser();

  tab_groups::TabGroupSyncService* tab_group_service =
      tab_groups::TabGroupSyncServiceFactory::GetForProfile(
          browser->GetProfile());

  if (!tab_group_service ||
      !tab_group_service->GetGroup(
          drag_data_.group_header_drag_data_.value().group)) {
    return;
  }

  observation_pauser_ = tab_group_service->CreateScopedLocalObserverPauser();
}

void TabDragController::MaybeResumeTrackingSavedTabGroup() {
  if (!drag_data_.group_header_drag_data_.has_value() || !observation_pauser_) {
    return;
  }

  BrowserWindowInterface* const browser =
      BrowserView::GetBrowserViewForNativeWindow(
          GetAttachedBrowserWidget()->GetNativeWindow())
          ->browser();

  tab_groups::TabGroupSyncService* tab_group_service =
      tab_groups::TabGroupSyncServiceFactory::GetForProfile(
          browser->GetProfile());

  if (!tab_group_service) {
    return;
  }

  observation_pauser_.reset();
}

void TabDragController::StartDraggingTabsSession(
    bool initial_move,
    gfx::Point start_point_in_screen) {
  CHECK(current_state_ == DragState::kDraggingTabs ||
        current_state_ == DragState::kWaitingToExitRunLoop);
  CHECK_EQ(dragging_tabs_session_, nullptr);
  CHECK(attached_context_);
  if (!attached_context_->GetPositioningDelegate()) {
    return;
  }

  dragging_tabs_session_ = std::make_unique<DraggingTabsSession>(
      drag_data_, *attached_context_,
      *attached_context_->GetPositioningDelegate(), initial_move,
      start_point_in_screen);
}

void TabDragController::UpdateSelectionModel(
    TabStripModel* tab_strip_model,
    ui::ListSelectionModel selection_model) {
  if (selection_model.active().has_value()) {
    std::optional<split_tabs::SplitTabId> split_id =
        tab_strip_model->GetSplitForTab(selection_model.active().value());
    if (split_id.has_value()) {
      selection_model.set_active(split_tabs::GetIndexOfLastActiveTab(
          tab_strip_model, split_id.value()));
    }
  }

  if (selection_model.anchor().has_value()) {
    std::optional<split_tabs::SplitTabId> split_id =
        tab_strip_model->GetSplitForTab(selection_model.anchor().value());
    if (split_id.has_value()) {
      selection_model.set_anchor(split_tabs::GetIndexOfLastActiveTab(
          tab_strip_model, split_id.value()));
    }
  }

  tab_strip_model->SetSelectionFromModel(selection_model);
}

void TabDragController::OnContextStartedDragging(
    const std::vector<TabSlotView*>& views) {
  CHECK(attached_context_);
  const bool dragging_window =
      views.size() ==
      static_cast<size_t>(attached_context_->GetTabStripModel()->count());
  BrowserView* attached_browser_view =
      GetBrowserViewForContext(attached_context_);
  CHECK(attached_browser_view);
  attached_browser_view->browser_widget()->SetTabDragKind(
      dragging_window ? TabDragKind::kAllTabs : TabDragKind::kTab);

  // Update the source browser as well to ensure fast-resizing is enabled
  // during dragging.
  BrowserView* source_browser_view = GetBrowserViewForContext(source_context_);
  if (source_browser_view && source_browser_view != attached_browser_view) {
    source_browser_view->browser_widget()->SetTabDragKind(TabDragKind::kTab);
  }

#if BUILDFLAG(IS_CHROMEOS)
  attached_browser_view->GetWidget()->GetNativeWindow()->SetProperty(
      ash::kIsDraggingTabsKey, true);
#endif  // BUILDFLAG(IS_CHROMEOS)
  attached_context_->StartedDragging(views);
}

void TabDragController::OnContextStoppedDragging() {
  UpdateBrowserViewsForDragEnd();
  attached_context_->StoppedDragging();
}

void TabDragController::OnContextDraggedTabsDetached() {
  UpdateBrowserViewsForDragEnd();
  attached_context_->DraggedTabsDetached();
}

void TabDragController::UpdateBrowserViewsForDragEnd() {
  CHECK(attached_context_);
  BrowserView* source_browser_view = GetBrowserViewForContext(source_context_);
  BrowserView* attached_browser_view =
      GetBrowserViewForContext(attached_context_);
  CHECK(attached_browser_view);
  // Only reset the source window's fast resize bit after the entire drag
  // ends.
  if (attached_browser_view != source_browser_view) {
    attached_browser_view->browser_widget()->SetTabDragKind(TabDragKind::kNone);
  }
  if (source_browser_view && !IsActive()) {
    source_browser_view->browser_widget()->SetTabDragKind(TabDragKind::kNone);
  }

#if BUILDFLAG(IS_CHROMEOS)
  // Clear the drag properties unless the drag browser's tabs got merged into
  // another browser, in which case SplitViewController::TabDragWindowObserver
  // still needs to read the properties. We detect this case by checking if the
  // tab strip model is now empty. Since it was non-empty originally and the
  // drag browser can't have any pending downloads. we know that it's about to
  // get destroyed anyways.
  if (!attached_context_->GetTabStripModel()->empty()) {
    attached_browser_view->GetWidget()->GetNativeWindow()->ClearProperty(
        ash::kIsDraggingTabsKey);
    attached_browser_view->GetWidget()->GetNativeWindow()->ClearProperty(
        ash::kTabDraggingSourceWindowKey);
  }
#endif  // BUILDFLAG(IS_CHROMEOS)
}

#if defined(USE_AURA)
void TabDragController::OnDragStarted() {
  VLOG(1) << __func__;
  if (drag_started_callback_) {
    std::move(drag_started_callback_).Run();
  }
}

void TabDragController::OnDragDropClientDestroying() {
  drag_drop_client_observation_.Reset();
}
#endif  // defined(USE_AURA)

void TabDragController::UpdateDragTarget(TabDragTarget* new_target,
                                         const gfx::Point& point_in_screen) {
  if (current_drag_target_ != new_target) {
    if (current_drag_target_) {
      current_drag_target_->OnTabDragExited(point_in_screen);
    }
    current_drag_target_ = new_target;
    if (current_drag_target_) {
      current_drag_target_->OnTabDragEntered();
      drag_target_destroyed_subscription_ =
          current_drag_target_->RegisterWillDestroyCallback(base::BindOnce(
              &TabDragController::ResetDragTarget, base::Unretained(this)));
    }
  }
}

void TabDragController::ResetDragTarget() {
  current_drag_target_ = nullptr;
}

// static
void TabDragController::SetTabDragPointResolver(
    TabDragPointResolver& resolver) {
  g_tab_drag_point_resolver_ = &resolver;
}

const char* TabDragController::GetTabStripMode() const {
  BrowserView* browser_view = GetBrowserViewForContext(attached_context_);
  CHECK(browser_view);

  return browser_view->ShouldDrawVerticalTabStrip() ? "VerticalTabStrip"
                                                    : "HorizontalTabStrip";
}
