Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/cascadia/TerminalApp/TabManagement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,12 @@ namespace winrt::TerminalApp::implementation
void TerminalPage::_TabDragCompleted(const IInspectable& /*sender*/,
const IInspectable& /*eventArgs*/)
{
// Complete smooth reorder animation
if (_tabReorderAnimator)
{
_tabReorderAnimator->OnDragCompleted();
}

auto& from{ _rearrangeFrom };
auto& to{ _rearrangeTo };

Expand Down
335 changes: 335 additions & 0 deletions src/cascadia/TerminalApp/TabReorderAnimator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "pch.h"
#include "TabReorderAnimator.h"

using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Media;
using namespace winrt::Windows::UI::Xaml::Media::Animation;

namespace MUX = winrt::Microsoft::UI::Xaml::Controls;

static constexpr int AnimationDurationMs = 200;
static constexpr double DefaultTabWidthFallback = 200.0;

namespace winrt::TerminalApp::implementation
{
TabReorderAnimator::TabReorderAnimator(const MUX::TabView& tabView, bool animationsEnabled) :
_tabView{ tabView },
_animationsEnabled{ animationsEnabled }
{
}

void TabReorderAnimator::SetAnimationsEnabled(bool enabled)
{
_animationsEnabled = enabled;
}

void TabReorderAnimator::OnDragStarting(uint32_t draggedTabIndex)
{
_isDragging = true;
_draggedTabIndex = static_cast<int>(draggedTabIndex);
_currentGapIndex = _draggedTabIndex;

_EnsureTransformsSetup();
_DisableBuiltInTransitions();
}

void TabReorderAnimator::OnDragOver(const DragEventArgs& e)
{
if (!_isDragging && _draggedTabIndex < 0)
{
// Cross-window drag initialization
_isDragging = true;
_draggedTabIndex = -1;
_currentGapIndex = -1;
_EnsureTransformsSetup();
_DisableBuiltInTransitions();
}

const auto pos = e.GetPosition(_tabView);
const auto newGapIndex = _CalculateGapIndex(pos.X);

if (newGapIndex != _currentGapIndex)
{
_AnimateTabsToMakeGap(newGapIndex);
}
}

void TabReorderAnimator::OnDragCompleted()
{
// Snap transforms back immediately (no animation) so we don't conflict
// with TabView's built-in reorder animation
_ResetAllTransforms(false);
_RestoreBuiltInTransitions();

_isDragging = false;
_draggedTabIndex = -1;
_currentGapIndex = -1;
_transforms.clear();
}

void TabReorderAnimator::OnDragLeave()
{
_ResetAllTransforms(true);
_RestoreBuiltInTransitions();

_isDragging = false;
_draggedTabIndex = -1;
_currentGapIndex = -1;
_transforms.clear();
}

void TabReorderAnimator::_EnsureTransformsSetup()
{
_StopAllAnimations();
_transforms.clear();

const auto tabCount = _tabView.TabItems().Size();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const auto tabCount = _tabView.TabItems().Size();
const auto tabCount = _tabView.TabItems().Size();
_transforms.reserve(tabCount);

nit: set the size of _transforms to tabCount since it'll always be that.


for (uint32_t i = 0; i < tabCount; i++)
{
if (const auto item = _tabView.ContainerFromIndex(i).try_as<MUX::TabViewItem>())
{
auto transform = item.RenderTransform().try_as<TranslateTransform>();
if (!transform)
{
transform = TranslateTransform{};
item.RenderTransform(transform);
}
transform.X(0.0);
_transforms.push_back(transform);
}
else
{
_transforms.push_back(nullptr);
}
}
}

int TabReorderAnimator::_CalculateGapIndex(double pointerX) const
{
const auto tabCount = static_cast<int>(_tabView.TabItems().Size());

for (int i = 0; i < tabCount; i++)
{
if (i == _draggedTabIndex)
{
continue;
}

if (const auto item = _tabView.ContainerFromIndex(i).try_as<MUX::TabViewItem>())
{
const auto itemTransform = item.TransformToVisual(_tabView);
const auto itemPos = itemTransform.TransformPoint({ 0, 0 });
const auto tabMidpoint = itemPos.X + (item.ActualWidth() / 2);

if (pointerX < tabMidpoint)
{
return i;
}
}
}

return tabCount;
}

double TabReorderAnimator::_GetTabWidth() const
{
const auto tabCount = _tabView.TabItems().Size();

for (uint32_t i = 0; i < tabCount; i++)
{
if (static_cast<int>(i) != _draggedTabIndex)
{
if (const auto item = _tabView.ContainerFromIndex(i).try_as<MUX::TabViewItem>())
{
return item.ActualWidth();
}
}
}

if (_draggedTabIndex >= 0 && _draggedTabIndex < static_cast<int>(tabCount))
{
if (const auto item = _tabView.ContainerFromIndex(_draggedTabIndex).try_as<MUX::TabViewItem>())
{
return item.ActualWidth();
}
}
Comment on lines +143 to +160
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need both? Why not just the second block?


return DefaultTabWidthFallback;
}

void TabReorderAnimator::_AnimateTabsToMakeGap(int gapIndex)
{
_currentGapIndex = gapIndex;
const auto tabWidth = _GetTabWidth();
const auto tabCount = static_cast<int>(_transforms.size());

_StopAllAnimations();

for (int i = 0; i < tabCount; i++)
{
if (i == _draggedTabIndex)
{
continue;
}

auto& transform = _transforms[i];
if (!transform)
{
continue;
}

double targetOffset = 0.0;

// Only animate for same-window drags. Cross-window drags don't shift tabs
// because there's no source gap to fill, and shifting right would push
// tabs off-screen.
if (_draggedTabIndex >= 0)
{
if (_draggedTabIndex < gapIndex)
{
if (i > _draggedTabIndex && i < gapIndex)
{
targetOffset = -tabWidth;
}
}
else if (_draggedTabIndex > gapIndex)
{
if (i >= gapIndex && i < _draggedTabIndex)
{
targetOffset = tabWidth;
}
}
}

_AnimateTransformTo(transform, targetOffset);
}
}

void TabReorderAnimator::_AnimateTransformTo(const TranslateTransform& transform, double targetX)
{
if (!transform)
{
return;
}

if (!_animationsEnabled)
{
transform.X(targetX);
return;
}

if (std::abs(transform.X() - targetX) < 0.5)
{
transform.X(targetX);
return;
}

const auto duration = DurationHelper::FromTimeSpan(
TimeSpan{ std::chrono::milliseconds(AnimationDurationMs) });
Comment on lines +232 to +233
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const auto duration = DurationHelper::FromTimeSpan(
TimeSpan{ std::chrono::milliseconds(AnimationDurationMs) });
const auto duration = DurationHelper::FromTimeSpan( TimeSpan{ std::chrono::milliseconds(AnimationDurationMs) });

nit: no need to split into 2 lines


DoubleAnimation animation;
animation.Duration(duration);
animation.To(targetX);
animation.EasingFunction(QuadraticEase{});
animation.EnableDependentAnimation(true);

Storyboard sb;
sb.Duration(duration);
sb.Children().Append(animation);
sb.SetTarget(animation, transform);
sb.SetTargetProperty(animation, L"X");

_activeAnimations.push_back(sb);
sb.Begin();
}

void TabReorderAnimator::_StopAllAnimations()
{
for (auto& anim : _activeAnimations)
{
if (anim)
{
anim.Stop();
}
}
_activeAnimations.clear();
}

void TabReorderAnimator::_ResetAllTransforms(bool animated)
{
_StopAllAnimations();

for (auto& transform : _transforms)
{
if (transform)
{
if (animated && _animationsEnabled)
{
_AnimateTransformTo(transform, 0.0);
}
else
{
transform.X(0.0);
}
}
}
}

void TabReorderAnimator::_DisableBuiltInTransitions()
{
try
{
const auto childCount = VisualTreeHelper::GetChildrenCount(_tabView);
for (int32_t i = 0; i < childCount; i++)
{
if (const auto listView = VisualTreeHelper::GetChild(_tabView, i).try_as<Controls::ListView>())
{
if (!_transitionsSaved)
{
_savedTransitions = listView.ItemContainerTransitions();
_transitionsSaved = true;
}
listView.ItemContainerTransitions(nullptr);
break;
}
}
}
catch (...)
{
// Do nothing on failure - visual tree structure may vary
}
}

void TabReorderAnimator::_RestoreBuiltInTransitions()
{
if (!_transitionsSaved)
{
return;
}

try
{
const auto childCount = VisualTreeHelper::GetChildrenCount(_tabView);
for (int32_t i = 0; i < childCount; i++)
{
if (const auto listView = VisualTreeHelper::GetChild(_tabView, i).try_as<Controls::ListView>())
{
listView.ItemContainerTransitions(_savedTransitions);
break;
}
}
}
catch (...)
{
// Do nothing on failure - visual tree structure may vary
}

_savedTransitions = nullptr;
_transitionsSaved = false;
}
}
42 changes: 42 additions & 0 deletions src/cascadia/TerminalApp/TabReorderAnimator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#pragma once

namespace winrt::TerminalApp::implementation
{
class TabReorderAnimator
{
public:
TabReorderAnimator(const Microsoft::UI::Xaml::Controls::TabView& tabView, bool animationsEnabled);

void OnDragStarting(uint32_t draggedTabIndex);
void OnDragOver(const Windows::UI::Xaml::DragEventArgs& e);
void OnDragCompleted();
void OnDragLeave();

void SetAnimationsEnabled(bool enabled);

private:
void _AnimateTabsToMakeGap(int gapIndex);
void _ResetAllTransforms(bool animated);
int _CalculateGapIndex(double pointerX) const;
void _EnsureTransformsSetup();
double _GetTabWidth() const;
void _AnimateTransformTo(const Windows::UI::Xaml::Media::TranslateTransform& transform, double targetX);
void _StopAllAnimations();
void _DisableBuiltInTransitions();
void _RestoreBuiltInTransitions();

Microsoft::UI::Xaml::Controls::TabView _tabView{ nullptr };
int _draggedTabIndex{ -1 };
int _currentGapIndex{ -1 };
std::vector<Windows::UI::Xaml::Media::TranslateTransform> _transforms;
std::vector<Windows::UI::Xaml::Media::Animation::Storyboard> _activeAnimations;
bool _animationsEnabled{ true };
bool _isDragging{ false };

Windows::UI::Xaml::Media::Animation::TransitionCollection _savedTransitions{ nullptr };
bool _transitionsSaved{ false };
Comment on lines +39 to +40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need _transitionsSaved? Can't we just check if _savedTransitions is null?

};
}
Loading
Loading