From 6e11780ca6afdcbe216c7a06bb2d6a19cf3edd97 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 17 May 2021 07:28:46 -0500 Subject: [PATCH] Add property to control dropdown speed of global summon (#9977) ## Summary of the Pull Request Adds the `dropdownDuration` property to `globalSummon`. This controls how fast the window appears on the screen when summoned from minimized. It similarly controls the speed for sliding out of view when the window is dismissed with `"toggleVisibility": true`. `dropdownDuration` specifies the duration in **milliseconds**. This defaults to `0` for `globalSummon`, and defaults to `200` for `quakeMode`. 200 was picked because, according to [`AnimateWindow`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-animatewindow): > Typically, an animation takes 200 milliseconds to play. Do note that you won't be able to interact with the window during the animation! Input sent during the dropdown will arrive at the end of the animation, but input sent during the slide-up _won't_. Avoid setting this to large values! The gifs are in Teams. ## References * Original thread: #653 * Spec: #9274 * megathread: #8888 ## PR Checklist * [x] Closes https://github.com/microsoft/terminal/projects/5#card-59030824 * [x] I work here * [x] Tests added/passed * [n/a] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments I had the following previously in the doc comments, but it feels better in the PR body: - This was chosen because it was easier to implement and generally nicer than: * `AnimateWindow`, which would show the window borders for the duration of the animation, and occasionally just plain not work. Additionally, for `AnimateWindow` to work, the window much not be visible, so we'd need to first restore the window, then hide it, then animate it. That would flash the taskbar. * `SetWindowRgn` on the root HWND, which caused the xaml content to shift to the left, and caused a black bar to be drawn on the right of the window. Presumably, `SetWindowRgn` and `DwmExtendFrameIntoClientArea` did not play well with each other. * `SetWindowPos(..., SWP_NOSENDCHANGING)`, which worked the absolute best for longer animations, and is the closest to the actual implementation of `AnimateWindow`. This would resize the ROOT window, without sending resizes to the XAML island, allowing the content to _not_ reflow. but for a duration of 200ms, would only ever display ~2 frames. That's basically not even animation anymore, it's now just an "appear". Since that's how long the default animation is, if felt silly to have it basically not work by default. - If a future reader would like to implement this better, **they should feel free to**, and not mistake my notes here as expertise. These are research notes into the dark and terrible land that is Win32 programming. I'm no expert. ## Validation Steps Performed This is the blob of json I'm testing with these days: ```jsonc { "keys": "ctrl+`", "command": { "action": "quakeMode" } }, { "keys": "ctrl+1", "command": { "action": "globalSummon" } }, // { "keys": "ctrl+2", "command": { "action": "globalSummon", "desktop": "toCurrent" } }, // { "keys": "ctrl+2", "command": { "action": "globalSummon", "toggleVisibility": false } }, { "keys": "ctrl+2", "command": { "action": "globalSummon", "dropdownDuration": 2000 } }, { "keys": "ctrl+3", "command": { "action": "globalSummon", "desktop": "onCurrent" } }, { "keys": "ctrl+4", "command": { "action": "globalSummon", "desktop": "any" } }, ``` * ctrl+\` will summon the quake window with a _quick_ animation * ctrl+2 will summon the window with a s l o w animation --- .github/actions/spelling/allow/apis.txt | 1 + src/cascadia/Remoting/Peasant.idl | 1 + src/cascadia/Remoting/SummonWindowBehavior.h | 2 + .../TerminalSettingsModel/ActionArgs.h | 8 + .../TerminalSettingsModel/ActionArgs.idl | 1 + src/cascadia/WindowsTerminal/AppHost.cpp | 5 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 175 +++++++++++++++--- src/cascadia/WindowsTerminal/IslandWindow.h | 9 +- 8 files changed, 174 insertions(+), 28 deletions(-) diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index b3661dc817e..d4aa706a022 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -35,6 +35,7 @@ HIGHCONTRASTON HIGHCONTRASTW hotkeys href +hrgn IActivation IApp IAppearance diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index 90ff1ab4513..33064bc9dfa 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -34,6 +34,7 @@ namespace Microsoft.Terminal.Remoting SummonWindowBehavior(); Boolean MoveToCurrentDesktop; Boolean ToggleVisibility; + UInt32 DropdownDuration; // Other options: // * CurrentMonitor } diff --git a/src/cascadia/Remoting/SummonWindowBehavior.h b/src/cascadia/Remoting/SummonWindowBehavior.h index d26a21aff98..caa11ee3bdc 100644 --- a/src/cascadia/Remoting/SummonWindowBehavior.h +++ b/src/cascadia/Remoting/SummonWindowBehavior.h @@ -23,10 +23,12 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation SummonWindowBehavior() = default; WINRT_PROPERTY(bool, MoveToCurrentDesktop, true); WINRT_PROPERTY(bool, ToggleVisibility, true); + WINRT_PROPERTY(uint32_t, DropdownDuration, 0); public: SummonWindowBehavior(const Remoting::SummonWindowBehavior& other) : _MoveToCurrentDesktop{ other.MoveToCurrentDesktop() }, + _DropdownDuration{ other.DropdownDuration() }, _ToggleVisibility{ other.ToggleVisibility() } {}; }; } diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.h b/src/cascadia/TerminalSettingsModel/ActionArgs.h index 2dbd2ac9124..520639880eb 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.h +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.h @@ -1159,10 +1159,12 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation WINRT_PROPERTY(winrt::hstring, Name, L""); WINRT_PROPERTY(Model::DesktopBehavior, Desktop, Model::DesktopBehavior::ToCurrent); WINRT_PROPERTY(bool, ToggleVisibility, true); + WINRT_PROPERTY(uint32_t, DropdownDuration, 0); static constexpr std::string_view NameKey{ "name" }; static constexpr std::string_view DesktopKey{ "desktop" }; static constexpr std::string_view ToggleVisibilityKey{ "toggleVisibility" }; + static constexpr std::string_view DropdownDurationKey{ "dropdownDuration" }; public: hstring GenerateName() const; @@ -1173,6 +1175,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { return otherAsUs->_Name == _Name && otherAsUs->_Desktop == _Desktop && + otherAsUs->_DropdownDuration == _DropdownDuration && otherAsUs->_ToggleVisibility == _ToggleVisibility; } return false; @@ -1183,6 +1186,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto args = winrt::make_self(); JsonUtils::GetValueForKey(json, NameKey, args->_Name); JsonUtils::GetValueForKey(json, DesktopKey, args->_Desktop); + JsonUtils::GetValueForKey(json, DropdownDurationKey, args->_DropdownDuration); JsonUtils::GetValueForKey(json, ToggleVisibilityKey, args->_ToggleVisibility); return { *args, {} }; } @@ -1191,6 +1195,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto copy{ winrt::make_self() }; copy->_Name = _Name; copy->_Desktop = _Desktop; + copy->_DropdownDuration = _DropdownDuration; copy->_ToggleVisibility = _ToggleVisibility; return *copy; } @@ -1200,7 +1205,10 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation { // LOAD BEARING: Not using make_self here _will_ break you in the future! auto args = winrt::make_self(); + // We want to summon the window with the name "_quake" specifically. args->_Name = QuakeWindowName; + // We want the window to dropdown, with a 200ms duration. + args->_DropdownDuration = 200; return { *args, {} }; } size_t Hash() const diff --git a/src/cascadia/TerminalSettingsModel/ActionArgs.idl b/src/cascadia/TerminalSettingsModel/ActionArgs.idl index f3570ae9a64..71c16a55e95 100644 --- a/src/cascadia/TerminalSettingsModel/ActionArgs.idl +++ b/src/cascadia/TerminalSettingsModel/ActionArgs.idl @@ -267,5 +267,6 @@ namespace Microsoft.Terminal.Settings.Model String Name { get; }; DesktopBehavior Desktop { get; }; Boolean ToggleVisibility { get; }; + UInt32 DropdownDuration { get; }; }; } diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 348a0a9a7f4..eebbc2920e6 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -591,7 +591,7 @@ void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable /*se { // Summon the window whenever we dispatch a commandline to it. This will // make it obvious when a new tab/pane is created in a window. - _window->SummonWindow(false); + _window->SummonWindow(false, 0); _logic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory()); } @@ -693,6 +693,7 @@ void AppHost::_GlobalHotkeyPressed(const long hotkeyIndex) args.OnCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::OnCurrent); args.SummonBehavior().MoveToCurrentDesktop(summonArgs.Desktop() == Settings::Model::DesktopBehavior::ToCurrent); args.SummonBehavior().ToggleVisibility(summonArgs.ToggleVisibility()); + args.SummonBehavior().DropdownDuration(summonArgs.DropdownDuration()); _windowManager.SummonWindow(args); if (args.FoundMatch()) @@ -775,7 +776,7 @@ bool AppHost::_LazyLoadDesktopManager() void AppHost::_HandleSummon(const winrt::Windows::Foundation::IInspectable& /*sender*/, const Remoting::SummonWindowBehavior& args) { - _window->SummonWindow(args.ToggleVisibility()); + _window->SummonWindow(args.ToggleVisibility(), args.DropdownDuration()); if (args != nullptr && args.MoveToCurrentDesktop()) { diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 55cd1b1e46e..f74de76ed03 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -1009,34 +1009,143 @@ void IslandWindow::SetGlobalHotkeys(const std::vector -winrt::fire_and_forget IslandWindow::SummonWindow(const bool toggleVisibility) +winrt::fire_and_forget IslandWindow::SummonWindow(const bool toggleVisibility, const uint32_t dropdownDuration) { // On the foreground thread: co_await winrt::resume_foreground(_rootGrid.Dispatcher()); + uint32_t actualDropdownDuration = dropdownDuration; + // If the user requested an animation, let's check if animations are enabled in the OS. + if (dropdownDuration > 0) + { + BOOL animationsEnabled = TRUE; + SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0); + if (!animationsEnabled) + { + // The OS has animations disabled - we should respect that and + // disable the animation here. + // + // We're doing this here, rather than in _doSlideAnimation, to + // preempt any other specific behavior that + // _globalActivateWindow/_globalDismissWindow might do if they think + // there should be an animation (like making the window appear with + // SetWindowPlacement rather than ShowWindow) + actualDropdownDuration = 0; + } + } + // * If the user doesn't want to toggleVisibility, then just always try to // activate. // * If the user does want to toggleVisibility, then dismiss the window if // we're the current foreground window. if (toggleVisibility && GetForegroundWindow() == _window.get()) { - _globalDismissWindow(); + _globalDismissWindow(actualDropdownDuration); } else { - _globalActivateWindow(); + _globalActivateWindow(actualDropdownDuration); } } +// Method Description: +// - Helper for performing a sliding animation. This will animate our _Xaml +// Island_, either growing down or shrinking up, using SetWindowRgn. +// - This function does the entire animation on the main thread (the UI thread), +// and **DOES NOT YIELD IT**. The window will be animating for the entire +// duration of dropdownDuration. +// - At the end of the animation, we'll reset the window region, so that it's as +// if nothing occurred. +// Arguments: +// - dropdownDuration: The duration to play the animation, in milliseconds. If +// 0, we won't perform a dropdown animation. +// - down: if true, increase the height from top to bottom. otherwise, decrease +// the height, from bottom to top. +// Return Value: +// - +void IslandWindow::_doSlideAnimation(const uint32_t dropdownDuration, const bool down) +{ + til::rectangle fullWindowSize{ GetWindowRect() }; + const double fullHeight = fullWindowSize.height(); + + const double animationDuration = dropdownDuration; // use floating-point math throughout + const auto start = std::chrono::system_clock::now(); + + // Do at most dropdownDuration frames. After that, just bail straight to the + // final state. + for (uint32_t i = 0; i < dropdownDuration; i++) + { + const auto end = std::chrono::system_clock::now(); + const auto millis = std::chrono::duration_cast(end - start); + const double dt = ::base::saturated_cast(millis.count()); + + if (dt > animationDuration) + { + break; + } + + // If going down, increase the height over time. If going up, decrease the height. + const double currentHeight = ::base::saturated_cast( + down ? ((dt / animationDuration) * fullHeight) : + ((1.0 - (dt / animationDuration)) * fullHeight)); + + wil::unique_hrgn rgn{ CreateRectRgn(0, + 0, + fullWindowSize.width(), + ::base::saturated_cast(currentHeight)) }; + + SetWindowRgn(_interopWindowHandle, rgn.get(), true); + + // Go immediately into another frame. This prevents the window from + // doing anything else (tearing our state). A Sleep() here will cause a + // weird stutter, and causes the animation to not be as smooth. + } + + // Reset the window. + SetWindowRgn(_interopWindowHandle, nullptr, true); +} + +void IslandWindow::_dropdownWindow(const uint32_t dropdownDuration) +{ + // First, restore the window. SetWindowPlacement has a fun undocumented + // piece of functionality where it will restore the window position + // _without_ the animation, so use that instead of ShowWindow(SW_RESTORE). + WINDOWPLACEMENT wpc{}; + wpc.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(_window.get(), &wpc); + wpc.showCmd = SW_RESTORE; + SetWindowPlacement(_window.get(), &wpc); + + // Now that we're visible, animate the dropdown. + _doSlideAnimation(dropdownDuration, true); +} + +void IslandWindow::_slideUpWindow(const uint32_t dropdownDuration) +{ + // First, animate the window sliding up. + _doSlideAnimation(dropdownDuration, false); + + // Then, use SetWindowPlacement to minimize without the animation. + WINDOWPLACEMENT wpc{}; + wpc.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(_window.get(), &wpc); + wpc.showCmd = SW_MINIMIZE; + SetWindowPlacement(_window.get(), &wpc); +} + // Method Description: // - Force activate this window. This method will bring us to the foreground and // activate us. If the window is minimized, it will restore the window. If the // window is on another desktop, the OS will switch to that desktop. +// - If the window is minimized, and dropdownDuration is greater than 0, we'll +// perform a "slide in" animation. We won't do this if the window is already +// on the screen (since that seems silly). // Arguments: -// - +// - dropdownDuration: The duration to play the dropdown animation, in +// milliseconds. If 0, we won't perform a dropdown animation. // Return Value: // - -void IslandWindow::_globalActivateWindow() +void IslandWindow::_globalActivateWindow(const uint32_t dropdownDuration) { // From: https://stackoverflow.com/a/59659421 // > The trick is to make windows ‘think’ that our process and the target @@ -1047,34 +1156,54 @@ void IslandWindow::_globalActivateWindow() // restore-down the window. if (IsIconic(_window.get())) { - LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_RESTORE)); + if (dropdownDuration > 0) + { + _dropdownWindow(dropdownDuration); + } + else + { + LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_RESTORE)); + } + } + else + { + const DWORD windowThreadProcessId = GetWindowThreadProcessId(GetForegroundWindow(), nullptr); + const DWORD currentThreadId = GetCurrentThreadId(); + + LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, true)); + // Just in case, add the thread detach as a scope_exit, to make _sure_ we do it. + auto detachThread = wil::scope_exit([windowThreadProcessId, currentThreadId]() { + LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, false)); + }); + LOG_IF_WIN32_BOOL_FALSE(BringWindowToTop(_window.get())); + LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_SHOW)); + + // Activate the window too. This will force us to the virtual desktop this + // window is on, if it's on another virtual desktop. + LOG_LAST_ERROR_IF_NULL(SetActiveWindow(_window.get())); } - const DWORD windowThreadProcessId = GetWindowThreadProcessId(GetForegroundWindow(), nullptr); - const DWORD currentThreadId = GetCurrentThreadId(); - - LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, true)); - // Just in case, add the thread detach as a scope_exit, to make _sure_ we do it. - auto detachThread = wil::scope_exit([windowThreadProcessId, currentThreadId]() { - LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, false)); - }); - LOG_IF_WIN32_BOOL_FALSE(BringWindowToTop(_window.get())); - LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_SHOW)); - - // Activate the window too. This will force us to the virtual desktop this - // window is on, if it's on another virtual desktop. - LOG_LAST_ERROR_IF_NULL(SetActiveWindow(_window.get())); } // Method Description: // - Minimize the window. This is called when the window is summoned, but is // already active +// - If dropdownDuration is greater than 0, we'll perform a "slide in" +// animation, before minimizing the window. // Arguments: -// - +// - dropdownDuration: The duration to play the slide-up animation, in +// milliseconds. If 0, we won't perform a slide-up animation. // Return Value: // - -void IslandWindow::_globalDismissWindow() +void IslandWindow::_globalDismissWindow(const uint32_t dropdownDuration) { - LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_MINIMIZE)); + if (dropdownDuration > 0) + { + _slideUpWindow(dropdownDuration); + } + else + { + LOG_IF_WIN32_BOOL_FALSE(ShowWindow(_window.get(), SW_MINIMIZE)); + } } bool IslandWindow::IsQuakeWindow() const noexcept diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index f73ecd5e614..09c27ffbaef 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -41,7 +41,7 @@ class IslandWindow : void UnsetHotkeys(const std::vector& hotkeyList); void SetGlobalHotkeys(const std::vector& hotkeyList); - winrt::fire_and_forget SummonWindow(const bool toggleVisibility); + winrt::fire_and_forget SummonWindow(const bool toggleVisibility, const uint32_t dropdownDuration); bool IsQuakeWindow() const noexcept; void IsQuakeWindow(bool isQuakeWindow) noexcept; @@ -93,8 +93,11 @@ class IslandWindow : void _OnGetMinMaxInfo(const WPARAM wParam, const LPARAM lParam); long _calculateTotalSize(const bool isWidth, const long clientSize, const long nonClientSize); - void _globalActivateWindow(); - void _globalDismissWindow(); + void _globalActivateWindow(const uint32_t dropdownDuration); + void _dropdownWindow(const uint32_t dropdownDuration); + void _slideUpWindow(const uint32_t dropdownDuration); + void _doSlideAnimation(const uint32_t dropdownDuration, const bool down); + void _globalDismissWindow(const uint32_t dropdownDuration); bool _isQuakeWindow{ false }; void _enterQuakeMode();