Skip to content

Commit 2bcb73a

Browse files
committed
Add smooth tab reorder animation during drag
1 parent f8506f4 commit 2bcb73a

File tree

6 files changed

+429
-3
lines changed

6 files changed

+429
-3
lines changed

src/cascadia/TerminalApp/TabManagement.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,12 @@ namespace winrt::TerminalApp::implementation
10961096
void TerminalPage::_TabDragCompleted(const IInspectable& /*sender*/,
10971097
const IInspectable& /*eventArgs*/)
10981098
{
1099+
// Complete smooth reorder animation
1100+
if (_tabReorderAnimator)
1101+
{
1102+
_tabReorderAnimator->OnDragCompleted();
1103+
}
1104+
10991105
auto& from{ _rearrangeFrom };
11001106
auto& to{ _rearrangeTo };
11011107

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
#include "pch.h"
5+
#include "TabReorderAnimator.h"
6+
7+
using namespace winrt::Windows::Foundation;
8+
using namespace winrt::Windows::UI::Xaml;
9+
using namespace winrt::Windows::UI::Xaml::Media;
10+
using namespace winrt::Windows::UI::Xaml::Media::Animation;
11+
12+
namespace MUX = winrt::Microsoft::UI::Xaml::Controls;
13+
14+
static constexpr int AnimationDurationMs = 200;
15+
static constexpr double DefaultTabWidthFallback = 200.0;
16+
17+
namespace winrt::TerminalApp::implementation
18+
{
19+
TabReorderAnimator::TabReorderAnimator(const MUX::TabView& tabView, bool animationsEnabled) :
20+
_tabView{ tabView },
21+
_animationsEnabled{ animationsEnabled }
22+
{
23+
}
24+
25+
void TabReorderAnimator::SetAnimationsEnabled(bool enabled)
26+
{
27+
_animationsEnabled = enabled;
28+
}
29+
30+
void TabReorderAnimator::OnDragStarting(uint32_t draggedTabIndex)
31+
{
32+
_isDragging = true;
33+
_draggedTabIndex = static_cast<int>(draggedTabIndex);
34+
_currentGapIndex = _draggedTabIndex;
35+
36+
_EnsureTransformsSetup();
37+
_DisableBuiltInTransitions();
38+
}
39+
40+
void TabReorderAnimator::OnDragOver(const DragEventArgs& e)
41+
{
42+
if (!_isDragging && _draggedTabIndex < 0)
43+
{
44+
// Cross-window drag initialization
45+
_isDragging = true;
46+
_draggedTabIndex = -1;
47+
_currentGapIndex = -1;
48+
_EnsureTransformsSetup();
49+
_DisableBuiltInTransitions();
50+
}
51+
52+
const auto pos = e.GetPosition(_tabView);
53+
const auto newGapIndex = _CalculateGapIndex(pos.X);
54+
55+
if (newGapIndex != _currentGapIndex)
56+
{
57+
_AnimateTabsToMakeGap(newGapIndex);
58+
}
59+
}
60+
61+
void TabReorderAnimator::OnDragCompleted()
62+
{
63+
// Snap transforms back immediately (no animation) so we don't conflict
64+
// with TabView's built-in reorder animation
65+
_ResetAllTransforms(false);
66+
_RestoreBuiltInTransitions();
67+
68+
_isDragging = false;
69+
_draggedTabIndex = -1;
70+
_currentGapIndex = -1;
71+
_transforms.clear();
72+
}
73+
74+
void TabReorderAnimator::OnDragLeave()
75+
{
76+
_ResetAllTransforms(true);
77+
_RestoreBuiltInTransitions();
78+
79+
_isDragging = false;
80+
_draggedTabIndex = -1;
81+
_currentGapIndex = -1;
82+
_transforms.clear();
83+
}
84+
85+
void TabReorderAnimator::_EnsureTransformsSetup()
86+
{
87+
_StopAllAnimations();
88+
_transforms.clear();
89+
90+
const auto tabCount = _tabView.TabItems().Size();
91+
92+
for (uint32_t i = 0; i < tabCount; i++)
93+
{
94+
if (const auto item = _tabView.ContainerFromIndex(i).try_as<MUX::TabViewItem>())
95+
{
96+
auto transform = item.RenderTransform().try_as<TranslateTransform>();
97+
if (!transform)
98+
{
99+
transform = TranslateTransform{};
100+
item.RenderTransform(transform);
101+
}
102+
transform.X(0.0);
103+
_transforms.push_back(transform);
104+
}
105+
else
106+
{
107+
_transforms.push_back(nullptr);
108+
}
109+
}
110+
}
111+
112+
int TabReorderAnimator::_CalculateGapIndex(double pointerX) const
113+
{
114+
const auto tabCount = static_cast<int>(_tabView.TabItems().Size());
115+
116+
for (int i = 0; i < tabCount; i++)
117+
{
118+
if (i == _draggedTabIndex)
119+
{
120+
continue;
121+
}
122+
123+
if (const auto item = _tabView.ContainerFromIndex(i).try_as<MUX::TabViewItem>())
124+
{
125+
const auto itemTransform = item.TransformToVisual(_tabView);
126+
const auto itemPos = itemTransform.TransformPoint({ 0, 0 });
127+
const auto tabMidpoint = itemPos.X + (item.ActualWidth() / 2);
128+
129+
if (pointerX < tabMidpoint)
130+
{
131+
return i;
132+
}
133+
}
134+
}
135+
136+
return tabCount;
137+
}
138+
139+
double TabReorderAnimator::_GetTabWidth() const
140+
{
141+
const auto tabCount = _tabView.TabItems().Size();
142+
143+
for (uint32_t i = 0; i < tabCount; i++)
144+
{
145+
if (static_cast<int>(i) != _draggedTabIndex)
146+
{
147+
if (const auto item = _tabView.ContainerFromIndex(i).try_as<MUX::TabViewItem>())
148+
{
149+
return item.ActualWidth();
150+
}
151+
}
152+
}
153+
154+
if (_draggedTabIndex >= 0 && _draggedTabIndex < static_cast<int>(tabCount))
155+
{
156+
if (const auto item = _tabView.ContainerFromIndex(_draggedTabIndex).try_as<MUX::TabViewItem>())
157+
{
158+
return item.ActualWidth();
159+
}
160+
}
161+
162+
return DefaultTabWidthFallback;
163+
}
164+
165+
void TabReorderAnimator::_AnimateTabsToMakeGap(int gapIndex)
166+
{
167+
_currentGapIndex = gapIndex;
168+
const auto tabWidth = _GetTabWidth();
169+
const auto tabCount = static_cast<int>(_transforms.size());
170+
171+
_StopAllAnimations();
172+
173+
for (int i = 0; i < tabCount; i++)
174+
{
175+
if (i == _draggedTabIndex)
176+
{
177+
continue;
178+
}
179+
180+
auto& transform = _transforms[i];
181+
if (!transform)
182+
{
183+
continue;
184+
}
185+
186+
double targetOffset = 0.0;
187+
188+
// Only animate for same-window drags. Cross-window drags don't shift tabs
189+
// because there's no source gap to fill, and shifting right would push
190+
// tabs off-screen.
191+
if (_draggedTabIndex >= 0)
192+
{
193+
if (_draggedTabIndex < gapIndex)
194+
{
195+
if (i > _draggedTabIndex && i < gapIndex)
196+
{
197+
targetOffset = -tabWidth;
198+
}
199+
}
200+
else if (_draggedTabIndex > gapIndex)
201+
{
202+
if (i >= gapIndex && i < _draggedTabIndex)
203+
{
204+
targetOffset = tabWidth;
205+
}
206+
}
207+
}
208+
209+
_AnimateTransformTo(transform, targetOffset);
210+
}
211+
}
212+
213+
void TabReorderAnimator::_AnimateTransformTo(const TranslateTransform& transform, double targetX)
214+
{
215+
if (!transform)
216+
{
217+
return;
218+
}
219+
220+
if (!_animationsEnabled)
221+
{
222+
transform.X(targetX);
223+
return;
224+
}
225+
226+
if (std::abs(transform.X() - targetX) < 0.5)
227+
{
228+
transform.X(targetX);
229+
return;
230+
}
231+
232+
const auto duration = DurationHelper::FromTimeSpan(
233+
TimeSpan{ std::chrono::milliseconds(AnimationDurationMs) });
234+
235+
DoubleAnimation animation;
236+
animation.Duration(duration);
237+
animation.To(targetX);
238+
animation.EasingFunction(QuadraticEase{});
239+
animation.EnableDependentAnimation(true);
240+
241+
Storyboard storyboard;
242+
storyboard.Duration(duration);
243+
storyboard.Children().Append(animation);
244+
storyboard.SetTarget(animation, transform);
245+
storyboard.SetTargetProperty(animation, L"X");
246+
247+
_runningStoryboards.push_back(storyboard);
248+
storyboard.Begin();
249+
}
250+
251+
void TabReorderAnimator::_StopAllAnimations()
252+
{
253+
for (auto& storyboard : _runningStoryboards)
254+
{
255+
if (storyboard)
256+
{
257+
storyboard.Stop();
258+
}
259+
}
260+
_runningStoryboards.clear();
261+
}
262+
263+
void TabReorderAnimator::_ResetAllTransforms(bool animated)
264+
{
265+
_StopAllAnimations();
266+
267+
for (auto& transform : _transforms)
268+
{
269+
if (transform)
270+
{
271+
if (animated && _animationsEnabled)
272+
{
273+
_AnimateTransformTo(transform, 0.0);
274+
}
275+
else
276+
{
277+
transform.X(0.0);
278+
}
279+
}
280+
}
281+
}
282+
283+
void TabReorderAnimator::_DisableBuiltInTransitions()
284+
{
285+
try
286+
{
287+
const auto childCount = VisualTreeHelper::GetChildrenCount(_tabView);
288+
for (int32_t i = 0; i < childCount; i++)
289+
{
290+
if (const auto listView = VisualTreeHelper::GetChild(_tabView, i).try_as<Controls::ListView>())
291+
{
292+
if (!_transitionsSaved)
293+
{
294+
_savedTransitions = listView.ItemContainerTransitions();
295+
_transitionsSaved = true;
296+
}
297+
listView.ItemContainerTransitions(nullptr);
298+
break;
299+
}
300+
}
301+
}
302+
catch (...)
303+
{
304+
// Do nothing on failure - visual tree structure may vary
305+
}
306+
}
307+
308+
void TabReorderAnimator::_RestoreBuiltInTransitions()
309+
{
310+
if (!_transitionsSaved)
311+
{
312+
return;
313+
}
314+
315+
try
316+
{
317+
const auto childCount = VisualTreeHelper::GetChildrenCount(_tabView);
318+
for (int32_t i = 0; i < childCount; i++)
319+
{
320+
if (const auto listView = VisualTreeHelper::GetChild(_tabView, i).try_as<Controls::ListView>())
321+
{
322+
listView.ItemContainerTransitions(_savedTransitions);
323+
break;
324+
}
325+
}
326+
}
327+
catch (...)
328+
{
329+
// Do nothing on failure - visual tree structure may vary
330+
}
331+
332+
_savedTransitions = nullptr;
333+
_transitionsSaved = false;
334+
}
335+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
#pragma once
5+
6+
namespace winrt::TerminalApp::implementation
7+
{
8+
class TabReorderAnimator
9+
{
10+
public:
11+
TabReorderAnimator(const Microsoft::UI::Xaml::Controls::TabView& tabView, bool animationsEnabled);
12+
13+
void OnDragStarting(uint32_t draggedTabIndex);
14+
void OnDragOver(const Windows::UI::Xaml::DragEventArgs& e);
15+
void OnDragCompleted();
16+
void OnDragLeave();
17+
18+
void SetAnimationsEnabled(bool enabled);
19+
20+
private:
21+
void _AnimateTabsToMakeGap(int gapIndex);
22+
void _ResetAllTransforms(bool animated);
23+
int _CalculateGapIndex(double pointerX) const;
24+
void _EnsureTransformsSetup();
25+
double _GetTabWidth() const;
26+
void _AnimateTransformTo(const Windows::UI::Xaml::Media::TranslateTransform& transform, double targetX);
27+
void _StopAllAnimations();
28+
void _DisableBuiltInTransitions();
29+
void _RestoreBuiltInTransitions();
30+
31+
Microsoft::UI::Xaml::Controls::TabView _tabView{ nullptr };
32+
int _draggedTabIndex{ -1 };
33+
int _currentGapIndex{ -1 };
34+
std::vector<Windows::UI::Xaml::Media::TranslateTransform> _transforms;
35+
std::vector<Windows::UI::Xaml::Media::Animation::Storyboard> _runningStoryboards;
36+
bool _animationsEnabled{ true };
37+
bool _isDragging{ false };
38+
39+
Windows::UI::Xaml::Media::Animation::TransitionCollection _savedTransitions{ nullptr };
40+
bool _transitionsSaved{ false };
41+
};
42+
}

0 commit comments

Comments
 (0)