Skip to content

Commit

Permalink
Add SearchBox entry/exit animations (#16808)
Browse files Browse the repository at this point in the history
Adds animations at the Entry and Exit of the Search Box.


https://github.com/microsoft/terminal/assets/55626797/14773bb7-89d8-4dc4-9aa4-1600139e97ae

Inspired by WinUI
[CommandBarFlyout](https://github.com/microsoft/microsoft-ui-xaml/tree/v2.8.6/dev/CommandBarFlyout)

## Validation Steps Performed
- Animation feels good ✅
- Works with multiple panes open.
- Pane opening/closing doesn't cause re-animations.
  • Loading branch information
tusharsnx authored Mar 18, 2024
1 parent 287422b commit 07a6f6a
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 7 deletions.
166 changes: 166 additions & 0 deletions src/cascadia/TerminalControl/SearchBoxControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
InitializeComponent();

_initialLoadedRevoker = Loaded(winrt::auto_revoke, [weakThis{ get_weak() }](auto&&, auto&&) {
if (auto searchbox{ weakThis.get() })
{
searchbox->_Initialize();
searchbox->_initialLoadedRevoker.revoke();
}
});
this->CharacterReceived({ this, &SearchBoxControl::_CharacterHandler });
this->KeyDown({ this, &SearchBoxControl::_KeyDownHandler });
this->RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) {
Expand All @@ -41,6 +48,165 @@ namespace winrt::Microsoft::Terminal::Control::implementation
StatusBox().Width(_GetStatusMaxWidth());
}

winrt::Windows::Foundation::Rect SearchBoxControl::ContentClipRect() const noexcept
{
return _contentClipRect;
}

void SearchBoxControl::_ContentClipRect(const winrt::Windows::Foundation::Rect& rect)
{
if (rect != _contentClipRect)
{
_contentClipRect = rect;
_PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"ContentClipRect" });
}
}

double SearchBoxControl::OpenAnimationStartPoint() const noexcept
{
return _openAnimationStartPoint;
}

void SearchBoxControl::_OpenAnimationStartPoint(double y)
{
if (y != _openAnimationStartPoint)
{
_openAnimationStartPoint = y;
_PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"OpenAnimationStartPoint" });
}
}

void SearchBoxControl::_UpdateSizeDependents()
{
const winrt::Windows::Foundation::Size infiniteSize{ std::numeric_limits<float>::infinity(), std::numeric_limits<float>::infinity() };
Measure(infiniteSize);
const auto desiredSize = DesiredSize();
_OpenAnimationStartPoint(-desiredSize.Height);
_ContentClipRect({ 0, 0, desiredSize.Width, desiredSize.Height });
}

void SearchBoxControl::_PlayOpenAnimation()
{
if (CloseAnimation().GetCurrentState() == Media::Animation::ClockState::Active)
{
CloseAnimation().Stop();
}

if (OpenAnimation().GetCurrentState() != Media::Animation::ClockState::Active)
{
OpenAnimation().Begin();
}
}

void SearchBoxControl::_PlayCloseAnimation()
{
if (OpenAnimation().GetCurrentState() == Media::Animation::ClockState::Active)
{
OpenAnimation().Stop();
}

if (CloseAnimation().GetCurrentState() != Media::Animation::ClockState::Active)
{
CloseAnimation().Begin();
}
}

// Method Description:
// - Sets the search box control to its initial state and calls the initialized callback if it's set.
void SearchBoxControl::_Initialize()
{
_UpdateSizeDependents();

// Search box is in Visible visibility state by default. This is to make
// sure DesiredSize() returns the correct size for the search box.
// (DesiredSize() seems to report a size of 0,0 until the control is
// visible for the first time, i.e not in Collapsed state).
// Here, we set the search box to "Closed" state (and hence Collapsed
// visibility) after we've updated the size-dependent properties.
VisualStateManager::GoToState(*this, L"Closed", false);

CloseAnimation().Completed([weakThis{ get_weak() }](auto&&, auto&&) {
if (auto searchbox{ weakThis.get() })
{
searchbox->CloseAnimation().Stop();
VisualStateManager::GoToState(*searchbox, L"Closed", false);
}
});

_initialized = true;
if (_initializedCallback)
{
_initializedCallback();
_initializedCallback = nullptr;
}
}

bool SearchBoxControl::_AnimationEnabled()
{
const auto uiSettings = winrt::Windows::UI::ViewManagement::UISettings{};
const auto isOsAnimationEnabled = uiSettings.AnimationsEnabled();
const auto isAppAnimationEnabled = Media::Animation::Timeline::AllowDependentAnimations();
return isOsAnimationEnabled && isAppAnimationEnabled;
}

// Method Description:
// - Opens the search box taking a callback to be executed when it's opened.
void SearchBoxControl::Open(std::function<void()> callback)
{
// defer opening the search box until we have initialized our size-dependent
// properties so we don't animate to wrong values.
if (!_initialized)
{
_initializedCallback = [this, callback]() { Open(callback); };
}
else
{
// don't run animation if we're already open.
// Note: We can't apply this check at the beginning of the function because the
// search box remains in Visible state (though not really *visible*) during the
// first load. So, we only need to apply this check here (after checking that
// we're done initializing).
if (Visibility() == Visibility::Visible)
{
callback();
return;
}

VisualStateManager::GoToState(*this, L"Opened", false);

// Call the callback only after we're in Opened state. Setting focus
// (through the callback) to a collapsed search box will not work.
callback();

// Don't animate if animation is disabled
if (_AnimationEnabled())
{
_PlayOpenAnimation();
}
}
}

// Method Description:
// - Closes the search box.
void SearchBoxControl::Close()
{
// Nothing to do if we're already closed
if (Visibility() == Visibility::Collapsed)
{
return;
}

if (_AnimationEnabled())
{
// close animation will set the state to "Closed" in its Completed handler.
_PlayCloseAnimation();
}
else
{
VisualStateManager::GoToState(*this, L"Closed", false);
}
}

// Method Description:
// - Check if the current search direction is forward
// Arguments:
Expand Down
19 changes: 19 additions & 0 deletions src/cascadia/TerminalControl/SearchBoxControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation

SearchBoxControl();

winrt::Windows::Foundation::Rect ContentClipRect() const noexcept;
double OpenAnimationStartPoint() const noexcept;

void TextBoxKeyDown(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e);
void Open(std::function<void()> callback);
void Close();

void SetFocusOnTextbox();
void PopulateTextbox(const winrt::hstring& text);
Expand All @@ -44,12 +49,26 @@ namespace winrt::Microsoft::Terminal::Control::implementation
void TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e);
void CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e);

WINRT_CALLBACK(PropertyChanged, winrt::Windows::UI::Xaml::Data::PropertyChangedEventHandler);
WINRT_CALLBACK(Search, SearchHandler);
WINRT_CALLBACK(SearchChanged, SearchHandler);
TYPED_EVENT(Closed, Control::SearchBoxControl, Windows::UI::Xaml::RoutedEventArgs);

private:
std::unordered_set<winrt::Windows::Foundation::IInspectable> _focusableElements;
winrt::Windows::Foundation::Rect _contentClipRect{ 0, 0, 0, 0 };
double _openAnimationStartPoint = 0;
winrt::Windows::UI::Xaml::FrameworkElement::Loaded_revoker _initialLoadedRevoker;
bool _initialized = false;
std::function<void()> _initializedCallback;

void _Initialize();
void _UpdateSizeDependents();
void _ContentClipRect(const winrt::Windows::Foundation::Rect& rect);
void _OpenAnimationStartPoint(double y);
void _PlayOpenAnimation();
void _PlayCloseAnimation();
bool _AnimationEnabled();

static winrt::hstring _FormatStatus(int32_t totalMatches, int32_t currentMatch);
static double _TextWidth(winrt::hstring text, double fontSize);
Expand Down
4 changes: 3 additions & 1 deletion src/cascadia/TerminalControl/SearchBoxControl.idl
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ namespace Microsoft.Terminal.Control
{
delegate void SearchHandler(String query, Boolean goForward, Boolean isCaseSensitive);

[default_interface] runtimeclass SearchBoxControl : Windows.UI.Xaml.Controls.UserControl
[default_interface] runtimeclass SearchBoxControl : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged
{
SearchBoxControl();
void SetFocusOnTextbox();
void PopulateTextbox(String text);
Boolean ContainsFocus();
void SetStatus(Int32 totalMatches, Int32 currentMatch);
Windows.Foundation.Rect ContentClipRect{ get; };
Double OpenAnimationStartPoint{ get; };

Boolean NavigationEnabled;

Expand Down
53 changes: 52 additions & 1 deletion src/cascadia/TerminalControl/SearchBoxControl.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<UserControl x:Class="Microsoft.Terminal.Control.SearchBoxControl"
<UserControl x:Class="Microsoft.Terminal.Control.SearchBoxControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
Expand Down Expand Up @@ -142,9 +142,38 @@
</Style>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>

<Duration x:Key="SearchBoxAnimationDuration">0:0:0.3</Duration>
<ExponentialEase x:Key="OpenAnimationEase"
EasingMode="EaseOut"
Exponent="6" />
<ExponentialEase x:Key="CloseAnimationEase"
EasingMode="EaseIn"
Exponent="6" />
<Storyboard x:Name="OpenAnimation">
<DoubleAnimation EasingFunction="{StaticResource OpenAnimationEase}"
Storyboard.TargetName="Transform"
Storyboard.TargetProperty="Y"
From="{x:Bind OpenAnimationStartPoint, Mode=OneWay}"
To="0"
Duration="{StaticResource SearchBoxAnimationDuration}" />
</Storyboard>
<Storyboard x:Name="CloseAnimation">
<DoubleAnimation EasingFunction="{StaticResource CloseAnimationEase}"
Storyboard.TargetName="Transform"
Storyboard.TargetProperty="Y"
From="0"
To="{x:Bind OpenAnimationStartPoint, Mode=OneWay}"
Duration="{StaticResource SearchBoxAnimationDuration}" />
</Storyboard>

</ResourceDictionary>
</UserControl.Resources>

<UserControl.Clip>
<RectangleGeometry Rect="{x:Bind ContentClipRect, Mode=OneWay}" />
</UserControl.Clip>

<StackPanel Margin="8"
Padding="4,8"
Background="{ThemeResource FlyoutPresenterBackground}"
Expand All @@ -154,6 +183,10 @@
Orientation="Horizontal"
Shadow="{StaticResource SharedShadow}"
Translation="0,0,16">
<StackPanel.RenderTransform>
<TranslateTransform x:Name="Transform" />
</StackPanel.RenderTransform>

<TextBox x:Name="TextBox"
x:Uid="SearchBox_TextBox"
Width="160"
Expand Down Expand Up @@ -220,5 +253,23 @@
FontSize="12"
Glyph="&#xE711;" />
</Button>

<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="SearchBoxStates">

<VisualState x:Name="Opened">
<VisualState.Setters>
<Setter Target="Root.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>

<VisualState x:Name="Closed">
<VisualState.Setters>
<Setter Target="Root.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>

</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</StackPanel>
</UserControl>
5 changes: 2 additions & 3 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
// get at its private implementation
_searchBox.copy_from(winrt::get_self<implementation::SearchBoxControl>(searchBox));
_searchBox->Visibility(Visibility::Visible);

// If a text is selected inside terminal, use it to populate the search box.
// If the search box already contains a value, it will be overridden.
Expand All @@ -423,7 +422,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
}

_searchBox->SetFocusOnTextbox();
_searchBox->Open([searchBox]() { searchBox.SetFocusOnTextbox(); });
}
}
}
Expand Down Expand Up @@ -504,7 +503,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const RoutedEventArgs& /*args*/)
{
_core.ClearSearch();
_searchBox->Visibility(Visibility::Collapsed);
_searchBox->Close();

// Set focus back to terminal control
this->Focus(FocusState::Programmatic);
Expand Down
3 changes: 1 addition & 2 deletions src/cascadia/TerminalControl/TermControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -1302,8 +1302,7 @@
x:Load="False"
Closed="_CloseSearchBoxControl"
Search="_Search"
SearchChanged="_SearchChanged"
Visibility="Collapsed" />
SearchChanged="_SearchChanged" />
</Grid>

<ScrollBar x:Name="ScrollBar"
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/pch.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include <winrt/Windows.UI.Xaml.Data.h>
#include <winrt/Windows.Ui.Xaml.Documents.h>
#include <winrt/Windows.UI.Xaml.Media.h>
#include <winrt/Windows.UI.Xaml.Media.Animation.h>
#include <winrt/Windows.UI.Xaml.Media.Imaging.h>
#include <winrt/Windows.UI.Xaml.Input.h>
#include <winrt/Windows.UI.Xaml.Interop.h>
Expand Down

0 comments on commit 07a6f6a

Please sign in to comment.