diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index ddbbafdbe9b..34b69947e54 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -33,7 +33,8 @@ constexpr const auto FrameUpdateInterval = std::chrono::milliseconds(16); AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, const Remoting::WindowManager& manager, - const Remoting::Peasant& peasant) noexcept : + const Remoting::Peasant& peasant, + std::unique_ptr window) noexcept : _appLogic{ logic }, _windowLogic{ nullptr }, // don't make one, we're going to take a ref on app's _windowManager{ manager }, @@ -48,13 +49,22 @@ AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, // _HandleCommandlineArgs will create a _windowLogic _useNonClientArea = _windowLogic.GetShowTabsInTitlebar(); - if (_useNonClientArea) + + const bool isWarmStart = window != nullptr; + if (isWarmStart) { - _window = std::make_unique(_windowLogic.GetRequestedTheme()); + _window = std::move(window); } else { - _window = std::make_unique(); + if (_useNonClientArea) + { + _window = std::make_unique(_windowLogic.GetRequestedTheme()); + } + else + { + _window = std::make_unique(); + } } // Update our own internal state tracking if we're in quake mode or not. @@ -69,14 +79,10 @@ AppHost::AppHost(const winrt::TerminalApp::AppLogic& logic, std::placeholders::_2); _window->SetCreateCallback(pfn); - _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); - _window->WindowActivated({ this, &AppHost::_WindowActivated }); - _window->WindowMoved({ this, &AppHost::_WindowMoved }); - - _window->ShouldExitFullscreen({ &_windowLogic, &winrt::TerminalApp::TerminalWindow::RequestExitFullscreen }); - - _window->SetAlwaysOnTop(_windowLogic.GetInitialAlwaysOnTop()); - _window->SetAutoHideWindow(_windowLogic.AutoHideWindow()); + _windowCallbacks.MouseScrolled = _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); + _windowCallbacks.WindowActivated = _window->WindowActivated({ this, &AppHost::_WindowActivated }); + _windowCallbacks.WindowMoved = _window->WindowMoved({ this, &AppHost::_WindowMoved }); + _windowCallbacks.ShouldExitFullscreen = _window->ShouldExitFullscreen({ &_windowLogic, &winrt::TerminalApp::TerminalWindow::RequestExitFullscreen }); _window->MakeWindow(); } @@ -286,6 +292,14 @@ void AppHost::Initialize() _windowLogic.SetTitleBarContent({ this, &AppHost::_UpdateTitleBarContent }); } + // These call APIs that are reentrant on the window message loop. If + // you call them in the ctor, we might deadlock. The ctor for AppHost isn't + // always called on the window thread - for reheated windows, it could be + // called on a random COM thread. + + _window->SetAlwaysOnTop(_windowLogic.GetInitialAlwaysOnTop()); + _window->SetAutoHideWindow(_windowLogic.AutoHideWindow()); + // MORE EVENT HANDLERS HERE! // MAKE SURE THEY ARE ALL: // * winrt::auto_revoke @@ -295,13 +309,14 @@ void AppHost::Initialize() // tearing down, after we've nulled out the window, during the dtor. That // can cause unexpected AV's everywhere. // - // _window callbacks don't need to be treated this way, because: - // * IslandWindow isn't a WinRT type (so it doesn't have neat revokers like this) - // * This particular bug scenario applies when we've already freed the window. + // _window callbacks are a little special: + // * IslandWindow isn't a WinRT type (so it doesn't have neat revokers like + // this), so instead they go in their own special helper struct. + // * they all need to be manually revoked in _revokeWindowCallbacks. // Register the 'X' button of the window for a warning experience of multiple // tabs opened, this is consistent with Alt+F4 closing - _window->WindowCloseButtonClicked([this]() { + _windowCallbacks.WindowCloseButtonClicked = _window->WindowCloseButtonClicked([this]() { _CloseRequested(nullptr, nullptr); }); // If the user requests a close in another way handle the same as if the 'X' @@ -310,11 +325,11 @@ void AppHost::Initialize() // Add an event handler to plumb clicks in the titlebar area down to the // application layer. - _window->DragRegionClicked([this]() { _windowLogic.TitlebarClicked(); }); + _windowCallbacks.DragRegionClicked = _window->DragRegionClicked([this]() { _windowLogic.TitlebarClicked(); }); - _window->WindowVisibilityChanged([this](bool showOrHide) { _windowLogic.WindowVisibilityChanged(showOrHide); }); + _windowCallbacks.WindowVisibilityChanged = _window->WindowVisibilityChanged([this](bool showOrHide) { _windowLogic.WindowVisibilityChanged(showOrHide); }); - _window->UpdateSettingsRequested({ this, &AppHost::_requestUpdateSettings }); + _windowCallbacks.UpdateSettingsRequested = _window->UpdateSettingsRequested({ this, &AppHost::_requestUpdateSettings }); _revokers.Initialized = _windowLogic.Initialized(winrt::auto_revoke, { this, &AppHost::_WindowInitializedHandler }); _revokers.RequestedThemeChanged = _windowLogic.RequestedThemeChanged(winrt::auto_revoke, { this, &AppHost::_UpdateTheme }); @@ -325,14 +340,14 @@ void AppHost::Initialize() _revokers.SystemMenuChangeRequested = _windowLogic.SystemMenuChangeRequested(winrt::auto_revoke, { this, &AppHost::_SystemMenuChangeRequested }); _revokers.ChangeMaximizeRequested = _windowLogic.ChangeMaximizeRequested(winrt::auto_revoke, { this, &AppHost::_ChangeMaximizeRequested }); - _window->MaximizeChanged([this](bool newMaximize) { + _windowCallbacks.MaximizeChanged = _window->MaximizeChanged([this](bool newMaximize) { if (_windowLogic) { _windowLogic.Maximized(newMaximize); } }); - _window->AutomaticShutdownRequested([this]() { + _windowCallbacks.AutomaticShutdownRequested = _window->AutomaticShutdownRequested([this]() { // Raised when the OS is beginning an update of the app. We will quit, // to save our state, before the OS manually kills us. Remoting::WindowManager::RequestQuitAll(_peasant); @@ -428,6 +443,9 @@ void AppHost::Close() _frameTimer.Tick(_frameTimerToken); } _showHideWindowThrottler.reset(); + + _revokeWindowCallbacks(); + _window->Close(); if (_windowLogic) @@ -437,6 +455,50 @@ void AppHost::Close() } } +void AppHost::_revokeWindowCallbacks() +{ + // You'll recall, IslandWindow isn't a WinRT type so it can't have auto-revokers. + // + // Instead, we need to manually remove our callbacks we registered on the window object. + _window->MouseScrolled(_windowCallbacks.MouseScrolled); + _window->WindowActivated(_windowCallbacks.WindowActivated); + _window->WindowMoved(_windowCallbacks.WindowMoved); + _window->ShouldExitFullscreen(_windowCallbacks.ShouldExitFullscreen); + _window->WindowCloseButtonClicked(_windowCallbacks.WindowCloseButtonClicked); + _window->DragRegionClicked(_windowCallbacks.DragRegionClicked); + _window->WindowVisibilityChanged(_windowCallbacks.WindowVisibilityChanged); + _window->UpdateSettingsRequested(_windowCallbacks.UpdateSettingsRequested); + _window->MaximizeChanged(_windowCallbacks.MaximizeChanged); + _window->AutomaticShutdownRequested(_windowCallbacks.AutomaticShutdownRequested); +} + +// revoke our callbacks, discard our XAML content (TerminalWindow & +// TerminalPage), and hand back our IslandWindow. This does _not_ close the XAML +// island for this thread. We should not be re-used after this, and our caller +// can destruct us like they normally would during a close. The returned +// IslandWindow will retain ownership of the DesktopWindowXamlSource, for later +// reuse. +[[nodiscard]] std::unique_ptr AppHost::Refrigerate() +{ + // After calling _window->Close() we should avoid creating more WinUI related actions. + // I suspect WinUI wouldn't like that very much. As such unregister all event handlers first. + _revokers = {}; + _showHideWindowThrottler.reset(); + + _revokeWindowCallbacks(); + + // DO NOT CLOSE THE WINDOW + _window->Refrigerate(); + + if (_windowLogic) + { + _windowLogic.DismissDialog(); + _windowLogic = nullptr; + } + + return std::move(_window); +} + // Method Description: // - Called every time when the active tab's title changes. We'll also fire off // a window message so we can update the window's title on the main thread, @@ -1049,22 +1111,7 @@ static bool _isActuallyDarkTheme(const auto requestedTheme) // Windows 10, so that we don't even get that spew void _frameColorHelper(const HWND h, const COLORREF color) { - static const bool isWindows11 = []() { - OSVERSIONINFOEXW osver{}; - osver.dwOSVersionInfoSize = sizeof(osver); - osver.dwBuildNumber = 22000; - - DWORDLONG dwlConditionMask = 0; - VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL); - - if (VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE) - { - return true; - } - return false; - }(); - - if (isWindows11) + if (Utils::IsWindows11()) { LOG_IF_FAILED(DwmSetWindowAttribute(h, DWMWA_BORDER_COLOR, &color, sizeof(color))); } diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 27b71af865d..dabe517394b 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -12,12 +12,16 @@ class AppHost AppHost(const winrt::TerminalApp::AppLogic& logic, winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, const winrt::Microsoft::Terminal::Remoting::WindowManager& manager, - const winrt::Microsoft::Terminal::Remoting::Peasant& peasant) noexcept; + const winrt::Microsoft::Terminal::Remoting::Peasant& peasant, + std::unique_ptr window = nullptr) noexcept; void AppTitleChanged(const winrt::Windows::Foundation::IInspectable& sender, winrt::hstring newTitle); void LastTabClosed(const winrt::Windows::Foundation::IInspectable& sender, const winrt::TerminalApp::LastTabClosedEventArgs& args); void Initialize(); void Close(); + + [[nodiscard]] std::unique_ptr Refrigerate(); + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); void SetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); @@ -58,6 +62,8 @@ class AppHost void _preInit(); + void _revokeWindowCallbacks(); + void _HandleCommandlineArgs(const winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs& args); void _HandleSessionRestore(const bool startedForContent); @@ -202,4 +208,24 @@ class AppHost winrt::Microsoft::Terminal::Remoting::WindowManager::QuitAllRequested_revoker QuitAllRequested; winrt::Microsoft::Terminal::Remoting::Peasant::SendContentRequested_revoker SendContentRequested; } _revokers{}; + + // our IslandWindow is not a WinRT type. It can't make auto_revokers like + // the above. We also need to make sure to unregister ourself from the + // window when we refrigerate the window thread so that the window can later + // be re-used. + struct WindowRevokers + { + winrt::event_token MouseScrolled; + winrt::event_token WindowActivated; + winrt::event_token WindowMoved; + winrt::event_token ShouldExitFullscreen; + winrt::event_token WindowCloseButtonClicked; + winrt::event_token DragRegionClicked; + winrt::event_token WindowVisibilityChanged; + winrt::event_token UpdateSettingsRequested; + winrt::event_token MaximizeChanged; + winrt::event_token AutomaticShutdownRequested; + // LOAD BEARING!! + //If you add events here, make sure they're revoked in AppHost::_revokeWindowCallbacks + } _windowCallbacks{}; }; diff --git a/src/cascadia/WindowsTerminal/BaseWindow.h b/src/cascadia/WindowsTerminal/BaseWindow.h index 75afc8e3b88..36c4eabb80e 100644 --- a/src/cascadia/WindowsTerminal/BaseWindow.h +++ b/src/cascadia/WindowsTerminal/BaseWindow.h @@ -210,13 +210,17 @@ class BaseWindow bool _minimized = false; + void _setupUserData() + { + SetWindowLongPtr(_window.get(), GWLP_USERDATA, reinterpret_cast(this)); + } // Method Description: // - This method is called when the window receives the WM_NCCREATE message. // Return Value: // - The value returned from the window proc. [[nodiscard]] virtual LRESULT OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept { - SetWindowLongPtr(_window.get(), GWLP_USERDATA, reinterpret_cast(this)); + _setupUserData(); EnableNonClientDpiScaling(_window.get()); _currentDpi = GetDpiForWindow(_window.get()); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 88e15c9ede5..87ec8d5dbce 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -42,46 +42,6 @@ IslandWindow::~IslandWindow() void IslandWindow::Close() { - static const bool isWindows11 = []() { - OSVERSIONINFOEXW osver{}; - osver.dwOSVersionInfoSize = sizeof(osver); - osver.dwBuildNumber = 22000; - - DWORDLONG dwlConditionMask = 0; - VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL); - - if (VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE) - { - return true; - } - return false; - }(); - - if (!isWindows11) - { - // BODGY - // ____ ____ _____ _______ __ - // | _ \ / __ \| __ \ / ____\ \ / / - // | |_) | | | | | | | | __ \ \_/ / - // | _ <| | | | | | | | |_ | \ / - // | |_) | |__| | |__| | |__| | | | - // |____/ \____/|_____/ \_____| |_| - // - // There's a bug in Windows 10 where closing a DesktopWindowXamlSource - // on any thread will free an internal static resource that's used by - // XAML for the entire process. This would result in closing window - // essentially causing the entire app to crash. - // - // To avoid this, leak the XAML island. We only need to leak this on - // Windows 10, since the bug is fixed in Windows 11. - // - // See GH #15384, MSFT:32109540 - auto a{ _source }; - winrt::detach_abi(_source); - - // - } - // GH#15454: Unset the user data for the window. This will prevent future // callbacks that come onto our window message loop from being sent to the // IslandWindow (or other derived class's) implementation. @@ -99,6 +59,27 @@ void IslandWindow::Close() } } +// Clear out any state that might be associated with this app instance, so that +// we can later re-use this HWND for another instance. +// +// This doesn't actually close out our HWND or DesktopWindowXamlSource, but it +// will remove all our content, and SW_HIDE the window, so it isn't accessible. +void IslandWindow::Refrigerate() noexcept +{ + // Similar to in Close - unset our HWND's user data. We'll re-set this when + // we get re-heated, so that while we're refrigerated, we won't have + // unexpected callbacks into us while we don't have content. + // + // This pointer will get re-set in _warmInitialize + SetWindowLongPtr(_window.get(), GWLP_USERDATA, 0); + + _pfnCreateCallback = nullptr; + _pfnSnapDimensionCallback = nullptr; + + _rootGrid.Children().Clear(); + ShowWindow(_window.get(), SW_HIDE); +} + HWND IslandWindow::GetInteropHandle() const { return _interopWindowHandle; @@ -112,6 +93,12 @@ HWND IslandWindow::GetInteropHandle() const // - void IslandWindow::MakeWindow() noexcept { + if (_window) + { + // no-op if we already have a window. + return; + } + WNDCLASS wc{}; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); wc.hInstance = reinterpret_cast(&__ImageBase); @@ -345,7 +332,30 @@ LRESULT IslandWindow::_OnMoving(const WPARAM /*wParam*/, const LPARAM lParam) return false; } -void IslandWindow::Initialize() +// return true if this was a "cold" initialize, that didn't start XAML before. +bool IslandWindow::Initialize() +{ + if (!_source) + { + _coldInitialize(); + return true; + } + else + { + // This was a "warm" initialize - we've already got an HWND, but we need + // to move it to the new correct place, new size, and reset any leftover + // runtime state. + _warmInitialize(); + return false; + } +} + +// Method Description: +// - Start this window for the first time. This will instantiate our XAML +// island, set up our root grid, and initialize some other members that only +// need to be initialized once. +// - This should only be called once. +void IslandWindow::_coldInitialize() { _source = DesktopWindowXamlSource{}; @@ -378,9 +388,32 @@ void IslandWindow::Initialize() // We don't really care if this failed or not. TerminalTrySetTransparentBackground(true); } +void IslandWindow::_warmInitialize() +{ + // re-add the pointer to us to our HWND's user data, so that we can start + // getting window proc callbacks again. + _setupUserData(); + + // Manually ask how we want to be created. + if (_pfnCreateCallback) + { + til::rect rc{ GetWindowRect() }; + _pfnCreateCallback(_window.get(), rc); + } + + // Don't call IslandWindow::OnSize - that will set the Width/Height members + // of the _rootGrid. However, NonClientIslandWindow doesn't use those! If you set them, here, + // the contents of the window will never resize. + UpdateWindow(_window.get()); + ForceResize(); +} void IslandWindow::OnSize(const UINT width, const UINT height) { + // NOTE: This _isn't_ called by NonClientIslandWindow::OnSize. The + // NonClientIslandWindow has very different logic for positioning the + // DesktopWindowXamlSource inside its HWND. + // update the interop window size SetWindowPos(_interopWindowHandle, nullptr, 0, 0, width, height, SWP_SHOWWINDOW | SWP_NOACTIVATE); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index db7411f7345..c0abd343e1b 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -3,6 +3,7 @@ #include "pch.h" #include "BaseWindow.h" +#include void SetWindowLongWHelper(const HWND hWnd, const int nIndex, const LONG dwNewLong) noexcept; @@ -21,6 +22,9 @@ class IslandWindow : virtual void MakeWindow() noexcept; virtual void Close(); + + virtual void Refrigerate() noexcept; + virtual void OnSize(const UINT width, const UINT height); HWND GetInteropHandle() const; @@ -37,7 +41,7 @@ class IslandWindow : virtual til::rect GetNonClientFrame(const UINT dpi) const noexcept; virtual til::size GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept; - virtual void Initialize(); + virtual bool Initialize(); void SetCreateCallback(std::function pfn) noexcept; @@ -114,6 +118,9 @@ class IslandWindow : RECT _rcWorkBeforeFullscreen{}; UINT _dpiBeforeFullscreen{ 96 }; + void _coldInitialize(); + void _warmInitialize(); + virtual void _SetIsBorderless(const bool borderlessEnabled); virtual void _SetIsFullscreen(const bool fullscreenEnabled); void _RestoreFullscreenPosition(const RECT& rcWork); diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index bb71bb1f69d..ee538bfa71a 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -57,6 +57,12 @@ static constexpr const wchar_t* dragBarClassName{ L"DRAG_BAR_WINDOW_CLASS" }; void NonClientIslandWindow::MakeWindow() noexcept { + if (_window) + { + // no-op if we already have a window. + return; + } + IslandWindow::MakeWindow(); static auto dragBarWindowClass{ []() { @@ -335,9 +341,17 @@ void NonClientIslandWindow::OnAppInitialized() IslandWindow::OnAppInitialized(); } -void NonClientIslandWindow::Initialize() +void NonClientIslandWindow::Refrigerate() noexcept +{ + IslandWindow::Refrigerate(); + + // Revoke all our XAML callbacks. + _callbacks = {}; +} + +bool NonClientIslandWindow::Initialize() { - IslandWindow::Initialize(); + const bool coldInit = IslandWindow::Initialize(); _UpdateFrameMargins(); @@ -349,6 +363,7 @@ void NonClientIslandWindow::Initialize() Controls::RowDefinition contentRow{}; titlebarRow.Height(GridLengthHelper::Auto()); + _rootGrid.RowDefinitions().Clear(); _rootGrid.RowDefinitions().Append(titlebarRow); _rootGrid.RowDefinitions().Append(contentRow); @@ -356,8 +371,8 @@ void NonClientIslandWindow::Initialize() _titlebar = winrt::TerminalApp::TitlebarControl{ reinterpret_cast(GetHandle()) }; _dragBar = _titlebar.DragBar(); - _dragBar.SizeChanged({ this, &NonClientIslandWindow::_OnDragBarSizeChanged }); - _rootGrid.SizeChanged({ this, &NonClientIslandWindow::_OnDragBarSizeChanged }); + _callbacks.dragBarSizeChanged = _dragBar.SizeChanged(winrt::auto_revoke, { this, &NonClientIslandWindow::_OnDragBarSizeChanged }); + _callbacks.rootGridSizeChanged = _rootGrid.SizeChanged(winrt::auto_revoke, { this, &NonClientIslandWindow::_OnDragBarSizeChanged }); _rootGrid.Children().Append(_titlebar); @@ -366,7 +381,15 @@ void NonClientIslandWindow::Initialize() // GH#3440 - When the titlebar is loaded (officially added to our UI tree), // then make sure to update its visual state to reflect if we're in the // maximized state on launch. - _titlebar.Loaded([this](auto&&, auto&&) { _OnMaximizeChange(); }); + _callbacks.titlebarLoaded = _titlebar.Loaded(winrt::auto_revoke, [this](auto&&, auto&&) { _OnMaximizeChange(); }); + + // LOAD BEARING: call _ResizeDragBarWindow to update the position of our + // XAML island to reflect our current bounds. In the case of a "warm init" + // (i.e. re-using an existing window), we need to manually update the + // island's position to fill the new window bounds. + _ResizeDragBarWindow(); + + return coldInit; } // Method Description: diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 09e02022486..d173ad68c2d 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -31,6 +31,8 @@ class NonClientIslandWindow : public IslandWindow NonClientIslandWindow(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) noexcept; + void Refrigerate() noexcept override; + virtual void Close() override; void MakeWindow() noexcept override; virtual void OnSize(const UINT width, const UINT height) override; @@ -40,7 +42,7 @@ class NonClientIslandWindow : public IslandWindow virtual til::rect GetNonClientFrame(UINT dpi) const noexcept override; virtual til::size GetTotalNonClientExclusiveSize(UINT dpi) const noexcept override; - void Initialize() override; + bool Initialize() override; void OnAppInitialized() override; void SetContent(winrt::Windows::UI::Xaml::UIElement content) override; @@ -95,4 +97,11 @@ class NonClientIslandWindow : public IslandWindow void _UpdateFrameMargins() const noexcept; void _UpdateMaximizedState(); void _UpdateIslandPosition(const UINT windowWidth, const UINT windowHeight); + + struct Revokers + { + winrt::Windows::UI::Xaml::Controls::Border::SizeChanged_revoker dragBarSizeChanged; + winrt::Windows::UI::Xaml::Controls::Grid::SizeChanged_revoker rootGridSizeChanged; + winrt::TerminalApp::TitlebarControl::Loaded_revoker titlebarLoaded; + } _callbacks{}; }; diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index f4cfe9fad3b..23aba3d683e 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -111,7 +111,8 @@ bool WindowEmperor::HandleCommandlineArgs() const auto result = _manager.ProposeCommandline(eventArgs, isolatedMode); - if (result.ShouldCreateWindow()) + const bool makeWindow = result.ShouldCreateWindow(); + if (makeWindow) { _createNewWindowThread(Remoting::WindowRequestedArgs{ result, eventArgs }); @@ -127,7 +128,7 @@ bool WindowEmperor::HandleCommandlineArgs() } } - return result.ShouldCreateWindow(); + return makeWindow; } void WindowEmperor::WaitForWindows() @@ -143,7 +144,38 @@ void WindowEmperor::WaitForWindows() void WindowEmperor::_createNewWindowThread(const Remoting::WindowRequestedArgs& args) { Remoting::Peasant peasant{ _manager.CreatePeasant(args) }; - auto window{ std::make_shared(_app.Logic(), args, _manager, peasant) }; + std::shared_ptr window{ nullptr }; + + // FIRST: Attempt to reheat an existing window that we refrigerated for + // later. If we have an existing unused window, then we don't need to create + // a new WindowThread & HWND for this request. + { // Add a scope to minimize lock duration. + auto fridge{ _oldThreads.lock() }; + if (!fridge->empty()) + { + // Look at that, a refrigerated thread ready to be used. Let's use that! + window = std::move(fridge->back()); + fridge->pop_back(); + } + } + + // Did we find one? + if (window) + { + // Cool! Let's increment the number of active windows, and re-heat it. + _windowThreadInstances.fetch_add(1, std::memory_order_relaxed); + + window->Microwave(args, peasant); + // This will unblock the event we're waiting on in KeepWarm, and the + // window thread (started below) will continue through it's loop + return; + } + + // At this point, there weren't any pending refrigerated threads we could + // just use. That's fine. Let's just go create a new one. + + window = std::make_shared(_app.Logic(), args, _manager, peasant); + std::weak_ptr weakThis{ weak_from_this() }; // Increment our count of window instances _now_, immediately. We're @@ -164,33 +196,66 @@ void WindowEmperor::_createNewWindowThread(const Remoting::WindowRequestedArgs& std::thread t([weakThis, window]() { try { - const auto decrementWindowCount = wil::scope_exit([&]() { - if (auto self{ weakThis.lock() }) - { - self->_decrementWindowCount(); - } - }); - auto removeWindow = wil::scope_exit([&]() { - if (auto self{ weakThis.lock() }) - { - self->_removeWindow(window->PeasantID()); - } - }); - window->CreateHost(); if (auto self{ weakThis.lock() }) { self->_windowStartedHandlerPostXAML(window); } + while (window->KeepWarm()) + { + // Now that the window is ready to go, we can add it to our list of windows, + // because we know it will be well behaved. + // + // Be sure to only modify the list of windows under lock. - window->RunMessagePump(); - - // Manually trigger the cleanup callback. This will ensure that we - // remove the window from our list of windows, before we release the - // AppHost (and subsequently, the host's Logic() member that we use - // elsewhere). - removeWindow.reset(); + if (auto self{ weakThis.lock() }) + { + auto lockedWindows{ self->_windows.lock() }; + lockedWindows->push_back(window); + } + auto removeWindow = wil::scope_exit([&]() { + if (auto self{ weakThis.lock() }) + { + self->_removeWindow(window->PeasantID()); + } + }); + + auto decrementWindowCount = wil::scope_exit([&]() { + if (auto self{ weakThis.lock() }) + { + self->_decrementWindowCount(); + } + }); + + window->RunMessagePump(); + + // Manually trigger the cleanup callback. This will ensure that we + // remove the window from our list of windows, before we release the + // AppHost (and subsequently, the host's Logic() member that we use + // elsewhere). + removeWindow.reset(); + + // On Windows 11, we DONT want to refrigerate the window. There, + // we can just close it like normal. Break out of the loop, so + // we don't try to put this window in the fridge. + if (Utils::IsWindows11()) + { + decrementWindowCount.reset(); + break; + } + else + { + window->Refrigerate(); + decrementWindowCount.reset(); + + if (auto self{ weakThis.lock() }) + { + auto fridge{ self->_oldThreads.lock() }; + fridge->push_back(window); + } + } + } // Now that we no longer care about this thread's window, let it // release it's app host and flush the rest of the XAML queue. @@ -224,15 +289,6 @@ void WindowEmperor::_windowStartedHandlerPostXAML(const std::shared_ptrpush_back(sender); - } } void WindowEmperor::_removeWindow(uint64_t senderID) @@ -518,8 +574,22 @@ LRESULT WindowEmperor::_messageHandler(UINT const message, WPARAM const wParam, return DefWindowProc(_window.get(), message, wParam, lParam); } +// Close the Terminal application. This will exit the main thread for the +// emperor itself. We should probably only ever be called when we have no +// windows left, and we don't want to keep running anymore. This will discard +// all our refrigerated windows. If we try to use XAML on Windows 10 after this, +// we'll undoubtedly crash. winrt::fire_and_forget WindowEmperor::_close() { + { + auto fridge{ _oldThreads.lock() }; + for (auto& window : *fridge) + { + window->ThrowAway(); + } + fridge->clear(); + } + // Important! Switch back to the main thread for the emperor. That way, the // quit will go to the emperor's message pump. co_await wil::resume_foreground(_dispatcher); diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index 96688ee08c2..47e8b356195 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -43,6 +43,8 @@ class WindowEmperor : public std::enable_shared_from_this til::shared_mutex>> _windows; std::atomic _windowThreadInstances; + til::shared_mutex>> _oldThreads; + std::optional> _getWindowLayoutThrottler; winrt::event_token _WindowCreatedToken; diff --git a/src/cascadia/WindowsTerminal/WindowThread.cpp b/src/cascadia/WindowsTerminal/WindowThread.cpp index 7ddef728b0b..25fb94d5b07 100644 --- a/src/cascadia/WindowsTerminal/WindowThread.cpp +++ b/src/cascadia/WindowsTerminal/WindowThread.cpp @@ -18,12 +18,18 @@ WindowThread::WindowThread(winrt::TerminalApp::AppLogic logic, void WindowThread::CreateHost() { + // Calling this while refrigerated won't work. + // * We can't re-initialize our winrt apartment. + // * AppHost::Initialize has to be done on the "UI" thread. + assert(_warmWindow == nullptr); + // Start the AppHost HERE, on the actual thread we want XAML to run on _host = std::make_unique<::AppHost>(_appLogic, _args, _manager, _peasant); - _host->UpdateSettingsRequested([this]() { _UpdateSettingsRequestedHandlers(); }); + + _UpdateSettingsRequestedToken = _host->UpdateSettingsRequested([this]() { _UpdateSettingsRequestedHandlers(); }); winrt::init_apartment(winrt::apartment_type::single_threaded); @@ -40,9 +46,29 @@ int WindowThread::RunMessagePump() return exitCode; } +void WindowThread::_pumpRemainingXamlMessages() +{ + MSG msg = {}; + while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + { + ::DispatchMessageW(&msg); + } +} + void WindowThread::RundownForExit() { - _host->Close(); + if (_host) + { + _host->UpdateSettingsRequested(_UpdateSettingsRequestedToken); + _host->Close(); + } + if (_warmWindow) + { + // If we have a _warmWindow, we're a refrigerated thread without a + // AppHost in control of the window. Manually close the window + // ourselves, to free the DesktopWindowXamlSource. + _warmWindow->Close(); + } // !! LOAD BEARING !! // @@ -52,15 +78,102 @@ void WindowThread::RundownForExit() // exiting. So do that now. If you don't, then the last tab to close // will never actually destruct the last tab / TermControl / ControlCore // / renderer. + _pumpRemainingXamlMessages(); +} + +void WindowThread::ThrowAway() +{ + // raise the signal to unblock KeepWarm. We won't have a host, so we'll drop + // out of the message loop to eventually RundownForExit. + // + // This should only be called when the app is fully quitting. After this is + // called on any thread, on win10, we won't be able to call into XAML + // anymore. + _microwaveBuzzer.notify_one(); +} + +// Method Description: +// - Check if we should keep this window alive, to try it's message loop again. +// If we were refrigerated for later, then this will block the thread on the +// _microwaveBuzzer. We'll sit there like that till the emperor decides if +// they want to re-use this window thread for a new window. +// Return Value: +// - true IFF we should enter this thread's message loop +// INVARIANT: This must be called on our "ui thread", our window thread. +bool WindowThread::KeepWarm() +{ + if (_host != nullptr) { - MSG msg = {}; - while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + // We're currently hot + return true; + } + + // If we're refrigerated, then wait on the microwave signal, which will be + // raised when we get re-heated by another thread to reactivate us. + + if (_warmWindow != nullptr) + { + std::unique_lock lock(_microwave); + _microwaveBuzzer.wait(lock); + + // If ThrowAway() was called, then the buzzer will be signalled without + // setting a new _host. In that case, the app is quitting, for real. We + // just want to exit with false. + const bool reheated = _host != nullptr; + if (reheated) { - ::DispatchMessageW(&msg); + _UpdateSettingsRequestedToken = _host->UpdateSettingsRequested([this]() { _UpdateSettingsRequestedHandlers(); }); + // Re-initialize the host here, on the window thread + _host->Initialize(); } + return reheated; + } + else + { + return false; } } +// Method Description: +// - "Refrigerate" this thread for later reuse. This will refrigerate the window +// itself, and tear down our current app host. We'll save our window for +// later. We'll also pump out the existing message from XAML, before +// returning. After we return, the emperor will add us to the list of threads +// that can be re-used. +void WindowThread::Refrigerate() +{ + _host->UpdateSettingsRequested(_UpdateSettingsRequestedToken); + + // keep a reference to the HWND and DesktopWindowXamlSource alive. + _warmWindow = std::move(_host->Refrigerate()); + + // rundown remaining messages before destructing the app host + _pumpRemainingXamlMessages(); + _host = nullptr; +} + +// Method Description: +// - "Reheat" this thread for reuse. We'll build a new AppHost, and pass in the +// existing window to it. We'll then trigger the _microwaveBuzzer, so KeepWarm +// (which is on the UI thread) will get unblocked, and we can initialize this +// window. +void WindowThread::Microwave( + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + winrt::Microsoft::Terminal::Remoting::Peasant peasant) +{ + _peasant = std::move(peasant); + _args = std::move(args); + + _host = std::make_unique<::AppHost>(_appLogic, + _args, + _manager, + _peasant, + std::move(_warmWindow)); + + // raise the signal to unblock KeepWarm and start the window message loop again. + _microwaveBuzzer.notify_one(); +} + winrt::TerminalApp::TerminalWindow WindowThread::Logic() { return _host->Logic(); diff --git a/src/cascadia/WindowsTerminal/WindowThread.h b/src/cascadia/WindowsTerminal/WindowThread.h index 8699a791092..0e8bc5429d1 100644 --- a/src/cascadia/WindowsTerminal/WindowThread.h +++ b/src/cascadia/WindowsTerminal/WindowThread.h @@ -17,6 +17,13 @@ class WindowThread : public std::enable_shared_from_this int RunMessagePump(); void RundownForExit(); + bool KeepWarm(); + void Refrigerate(); + void Microwave( + winrt::Microsoft::Terminal::Remoting::WindowRequestedArgs args, + winrt::Microsoft::Terminal::Remoting::Peasant peasant); + void ThrowAway(); + uint64_t PeasantID(); WINRT_CALLBACK(UpdateSettingsRequested, winrt::delegate); @@ -29,6 +36,12 @@ class WindowThread : public std::enable_shared_from_this winrt::Microsoft::Terminal::Remoting::WindowManager _manager{ nullptr }; std::unique_ptr<::AppHost> _host{ nullptr }; + winrt::event_token _UpdateSettingsRequestedToken; + + std::unique_ptr<::IslandWindow> _warmWindow{ nullptr }; + std::mutex _microwave; + std::condition_variable _microwaveBuzzer; int _messagePump(); + void _pumpRemainingXamlMessages(); }; diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp index b8ce375f1ea..381e07b243c 100644 --- a/src/types/inc/utils.hpp +++ b/src/types/inc/utils.hpp @@ -115,4 +115,6 @@ namespace Microsoft::Console::Utils // Same deal, but in TerminalPage::_evaluatePathForCwd std::wstring EvaluateStartingDirectory(std::wstring_view cwd, std::wstring_view startingDirectory); + bool IsWindows11() noexcept; + } diff --git a/src/types/utils.cpp b/src/types/utils.cpp index 5207d87a91b..91fd21e2633 100644 --- a/src/types/utils.cpp +++ b/src/types/utils.cpp @@ -852,3 +852,22 @@ std::wstring Utils::EvaluateStartingDirectory( } return resultPath; } + +bool Utils::IsWindows11() noexcept +{ + static const bool isWindows11 = []() noexcept { + OSVERSIONINFOEXW osver{}; + osver.dwOSVersionInfoSize = sizeof(osver); + osver.dwBuildNumber = 22000; + + DWORDLONG dwlConditionMask = 0; + VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL); + + if (VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE) + { + return true; + } + return false; + }(); + return isWindows11; +}