Skip to content

Commit

Permalink
Add an animation to pane entrance/exit (#7364)
Browse files Browse the repository at this point in the history
Adds an entrance animation when panes are created. This animation can be
disabled with the `disableAnimations` global setting. 

Although the XAML animation documentation was pretty heavy on the _do it
in XAML_ route, our panes are created pretty much entirely in code, so
we've got to create the animations in code as well. 

200ms as the duration of the animation was picked _super_ arbitrarily.
300ms felt too long, and 166ms felt like it was only visible for a
single frame. 

see also:
* [Motion in practice](https://docs.microsoft.com/en-us/windows/uwp/design/motion/motion-in-practice)
* [This example](https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Media.Animation.Storyboard?view=winrt-19041#examples) what what I ended up using, albeit ported to cppwinrt.
* [`Timeline.AllowDependentAnimations`](https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.animation.timeline.allowdependentanimations?view=winrt-19041#Windows_UI_Xaml_Media_Animation_Timeline_AllowDependentAnimations)
* [easing functions](https://docs.microsoft.com/en-us/windows/uwp/design/motion/key-frame-and-easing-function-animations#easing-functions)

## Validation Steps Performed
Man have I been opening panes

Closes #1001
Closes #7366
  • Loading branch information
zadjii-msft authored Oct 9, 2020
1 parent 9e86e29 commit 9dc38ad
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 2 deletions.
5 changes: 5 additions & 0 deletions doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,11 @@
"description": "When set to `true`, the color and font formatting of selected text is also copied to your clipboard. When set to `false`, only plain text is copied to your clipboard. An array of specific formats can also be used. Supported array values include `html` and `rtf`. Plain text is always copied.",
"$ref": "#/definitions/CopyFormat"
},
"disableAnimations": {
"default": false,
"description": "When set to `true`, visual animations will be disabled across the application.",
"type": "boolean"
},
"largePasteWarning": {
"default": true,
"description": "When set to true, trying to paste text with more than 5 KiB of characters will display a warning asking you whether to continue or not with the paste.",
Expand Down
275 changes: 273 additions & 2 deletions src/cascadia/TerminalApp/Pane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ static const int PaneBorderSize = 2;
static const int CombinedPaneBorderSize = 2 * PaneBorderSize;
static const float Half = 0.50f;

// WARNING: Don't do this! This won't work
// Duration duration{ std::chrono::milliseconds{ 200 } };
// Instead, make a duration from a TimeSpan from the time in millis
//
// 200ms was chosen because it's quick enough that it doesn't break your
// flow, but not too quick to see
static const int AnimationDurationInMilliseconds = 200;
static const Duration AnimationDuration = DurationHelper::FromTimeSpan(winrt::Windows::Foundation::TimeSpan(std::chrono::milliseconds(AnimationDurationInMilliseconds)));

winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_focusedBorderBrush = { nullptr };
winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_unfocusedBorderBrush = { nullptr };

Expand All @@ -42,6 +51,10 @@ Pane::Pane(const GUID& profile, const TermControl& control, const bool lastFocus
_SetupResources();
}

// Use the unfocused border color as the pane background, so an actual color
// appears behind panes as we animate them sliding in.
_root.Background(s_unfocusedBorderBrush);

// Register an event with the control to have it inform us when it gains focus.
_gotFocusRevoker = control.GotFocus(winrt::auto_revoke, { this, &Pane::_ControlGotFocusHandler });

Expand Down Expand Up @@ -327,7 +340,7 @@ void Pane::_ControlConnectionStateChangedHandler(const TermControl& /*sender*/,
if ((mode == CloseOnExitMode::Always) ||
(mode == CloseOnExitMode::Graceful && newConnectionState == ConnectionState::Closed))
{
_ClosedHandlers(nullptr, nullptr);
Close();
}
}
}
Expand Down Expand Up @@ -761,7 +774,129 @@ winrt::fire_and_forget Pane::_CloseChildRoutine(const bool closeFirst)

if (auto pane{ weakThis.get() })
{
_CloseChild(closeFirst);
// This will query if animations are enabled via the "Show animations in
// Windows" setting in the OS
winrt::Windows::UI::ViewManagement::UISettings uiSettings;
const auto animationsEnabledInOS = uiSettings.AnimationsEnabled();
const auto animationsEnabledInApp = Media::Animation::Timeline::AllowDependentAnimations();

// If animations are disabled, just skip this and go straight to
// _CloseChild. Curiously, the pane opening animation doesn't need this,
// and will skip straight to Completed when animations are disabled, but
// this one doesn't seem to.
if (!animationsEnabledInOS || !animationsEnabledInApp)
{
pane->_CloseChild(closeFirst);
co_return;
}

// Setup the animation

auto removedChild = closeFirst ? _firstChild : _secondChild;
auto remainingChild = closeFirst ? _secondChild : _firstChild;
const bool splitWidth = _splitState == SplitState::Vertical;
const auto totalSize = splitWidth ? _root.ActualWidth() : _root.ActualHeight();

Size removedOriginalSize{
::base::saturated_cast<float>(removedChild->_root.ActualWidth()),
::base::saturated_cast<float>(removedChild->_root.ActualHeight())
};
Size remainingOriginalSize{
::base::saturated_cast<float>(remainingChild->_root.ActualWidth()),
::base::saturated_cast<float>(remainingChild->_root.ActualHeight())
};

// Remove both children from the grid
_root.Children().Clear();
// Add the remaining child back to the grid, in the right place.
_root.Children().Append(remainingChild->GetRootElement());
if (_splitState == SplitState::Vertical)
{
Controls::Grid::SetColumn(remainingChild->GetRootElement(), closeFirst ? 1 : 0);
}
else if (_splitState == SplitState::Horizontal)
{
Controls::Grid::SetRow(remainingChild->GetRootElement(), closeFirst ? 1 : 0);
}

// Create the dummy grid. This grid will be the one we actually animate,
// in the place of the closed pane.
Controls::Grid dummyGrid;
dummyGrid.Background(s_unfocusedBorderBrush);
// It should be the size of the closed pane.
dummyGrid.Width(removedOriginalSize.Width);
dummyGrid.Height(removedOriginalSize.Height);
// Put it where the removed child is
if (_splitState == SplitState::Vertical)
{
Controls::Grid::SetColumn(dummyGrid, closeFirst ? 0 : 1);
}
else if (_splitState == SplitState::Horizontal)
{
Controls::Grid::SetRow(dummyGrid, closeFirst ? 0 : 1);
}
// Add it to the tree
_root.Children().Append(dummyGrid);

// Set up the rows/cols as auto/auto, so they'll only use the size of
// the elements in the grid.
//
// * For the closed pane, we want to make that row/col "auto" sized, so
// it takes up as much space as is available.
// * For the remaining pane, we'll make that row/col "*" sized, so it
// takes all the remaining space. As the dummy grid is resized down,
// the remaining pane will expand to take the rest of the space.
_root.ColumnDefinitions().Clear();
_root.RowDefinitions().Clear();
if (_splitState == SplitState::Vertical)
{
auto firstColDef = Controls::ColumnDefinition();
auto secondColDef = Controls::ColumnDefinition();
firstColDef.Width(!closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto());
secondColDef.Width(closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto());
_root.ColumnDefinitions().Append(firstColDef);
_root.ColumnDefinitions().Append(secondColDef);
}
else if (_splitState == SplitState::Horizontal)
{
auto firstRowDef = Controls::RowDefinition();
auto secondRowDef = Controls::RowDefinition();
firstRowDef.Height(!closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto());
secondRowDef.Height(closeFirst ? GridLengthHelper::FromValueAndType(1, GridUnitType::Star) : GridLengthHelper::Auto());
_root.RowDefinitions().Append(firstRowDef);
_root.RowDefinitions().Append(secondRowDef);
}

// Animate the dummy grid from its current size down to 0
Media::Animation::DoubleAnimation animation{};
animation.Duration(AnimationDuration);
animation.From(splitWidth ? removedOriginalSize.Width : removedOriginalSize.Height);
animation.To(0.0);
// This easing is the same as the entrance animation.
animation.EasingFunction(Media::Animation::QuadraticEase{});
animation.EnableDependentAnimation(true);

Media::Animation::Storyboard s;
s.Duration(AnimationDuration);
s.Children().Append(animation);
s.SetTarget(animation, dummyGrid);
s.SetTargetProperty(animation, splitWidth ? L"Width" : L"Height");

// Start the animation.
s.Begin();

std::weak_ptr<Pane> weakThis{ shared_from_this() };

// When the animation is completed, reparent the child's content up to
// us, and remove the child nodes from the tree.
animation.Completed([weakThis, closeFirst](auto&&, auto&&) {
if (auto pane{ weakThis.lock() })
{
// We don't need to manually undo any of the above trickiness.
// We're going to re-parent the child's content into us anyways
pane->_CloseChild(closeFirst);
}
});
}
}

Expand Down Expand Up @@ -903,6 +1038,140 @@ void Pane::_ApplySplitDefinitions()
}
}

// Method Description:
// - Create a pair of animations when a new control enters this pane. This
// should _ONLY_ be called in _Split, AFTER the first and second child panes
// have been set up.
void Pane::_SetupEntranceAnimation()
{
// This will query if animations are enabled via the "Show animations in
// Windows" setting in the OS
winrt::Windows::UI::ViewManagement::UISettings uiSettings;
const auto animationsEnabledInOS = uiSettings.AnimationsEnabled();

const bool splitWidth = _splitState == SplitState::Vertical;
const auto totalSize = splitWidth ? _root.ActualWidth() : _root.ActualHeight();
// If we don't have a size yet, it's likely that we're in startup, or we're
// being executed as a sequence of actions. In that case, just skip the
// animation.
if (totalSize <= 0 || !animationsEnabledInOS)
{
return;
}

const auto [firstSize, secondSize] = _CalcChildrenSizes(::base::saturated_cast<float>(totalSize));

// This is safe to capture this, because it's only being called in the
// context of this method (not on another thread)
auto setupAnimation = [&](const auto& size, const bool isFirstChild) {
auto child = isFirstChild ? _firstChild : _secondChild;
auto childGrid = child->_root;
auto control = child->_control;
// Build up our animation:
// * it'll take as long as our duration (200ms)
// * it'll change the value of our property from 0 to secondSize
// * it'll animate that value using a quadratic function (like f(t) = t^2)
// * IMPORTANT! We'll manually tell the animation that "yes we know what
// we're doing, we want an animation here."
Media::Animation::DoubleAnimation animation{};
animation.Duration(AnimationDuration);
if (isFirstChild)
{
// If we're animating the first pane, the size should decrease, from
// the full size down to the given size.
animation.From(totalSize);
animation.To(size);
}
else
{
// Otherwise, we want to show the pane getting larger, so animate
// from 0 to the requested size.
animation.From(0.0);
animation.To(size);
}
animation.EasingFunction(Media::Animation::QuadraticEase{});
animation.EnableDependentAnimation(true);

// Now we're going to set up the Storyboard. This is a unit that uses the
// Animation from above, and actually applies it to a property.
// * we'll set it up for the same duration as the animation we have
// * Apply the animation to the grid of the new pane we're adding to the tree.
// * apply the animation to the Width or Height property.
Media::Animation::Storyboard s;
s.Duration(AnimationDuration);
s.Children().Append(animation);
s.SetTarget(animation, childGrid);
s.SetTargetProperty(animation, splitWidth ? L"Width" : L"Height");

// BE TRICKY:
// We're animating the width or height of our child pane's grid.
//
// We DON'T want to change the size of the control itself, because the
// terminal has to reflow the buffer every time the control changes size. So
// what we're going to do there is manually set the control's size to how
// big we _actually know_ the control will be.
//
// We're also going to be changing alignment of our child pane and the
// control. This way, we'll be able to have the control stick to the inside
// of the child pane's grid (the side that's moving), while we also have the
// pane's grid stick to "outside" of the grid (the side that's not moving)
if (splitWidth)
{
// If we're animating the first child, then stick to the top/left of
// the parent pane, otherwise use the bottom/right. This is always
// the "outside" of the parent pane.
childGrid.HorizontalAlignment(isFirstChild ? HorizontalAlignment::Left : HorizontalAlignment::Right);
control.HorizontalAlignment(HorizontalAlignment::Left);
control.Width(isFirstChild ? totalSize : size);
}
else
{
// If we're animating the first child, then stick to the top/left of
// the parent pane, otherwise use the bottom/right. This is always
// the "outside" of the parent pane.
childGrid.VerticalAlignment(isFirstChild ? VerticalAlignment::Top : VerticalAlignment::Bottom);
control.VerticalAlignment(VerticalAlignment::Top);
control.Height(isFirstChild ? totalSize : size);
}

// Start the animation.
s.Begin();

std::weak_ptr<Pane> weakThis{ shared_from_this() };
// When the animation is completed, undo the trickiness from before, to
// restore the controls to the behavior they'd usually have.
animation.Completed([weakThis, isFirstChild, splitWidth](auto&&, auto&&) {
if (auto pane{ weakThis.lock() })
{
auto child = isFirstChild ? pane->_firstChild : pane->_secondChild;
auto childGrid = child->_root;
if (auto control = child->_control)
{
if (splitWidth)
{
control.Width(NAN);
childGrid.Width(NAN);
childGrid.HorizontalAlignment(HorizontalAlignment::Stretch);
control.HorizontalAlignment(HorizontalAlignment::Stretch);
}
else
{
control.Height(NAN);
childGrid.Height(NAN);
childGrid.VerticalAlignment(VerticalAlignment::Stretch);
control.VerticalAlignment(VerticalAlignment::Stretch);
}
}
}
});
};

// TODO: GH#7365 - animating the first child right now doesn't _really_ do
// anything. We could do better though.
setupAnimation(firstSize, true);
setupAnimation(secondSize, false);
}

// Method Description:
// - Determines whether the pane can be split
// Arguments:
Expand Down Expand Up @@ -1177,6 +1446,8 @@ std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitState

_lastActive = false;

_SetupEntranceAnimation();

return { _firstChild, _secondChild };
}

Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/Pane.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class Pane : public std::enable_shared_from_this<Pane>

void _CreateRowColDefinitions();
void _ApplySplitDefinitions();
void _SetupEntranceAnimation();
void _UpdateBorders();

bool _Resize(const winrt::Microsoft::Terminal::Settings::Model::Direction& direction);
Expand Down
10 changes: 10 additions & 0 deletions src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ namespace winrt::TerminalApp::implementation
}
});

// Settings AllowDependentAnimations will affect whether animations are
// enabled application-wide, so we don't need to check it each time we
// want to create an animation.
WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations());

// Once the page is actually laid out on the screen, trigger all our
// startup actions. Things like Panes need to know at least how big the
// window will be, so they can subdivide that space.
Expand Down Expand Up @@ -2087,6 +2092,11 @@ namespace winrt::TerminalApp::implementation
// the alwaysOnTop setting will be lost.
_isAlwaysOnTop = _settings.GlobalSettings().AlwaysOnTop();
_alwaysOnTopChangedHandlers(*this, nullptr);

// Settings AllowDependentAnimations will affect whether animations are
// enabled application-wide, so we don't need to check it each time we
// want to create an animation.
WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations());
}

// This is a helper to aid in sorting commands by their `Name`s, alphabetically.
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalApp/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@
#include <winrt/Windows.UI.Xaml.Controls.Primitives.h>
#include <winrt/Windows.UI.Xaml.Data.h>
#include <winrt/Windows.ui.xaml.media.h>
#include <winrt/Windows.UI.Xaml.Media.Animation.h>
#include <winrt/Windows.ui.xaml.input.h>
#include <winrt/Windows.UI.Xaml.Hosting.h>
#include "winrt/Windows.UI.Xaml.Markup.h"
#include "winrt/Windows.UI.Xaml.Documents.h"
#include "winrt/Windows.UI.Xaml.Automation.h"
#include "winrt/Windows.UI.ViewManagement.h"
#include <winrt/Windows.ApplicationModel.h>
#include <winrt/Windows.ApplicationModel.DataTransfer.h>

Expand Down
3 changes: 3 additions & 0 deletions src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ static constexpr std::string_view SnapToGridOnResizeKey{ "snapToGridOnResize" };
static constexpr std::string_view EnableStartupTaskKey{ "startOnUserLogin" };
static constexpr std::string_view AlwaysOnTopKey{ "alwaysOnTop" };
static constexpr std::string_view UseTabSwitcherKey{ "useTabSwitcher" };
static constexpr std::string_view DisableAnimationsKey{ "disableAnimations" };

static constexpr std::string_view DebugFeaturesKey{ "debugFeatures" };

Expand Down Expand Up @@ -153,6 +154,8 @@ void GlobalAppSettings::LayerJson(const Json::Value& json)

JsonUtils::GetValueForKey(json, UseTabSwitcherKey, _UseTabSwitcher);

JsonUtils::GetValueForKey(json, DisableAnimationsKey, _DisableAnimations);

// This is a helper lambda to get the keybindings and commands out of both
// and array of objects. We'll use this twice, once on the legacy
// `keybindings` key, and again on the newer `bindings` key.
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalSettingsModel/GlobalAppSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
GETSET_PROPERTY(bool, StartOnUserLogin, false);
GETSET_PROPERTY(bool, AlwaysOnTop, false);
GETSET_PROPERTY(bool, UseTabSwitcher, true);
GETSET_PROPERTY(bool, DisableAnimations, false);

private:
hstring _unparsedDefaultProfile;
Expand Down
Loading

0 comments on commit 9dc38ad

Please sign in to comment.