From 5dfc021d8ed24de749504713a2ad8a4bd0903a79 Mon Sep 17 00:00:00 2001 From: greg904 <56923875+greg904@users.noreply.github.com> Date: Tue, 5 Nov 2019 00:45:40 +0100 Subject: [PATCH] Use standard 1px window borders on NC Island Window (#3394) We take the standard window frame except that we remove the top part (see `NonClientIslandWindow::_OnNcCalcSize`), then we put little 1 pixel wide top border back in the client area using `DwmExtendFrameIntoClientArea` and then we put the XAML island and the drag bar on top. Most of this PR is comments to explain how the code works and also removing complex code that was needed to handle the weird cases when the borders were custom. I've also refactored a little bit the `NonClientIslandWindow` class. * Fix DwmExtendFrameIntoClientArea values * Fix WM_NCHITTEST handling * Position the XAML island window correctly * Fix weird colors in drag bar and hide old title bar buttons * Fix the window's position when maximized * Add support for dark theme on the frame * DRY shared code between conhost and new terminal * Fix drag bar and remove dead code * Remove dead code and use cached DPI * Refactor code * Remove impossible TODO * Use system metrics instead of hardcoding resize border height * Use theme from app settings instead of system theme. Improve comments. Remove unused DWM frame on maximize. * Fix initial position DPI handling bug and apply review changes * Fix thick borders with DPI > 96 Closes #3064. Closes #1307. Closes #3136. Closes #1897. Closes #3222. Closes #1859. --- src/cascadia/TerminalApp/App.cpp | 14 +- src/cascadia/TerminalApp/App.h | 1 + src/cascadia/TerminalApp/App.idl | 1 + src/cascadia/WindowsTerminal/AppHost.cpp | 55 +- src/cascadia/WindowsTerminal/BaseWindow.h | 18 +- src/cascadia/WindowsTerminal/IslandWindow.cpp | 11 +- src/cascadia/WindowsTerminal/IslandWindow.h | 4 +- .../WindowsTerminal/NonClientIslandWindow.cpp | 883 +++++++----------- .../WindowsTerminal/NonClientIslandWindow.h | 42 +- .../WindowsTerminal/WindowsTerminal.vcxproj | 2 +- src/types/ThemeUtils.cpp | 19 + src/types/inc/ThemeUtils.h | 8 + src/types/lib/types.vcxproj | 6 +- src/types/lib/types.vcxproj.filters | 23 +- src/types/sources.inc | 1 + 15 files changed, 438 insertions(+), 650 deletions(-) create mode 100644 src/types/ThemeUtils.cpp create mode 100644 src/types/inc/ThemeUtils.h diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp index 72b6ae38821..3126dfbff3c 100644 --- a/src/cascadia/TerminalApp/App.cpp +++ b/src/cascadia/TerminalApp/App.cpp @@ -335,7 +335,8 @@ namespace winrt::TerminalApp::implementation TerminalSettings settings = _settings->MakeSettings(std::nullopt); // TODO MSFT:21150597 - If the global setting "Always show tab bar" is - // set, then we'll need to add the height of the tab bar here. + // set or if "Show tabs in title bar" is set, then we'll need to add + // the height of the tab bar here. return TermControl::GetProposedDimensions(settings, dpi); } @@ -394,6 +395,17 @@ namespace winrt::TerminalApp::implementation return point; } + winrt::Windows::UI::Xaml::ElementTheme App::GetRequestedTheme() + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + + return _settings->GlobalSettings().GetRequestedTheme(); + } + bool App::GetShowTabsInTitlebar() { if (!_loadedInitialSettings) diff --git a/src/cascadia/TerminalApp/App.h b/src/cascadia/TerminalApp/App.h index 770e810811e..eb0db0dd785 100644 --- a/src/cascadia/TerminalApp/App.h +++ b/src/cascadia/TerminalApp/App.h @@ -32,6 +32,7 @@ namespace winrt::TerminalApp::implementation Windows::Foundation::Point GetLaunchDimensions(uint32_t dpi); winrt::Windows::Foundation::Point GetLaunchInitialPositions(int32_t defaultInitialX, int32_t defaultInitialY); + winrt::Windows::UI::Xaml::ElementTheme GetRequestedTheme(); LaunchMode GetLaunchMode(); bool GetShowTabsInTitlebar(); diff --git a/src/cascadia/TerminalApp/App.idl b/src/cascadia/TerminalApp/App.idl index c724191ac36..b65d419a41f 100644 --- a/src/cascadia/TerminalApp/App.idl +++ b/src/cascadia/TerminalApp/App.idl @@ -31,6 +31,7 @@ namespace TerminalApp Windows.Foundation.Point GetLaunchDimensions(UInt32 dpi); Windows.Foundation.Point GetLaunchInitialPositions(Int32 defaultInitialX, Int32 defaultInitialY); + Windows.UI.Xaml.ElementTheme GetRequestedTheme(); LaunchMode GetLaunchMode(); Boolean GetShowTabsInTitlebar(); void TitlebarClicked(); diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 8985f23202a..960a7458980 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -22,7 +22,7 @@ AppHost::AppHost() noexcept : if (_useNonClientArea) { - _window = std::make_unique(); + _window = std::make_unique(_app.GetRequestedTheme()); } else { @@ -188,54 +188,25 @@ void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, winrt::Ter auto initialSize = _app.GetLaunchDimensions(dpix); - const short _currentWidth = Utils::ClampToShortMax( + const short islandWidth = Utils::ClampToShortMax( static_cast(ceil(initialSize.X)), 1); - const short _currentHeight = Utils::ClampToShortMax( + const short islandHeight = Utils::ClampToShortMax( static_cast(ceil(initialSize.Y)), 1); - // Create a RECT from our requested client size - auto nonClient = Viewport::FromDimensions({ _currentWidth, - _currentHeight }) - .ToRect(); + RECT islandFrame = {}; + bool succeeded = AdjustWindowRectExForDpi(&islandFrame, WS_OVERLAPPEDWINDOW, false, 0, dpix); + // If we failed to get the correct window size for whatever reason, log + // the error and go on. We'll use whatever the control proposed as the + // size of our window, which will be at least close. + LOG_LAST_ERROR_IF(!succeeded); - // Get the size of a window we'd need to host that client rect. This will - // add the titlebar space. if (_useNonClientArea) { - // If we're in NC tabs mode, do the math ourselves. Get the margins - // we're using for the window - this will include the size of the - // titlebar content. - const auto pNcWindow = static_cast(_window.get()); - const MARGINS margins = pNcWindow->GetFrameMargins(); - nonClient.left = 0; - nonClient.top = 0; - nonClient.right = margins.cxLeftWidth + nonClient.right + margins.cxRightWidth; - nonClient.bottom = margins.cyTopHeight + nonClient.bottom + margins.cyBottomHeight; - } - else - { - bool succeeded = AdjustWindowRectExForDpi(&nonClient, WS_OVERLAPPEDWINDOW, false, 0, dpix); - if (!succeeded) - { - // If we failed to get the correct window size for whatever reason, log - // the error and go on. We'll use whatever the control proposed as the - // size of our window, which will be at least close. - LOG_LAST_ERROR(); - nonClient = Viewport::FromDimensions({ _currentWidth, - _currentHeight }) - .ToRect(); - } - - // For client island scenario, there is an invisible border of 8 pixels. - // We need to remove this border to guarantee the left edge of the window - // coincides with the screen - const auto pCWindow = static_cast(_window.get()); - const RECT frame = pCWindow->GetFrameBorderMargins(dpix); - proposedRect.left += frame.left; + islandFrame.top = -NonClientIslandWindow::topBorderVisibleHeight; } - adjustedHeight = nonClient.bottom - nonClient.top; - adjustedWidth = nonClient.right - nonClient.left; + adjustedWidth = -islandFrame.left + islandWidth + islandFrame.right; + adjustedHeight = -islandFrame.top + islandHeight + islandFrame.bottom; } const COORD origin{ gsl::narrow(proposedRect.left), @@ -294,5 +265,5 @@ void AppHost::_UpdateTitleBarContent(const winrt::Windows::Foundation::IInspecta // - void AppHost::_UpdateTheme(const winrt::TerminalApp::App&, const winrt::Windows::UI::Xaml::ElementTheme& arg) { - _window->UpdateTheme(arg); + _window->OnApplicationThemeChanged(arg); } diff --git a/src/cascadia/WindowsTerminal/BaseWindow.h b/src/cascadia/WindowsTerminal/BaseWindow.h index 25f3d6f41d8..899bc56b3bc 100644 --- a/src/cascadia/WindowsTerminal/BaseWindow.h +++ b/src/cascadia/WindowsTerminal/BaseWindow.h @@ -34,10 +34,8 @@ class BaseWindow WINRT_ASSERT(that); WINRT_ASSERT(!that->_window); that->_window = wil::unique_hwnd(window); - SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(that)); - EnableNonClientDpiScaling(window); - that->_currentDpi = GetDpiForWindow(window); + return that->_OnNcCreate(wparam, lparam); } else if (T* that = GetThisFromHandle(window)) { @@ -245,6 +243,20 @@ class BaseWindow std::wstring _title = L""; bool _minimized = false; + + // Method Description: + // - This method is called when the window receives the WM_NCCREATE message. + // Return Value: + // - The value returned from the window proc. + virtual [[nodiscard]] LRESULT _OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept + { + SetWindowLongPtr(_window.get(), GWLP_USERDATA, reinterpret_cast(this)); + + EnableNonClientDpiScaling(_window.get()); + _currentDpi = GetDpiForWindow(_window.get()); + + return DefWindowProc(_window.get(), WM_NCCREATE, wParam, lParam); + }; }; template diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index f546cc4dc0d..d87c92a46a3 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -243,15 +243,6 @@ IRawElementProviderSimple* IslandWindow::_GetUiaProvider() return _pUiaProvider; } -RECT IslandWindow::GetFrameBorderMargins(unsigned int currentDpi) -{ - const auto windowStyle = GetWindowStyle(_window.get()); - const auto targetStyle = windowStyle & ~WS_DLGFRAME; - RECT frame{}; - AdjustWindowRectExForDpi(&frame, targetStyle, false, GetWindowExStyle(_window.get()), currentDpi); - return frame; -} - // Method Description: // - Called when the window has been resized (or maximized) // Arguments: @@ -300,7 +291,7 @@ void IslandWindow::OnAppInitialized() // - arg: the ElementTheme to use as the new theme for the UI // Return Value: // - -void IslandWindow::UpdateTheme(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) +void IslandWindow::OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) { _rootGrid.RequestedTheme(requestedTheme); // Invalidate the window rect, so that we'll repaint any elements we're diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 374949f0c79..6493f0a9ce7 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -23,19 +23,17 @@ class IslandWindow : [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; IRawElementProviderSimple* _GetUiaProvider(); - RECT GetFrameBorderMargins(unsigned int currentDpi); void OnResize(const UINT width, const UINT height) override; void OnMinimize() override; void OnRestore() override; virtual void OnAppInitialized(); virtual void SetContent(winrt::Windows::UI::Xaml::UIElement content); + virtual void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); virtual void Initialize(); void SetCreateCallback(std::function pfn) noexcept; - void UpdateTheme(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); - #pragma region IUiaWindow void ChangeViewport(const SMALL_RECT /*NewWindow*/) { diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index 0908481d67b..be77c428026 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -5,6 +5,8 @@ ********************************************************/ #include "pch.h" #include "NonClientIslandWindow.h" +#include "../types/inc/ThemeUtils.h" +#include "../types/inc/utils.hpp" extern "C" IMAGE_DOS_HEADER __ImageBase; @@ -13,19 +15,13 @@ using namespace winrt::Windows::UI::Composition; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Hosting; using namespace winrt::Windows::Foundation::Numerics; +using namespace ::Microsoft::Console; using namespace ::Microsoft::Console::Types; -constexpr int RECT_WIDTH(const RECT* const pRect) -{ - return pRect->right - pRect->left; -} -constexpr int RECT_HEIGHT(const RECT* const pRect) -{ - return pRect->bottom - pRect->top; -} - -NonClientIslandWindow::NonClientIslandWindow() noexcept : +NonClientIslandWindow::NonClientIslandWindow(const ElementTheme& requestedTheme) noexcept : IslandWindow{}, + _backgroundBrushColor{ RGB(0, 0, 0) }, + _theme{ requestedTheme }, _isMaximized{ false } { } @@ -42,10 +38,10 @@ NonClientIslandWindow::~NonClientIslandWindow() // - // Return Value: // - -void NonClientIslandWindow::OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable /*sender*/, - winrt::Windows::UI::Xaml::SizeChangedEventArgs /*eventArgs*/) +void NonClientIslandWindow::_OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable /*sender*/, + winrt::Windows::UI::Xaml::SizeChangedEventArgs /*eventArgs*/) const { - _UpdateDragRegion(); + _UpdateIslandRegion(); } void NonClientIslandWindow::OnAppInitialized() @@ -57,6 +53,8 @@ void NonClientIslandWindow::Initialize() { IslandWindow::Initialize(); + THROW_IF_FAILED(_UpdateFrameMargins()); + // Set up our grid of content. We'll use _rootGrid as our root element. // There will be two children of this grid - the TitlebarControl, and the // "client content" @@ -72,8 +70,8 @@ void NonClientIslandWindow::Initialize() _titlebar = winrt::TerminalApp::TitlebarControl{ reinterpret_cast(GetHandle()) }; _dragBar = _titlebar.DragBar(); - _dragBar.SizeChanged({ this, &NonClientIslandWindow::OnDragBarSizeChanged }); - _rootGrid.SizeChanged({ this, &NonClientIslandWindow::OnDragBarSizeChanged }); + _dragBar.SizeChanged({ this, &NonClientIslandWindow::_OnDragBarSizeChanged }); + _rootGrid.SizeChanged({ this, &NonClientIslandWindow::_OnDragBarSizeChanged }); _rootGrid.Children().Append(_titlebar); @@ -113,7 +111,23 @@ void NonClientIslandWindow::SetTitlebarContent(winrt::Windows::UI::Xaml::UIEleme _titlebar.Content(content); } -RECT NonClientIslandWindow::GetDragAreaRect() const noexcept +// Method Description: +// - This method computes the height of the little border above the title bar +// and returns it. If the border is disabled, then this method will return 0. +// Return Value: +// - the height of the border above the title bar or 0 if it's disabled +int NonClientIslandWindow::_GetTopBorderHeight() const noexcept +{ + if (_isMaximized) + { + // no border when maximized + return 0; + } + + return topBorderVisibleHeight; +} + +RECT NonClientIslandWindow::_GetDragAreaRect() const noexcept { if (_dragBar) { @@ -139,56 +153,93 @@ RECT NonClientIslandWindow::GetDragAreaRect() const noexcept } // Method Description: -// - called when the size of the window changes for any reason. Updates the -// sizes of our child Xaml Islands to match our new sizing. +// - Called when the size of the window changes for any reason. Updates the +// XAML island to match our new sizing and also updates the maximize icon +// if the window went from maximized to restored or the opposite. void NonClientIslandWindow::OnSize(const UINT width, const UINT height) { - if (!_interopWindowHandle) + _UpdateMaximizedState(); + + if (_interopWindowHandle) { - return; + _UpdateIslandPosition(width, height); } +} - const auto scale = GetCurrentDpiScale(); - const auto dpi = ::GetDpiForWindow(_window.get()); - - const auto dragY = ::GetSystemMetricsForDpi(SM_CYDRAG, dpi); - const auto dragX = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi); - - // If we're maximized, we don't want to use the frame as our margins, - // instead we want to use the margins from the maximization. If we included - // the left&right sides of the frame in this calculation while maximized, - // you' have a few pixels of the window border on the sides while maximized, - // which most apps do not have. - const auto bordersWidth = _isMaximized ? - (_maximizedMargins.cxLeftWidth + _maximizedMargins.cxRightWidth) : - (dragX * 2); - const auto bordersHeight = _isMaximized ? - (_maximizedMargins.cyBottomHeight + _maximizedMargins.cyTopHeight) : - (dragY * 2); - - const auto windowsWidth = width - bordersWidth; - const auto windowsHeight = height - bordersHeight; - const auto xPos = _isMaximized ? _maximizedMargins.cxLeftWidth : dragX; - const auto yPos = _isMaximized ? _maximizedMargins.cyTopHeight : dragY; - - if (_rootGrid) +// Method Description: +// - Checks if the window has been maximized or restored since the last time. +// If it has been maximized or restored, then it updates the _isMaximized +// flags and notifies of the change by calling +// NonClientIslandWindow::_OnMaximizeChange. +void NonClientIslandWindow::_UpdateMaximizedState() +{ + const auto windowStyle = GetWindowStyle(_window.get()); + const auto newIsMaximized = WI_IsFlagSet(windowStyle, WS_MAXIMIZE); + + if (_isMaximized != newIsMaximized) { - winrt::Windows::Foundation::Size size{ (windowsWidth / scale) + 0.5f, (windowsHeight / scale) + 0.5f }; - _rootGrid.Height(size.Height); - _rootGrid.Width(size.Width); - _rootGrid.Measure(size); - winrt::Windows::Foundation::Rect finalRect{}; - _rootGrid.Arrange(finalRect); + _isMaximized = newIsMaximized; + _OnMaximizeChange(); } +} - // I'm not sure that HWND_BOTTOM does anything differnet than HWND_TOP for us. +// Method Description: +// - Called when the the windows goes from restored to maximized or from +// maximized to restored. Updates the maximize button's icon and the frame +// margins. +void NonClientIslandWindow::_OnMaximizeChange() noexcept +{ + if (_titlebar) + { + const auto windowStyle = GetWindowStyle(_window.get()); + const auto isIconified = WI_IsFlagSet(windowStyle, WS_ICONIC); + + const auto state = _isMaximized ? winrt::TerminalApp::WindowVisualState::WindowVisualStateMaximized : + isIconified ? winrt::TerminalApp::WindowVisualState::WindowVisualStateIconified : + winrt::TerminalApp::WindowVisualState::WindowVisualStateNormal; + + try + { + _titlebar.SetWindowVisualState(state); + } + CATCH_LOG(); + } + + // no frame margin when maximized + THROW_IF_FAILED(_UpdateFrameMargins()); +} + +// Method Description: +// - Called when the size of the window changes for any reason. Updates the +// sizes of our child XAML Islands to match our new sizing. +void NonClientIslandWindow::_UpdateIslandPosition(const UINT windowWidth, const UINT windowHeight) +{ + const auto topBorderHeight = Utils::ClampToShortMax(_GetTopBorderHeight(), 0); + + const COORD newIslandPos = { 0, topBorderHeight }; + + // I'm not sure that HWND_BOTTOM does anything different than HWND_TOP for us. winrt::check_bool(SetWindowPos(_interopWindowHandle, HWND_BOTTOM, - xPos, - yPos, - windowsWidth, - windowsHeight, + newIslandPos.X, + newIslandPos.Y, + windowWidth, + windowHeight - topBorderHeight, SWP_SHOWWINDOW)); + + // This happens when we go from maximized to restored or the opposite + // because topBorderHeight changes. + if (!_oldIslandPos.has_value() || _oldIslandPos.value() != newIslandPos) + { + // The drag bar's position changed compared to the client area because + // the island moved but we will not be notified about this in the + // NonClientIslandWindow::OnDragBarSizeChanged method because this + // method is only called when the position of the drag bar changes + // **inside** the island which is not the case here. + _UpdateIslandRegion(); + + _oldIslandPos = { newIslandPos }; + } } // Method Description: @@ -202,142 +253,125 @@ void NonClientIslandWindow::OnSize(const UINT width, const UINT height) // - // Return Value: // - -void NonClientIslandWindow::_UpdateDragRegion() +void NonClientIslandWindow::_UpdateIslandRegion() const { - if (_dragBar) + if (!_interopWindowHandle || !_dragBar) { - // TODO:GH#1897 This is largely duplicated from OnSize, and we should do - // better than that. - const auto windowRect = GetWindowRect(); - const auto width = windowRect.right - windowRect.left; - const auto height = windowRect.bottom - windowRect.top; - - const auto scale = GetCurrentDpiScale(); - const auto dpi = ::GetDpiForWindow(_window.get()); - - const auto dragY = ::GetSystemMetricsForDpi(SM_CYDRAG, dpi); - const auto dragX = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi); - - // If we're maximized, we don't want to use the frame as our margins, - // instead we want to use the margins from the maximization. If we included - // the left&right sides of the frame in this calculation while maximized, - // you' have a few pixels of the window border on the sides while maximized, - // which most apps do not have. - const auto bordersWidth = _isMaximized ? - (_maximizedMargins.cxLeftWidth + _maximizedMargins.cxRightWidth) : - (dragX * 2); - const auto bordersHeight = _isMaximized ? - (_maximizedMargins.cyBottomHeight + _maximizedMargins.cyTopHeight) : - (dragY * 2); - - const auto windowsWidth = width - bordersWidth; - const auto windowsHeight = height - bordersHeight; - const auto xPos = _isMaximized ? _maximizedMargins.cxLeftWidth : dragX; - const auto yPos = _isMaximized ? _maximizedMargins.cyTopHeight : dragY; - - const auto dragBarRect = GetDragAreaRect(); - const auto nonClientHeight = dragBarRect.bottom - dragBarRect.top; - - auto nonClientRegion = wil::unique_hrgn(CreateRectRgn(0, 0, 0, 0)); - auto nonClientLeftRegion = wil::unique_hrgn(CreateRectRgn(0, 0, dragBarRect.left, nonClientHeight)); - auto nonClientRightRegion = wil::unique_hrgn(CreateRectRgn(dragBarRect.right, 0, windowsWidth, nonClientHeight)); - winrt::check_bool(CombineRgn(nonClientRegion.get(), nonClientLeftRegion.get(), nonClientRightRegion.get(), RGN_OR)); - - _dragBarRegion = wil::unique_hrgn(CreateRectRgn(0, 0, 0, 0)); - auto clientRegion = wil::unique_hrgn(CreateRectRgn(0, nonClientHeight, windowsWidth, windowsHeight)); - winrt::check_bool(CombineRgn(_dragBarRegion.get(), nonClientRegion.get(), clientRegion.get(), RGN_OR)); - winrt::check_bool(SetWindowRgn(_interopWindowHandle, _dragBarRegion.get(), true)); + return; } + + RECT rcIsland; + winrt::check_bool(::GetWindowRect(_interopWindowHandle, &rcIsland)); + const auto islandWidth = rcIsland.right - rcIsland.left; + const auto islandHeight = rcIsland.bottom - rcIsland.top; + const auto totalRegion = wil::unique_hrgn(CreateRectRgn(0, 0, islandWidth, islandHeight)); + + const auto rcDragBar = _GetDragAreaRect(); + const auto dragBarRegion = wil::unique_hrgn(CreateRectRgn(rcDragBar.left, rcDragBar.top, rcDragBar.right, rcDragBar.bottom)); + + // island region = total region - drag bar region + const auto islandRegion = wil::unique_hrgn(CreateRectRgn(0, 0, 0, 0)); + winrt::check_bool(CombineRgn(islandRegion.get(), totalRegion.get(), dragBarRegion.get(), RGN_DIFF)); + + winrt::check_bool(SetWindowRgn(_interopWindowHandle, islandRegion.get(), true)); } // Method Description: -// Hit test the frame for resizing and moving. -// Method Description: -// - Hit test the frame for resizing and moving. -// Arguments: -// - ptMouse: the mouse point being tested, in absolute (NOT WINDOW) coordinates. +// - Returns the height of the little space at the top of the window used to +// resize the window. // Return Value: -// - one of the values from -// https://docs.microsoft.com/en-us/windows/desktop/inputdev/wm-nchittest#return-value -// corresponding to the area of the window that was hit -// NOTE: -// - Largely taken from code on: -// https://docs.microsoft.com/en-us/windows/desktop/dwm/customframe -[[nodiscard]] LRESULT NonClientIslandWindow::HitTestNCA(POINT ptMouse) const noexcept +// - the height of the window's top resize handle +int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept { - // Get the window rectangle. - RECT rcWindow = BaseWindow::GetWindowRect(); + // there isn't a SM_CYPADDEDBORDER for the Y axis + return ::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, _currentDpi) + + ::GetSystemMetricsForDpi(SM_CYSIZEFRAME, _currentDpi); +} - MARGINS margins = GetFrameMargins(); +// Method Description: +// - Responds to the WM_NCCALCSIZE message by calculating and creating the new +// window frame. +[[nodiscard]] LRESULT NonClientIslandWindow::_OnNcCalcSize(const WPARAM wParam, const LPARAM lParam) noexcept +{ + if (wParam == false) + { + return 0; + } - // Get the frame rectangle, adjusted for the style without a caption. - RECT rcFrame = { 0 }; - auto expectedStyle = WS_OVERLAPPEDWINDOW; - WI_ClearAllFlags(expectedStyle, WS_CAPTION); - AdjustWindowRectEx(&rcFrame, expectedStyle, false, 0); + NCCALCSIZE_PARAMS* params = reinterpret_cast(lParam); - // Determine if the hit test is for resizing. Default middle (1,1). - unsigned short uRow = 1; - unsigned short uCol = 1; - bool fOnResizeBorder = false; + // Store the original top before the default window proc applies the + // default frame. + const auto originalTop = params->rgrc[0].top; - // Determine if the point is at the top or bottom of the window. - if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + margins.cyTopHeight) + // apply the default frame + auto ret = DefWindowProc(_window.get(), WM_NCCALCSIZE, wParam, lParam); + if (ret != 0) { - fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top)); - uRow = 0; - } - else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - margins.cyBottomHeight) - { - uRow = 2; + return ret; } - // Determine if the point is at the left or right of the window. - if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + margins.cxLeftWidth) - { - uCol = 0; // left side - } - else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - margins.cxRightWidth) + auto newTop = originalTop; + + // WM_NCCALCSIZE is called before WM_SIZE + _UpdateMaximizedState(); + + if (_isMaximized) { - uCol = 2; // right side + // When a window is maximized, its size is actually a little bit more + // than the monitor's work area. The window is positioned and sized in + // such a way that the resize handles are outside of the monitor and + // then the window is clipped to the monitor so that the resize handle + // do not appear because you don't need them (because you can't resize + // a window when it's maximized unless you restore it). + newTop += _GetResizeHandleHeight(); } - // clang-format off - // Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT) - const auto topHt = fOnResizeBorder ? HTTOP : HTCAPTION; - LRESULT hitTests[3][3] = { - { HTTOPLEFT, topHt, HTTOPRIGHT }, - { HTLEFT, HTNOWHERE, HTRIGHT }, - { HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT }, - }; - // clang-format on - - return hitTests[uRow][uCol]; + // only modify the top of the frame to remove the title bar + params->rgrc[0].top = newTop; + + return 0; } // Method Description: -// - Get the size of the borders we want to use. The sides and bottom will just -// be big enough for resizing, but the top will be as big as we need for the -// non-client content. +// - Hit test the frame for resizing and moving. +// Arguments: +// - ptMouse: the mouse point being tested, in absolute (NOT WINDOW) coordinates. // Return Value: -// - A MARGINS struct containing the border dimensions we want. -MARGINS NonClientIslandWindow::GetFrameMargins() const noexcept +// - one of the values from +// https://docs.microsoft.com/en-us/windows/desktop/inputdev/wm-nchittest#return-value +// corresponding to the area of the window that was hit +[[nodiscard]] LRESULT NonClientIslandWindow::_OnNcHitTest(POINT ptMouse) const noexcept { - const auto scale = GetCurrentDpiScale(); - const auto dpi = ::GetDpiForWindow(_window.get()); - const auto windowMarginSides = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi); - const auto windowMarginBottom = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi); + // This will handle the left, right and bottom parts of the frame because + // we didn't change them. + LPARAM lParam = MAKELONG(ptMouse.x, ptMouse.y); + const auto originalRet = DefWindowProc(_window.get(), WM_NCHITTEST, 0, lParam); + if (originalRet != HTCLIENT) + { + return originalRet; + } - const auto dragBarRect = GetDragAreaRect(); - const auto nonClientHeight = dragBarRect.bottom - dragBarRect.top; + // At this point, we know that the cursor is inside the client area so it + // has to be either the little border at the top of our custom title bar, + // the drag bar or something else in the XAML island. But the XAML Island + // handles WM_NCHITTEST on its own so actually it cannot be the XAML + // Island. Then it must be the drag bar or the little border at the top + // which the user can use to move or resize the window. - MARGINS margins{ 0 }; - margins.cxLeftWidth = windowMarginSides; - margins.cxRightWidth = windowMarginSides; - margins.cyBottomHeight = windowMarginBottom; - margins.cyTopHeight = nonClientHeight + windowMarginBottom; + RECT rcWindow; + winrt::check_bool(::GetWindowRect(_window.get(), &rcWindow)); - return margins; + const auto resizeBorderHeight = _GetResizeHandleHeight(); + const auto isOnResizeBorder = ptMouse.y < rcWindow.top + resizeBorderHeight; + + // the top of the drag bar is used to resize the window + if (!_isMaximized && isOnResizeBorder) + { + return HTTOP; + } + + return HTCAPTION; } // Method Description: @@ -348,104 +382,33 @@ MARGINS NonClientIslandWindow::GetFrameMargins() const noexcept // - the HRESULT returned by DwmExtendFrameIntoClientArea. [[nodiscard]] HRESULT NonClientIslandWindow::_UpdateFrameMargins() const noexcept { - // Set frame margins with just a single pixel on the bottom. We don't - // really want a window frame at all - we're drawing all of it. We - // especially don't want a top margin - that's where the caption buttons - // are, and we're drawing those. So just set a single pixel on the bottom, - // because the method won't work with {0}. - MARGINS margins = { 0, 0, 0, 1 }; - - // Extend the frame into the client area. - return DwmExtendFrameIntoClientArea(_window.get(), &margins); -} - -// Routine Description: -// - Gets the maximum possible window rectangle in pixels. Based on the monitor -// the window is on or the primary monitor if no window exists yet. -// Arguments: -// - prcSuggested - If we were given a suggested rectangle for where the window -// is going, we can pass it in here to find out the max size -// on that monitor. -// - If this value is zero and we had a valid window handle, -// we'll use that instead. Otherwise the value of 0 will make -// us use the primary monitor. -// - pDpiSuggested - The dpi that matches the suggested rect. We will attempt to -// compute this during the function, but if we fail for some -// reason, the original value passed in will be left untouched. -// Return Value: -// - RECT containing the left, right, top, and bottom positions from the desktop -// origin in pixels. Measures the outer edges of the potential window. -// NOTE: -// Heavily taken from WindowMetrics::GetMaxWindowRectInPixels in conhost. -RECT NonClientIslandWindow::GetMaxWindowRectInPixels(const RECT* const prcSuggested, _Out_opt_ UINT* pDpiSuggested) -{ - // prepare rectangle - RECT rc = *prcSuggested; - - // use zero rect to compare. - RECT rcZero; - SetRectEmpty(&rcZero); - - // First get the monitor pointer from either the active window or the default location (0,0,0,0) - HMONITOR hMonitor = nullptr; - - // NOTE: We must use the nearest monitor because sometimes the system moves - // the window around into strange spots while performing snap and Win+D - // operations. Those operations won't work correctly if we use - // MONITOR_DEFAULTTOPRIMARY. - if (!EqualRect(&rc, &rcZero)) - { - // For invalid window handles or when we were passed a non-zero - // suggestion rectangle, get the monitor from the rect. - hMonitor = MonitorFromRect(&rc, MONITOR_DEFAULTTONEAREST); - } - else - { - // Otherwise, get the monitor from the window handle. - hMonitor = MonitorFromWindow(_window.get(), MONITOR_DEFAULTTONEAREST); - } - - // If for whatever reason there is no monitor, we're going to give back - // whatever we got since we can't figure anything out. We won't adjust the - // DPI either. That's OK. DPI doesn't make much sense with no display. - if (nullptr == hMonitor) - { - return rc; - } - - // Now obtain the monitor pixel dimensions - MONITORINFO MonitorInfo = { 0 }; - MonitorInfo.cbSize = sizeof(MONITORINFO); + MARGINS margins = {}; - GetMonitorInfoW(hMonitor, &MonitorInfo); - - // We have to make a correction to the work area. If we actually consume the - // entire work area (by maximizing the window). The window manager will - // render the borders off-screen. We need to pad the work rectangle with the - // border dimensions to represent the actual max outer edges of the window - // rect. - WINDOWINFO wi = { 0 }; - wi.cbSize = sizeof(WINDOWINFO); - GetWindowInfo(_window.get(), &wi); - - // In non-full screen, we want to only use the work area (avoiding the task bar space) - rc = MonitorInfo.rcWork; - - if (pDpiSuggested != nullptr) + if (_GetTopBorderHeight() != 0) { - UINT monitorDpiX; - UINT monitorDpiY; - if (SUCCEEDED(GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &monitorDpiX, &monitorDpiY))) - { - *pDpiSuggested = monitorDpiX; - } - else - { - *pDpiSuggested = GetDpiForWindow(_window.get()); - } + RECT frame = {}; + winrt::check_bool(::AdjustWindowRectExForDpi(&frame, GetWindowStyle(_window.get()), FALSE, 0, _currentDpi)); + + // We removed the whole top part of the frame (see handling of + // WM_NCCALCSIZE) so the top border is missing now. We add it back here. + // Note #1: You might wonder why we don't remove just the title bar instead + // of removing the whole top part of the frame and then adding the little + // top border back. I tried to do this but it didn't work: DWM drew the + // whole title bar anyways on top of the window. It seems that DWM only + // wants to draw either nothing or the whole top part of the frame. + // Note #2: For some reason if you try to set the top margin to just the + // top border height (what we want to do), then there is a transparency + // bug when the window is inactive, so I've decided to add the whole top + // part of the frame instead and then we will hide everything that we + // don't need (that is, the whole thing but the little 1 pixel wide border + // at the top) in the WM_PAINT handler. This eliminates the transparency + // bug and it's what a lot of Win32 apps that customize the title bar do + // so it should work fine. + margins.cyTopHeight = -frame.top; } - return rc; + // Extend the frame into the client area. + return DwmExtendFrameIntoClientArea(_window.get(), &margins); } // Method Description: @@ -461,342 +424,144 @@ RECT NonClientIslandWindow::GetMaxWindowRectInPixels(const RECT* const prcSugges WPARAM const wParam, LPARAM const lParam) noexcept { - LRESULT lRet = 0; - - // First call DwmDefWindowProc. This might handle things like the - // min/max/close buttons for us. - const bool dwmHandledMessage = DwmDefWindowProc(_window.get(), message, wParam, lParam, &lRet); - switch (message) { - case WM_ACTIVATE: - { - _HandleActivateWindow(); - break; - } case WM_NCCALCSIZE: - { - if (wParam == false) - { - return 0; - } - // Handle the non-client size message. - if (wParam == TRUE && lParam) - { - // Calculate new NCCALCSIZE_PARAMS based on custom NCA inset. - NCCALCSIZE_PARAMS* pncsp = reinterpret_cast(lParam); - - pncsp->rgrc[0].left = pncsp->rgrc[0].left + 0; - pncsp->rgrc[0].top = pncsp->rgrc[0].top + 0; - pncsp->rgrc[0].right = pncsp->rgrc[0].right - 0; - pncsp->rgrc[0].bottom = pncsp->rgrc[0].bottom - 0; - - return 0; - } - break; - } + return _OnNcCalcSize(wParam, lParam); case WM_NCHITTEST: - { - if (dwmHandledMessage) - { - return lRet; - } - - // Handle hit testing in the NCA if not handled by DwmDefWindowProc. - if (lRet == 0) - { - lRet = HitTestNCA({ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }); - if (lRet != HTNOWHERE) - { - return lRet; - } - } - break; + return _OnNcHitTest({ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }); + case WM_PAINT: + return _OnPaint(); } - case WM_EXITSIZEMOVE: + return IslandWindow::MessageHandler(message, wParam, lParam); +} + +// Method Description: +// - This method is called when the window receives the WM_PAINT message. It +// paints the background of the window to the color of the drag bar because +// the drag bar cannot be painted on the window by the XAML Island (see +// NonClientIslandWindow::_UpdateIslandRegion). +// Return Value: +// - The value returned from the window proc. +[[nodiscard]] LRESULT NonClientIslandWindow::_OnPaint() noexcept +{ + if (!_titlebar) { - ForceResize(); - break; + return 0; } - case WM_PAINT: + PAINTSTRUCT ps{ 0 }; + const auto hdc = wil::BeginPaint(_window.get(), &ps); + if (!hdc) { - if (!_dragBar) - { - return 0; - } - - PAINTSTRUCT ps{ 0 }; - const auto hdc = wil::BeginPaint(_window.get(), &ps); - if (hdc.get()) - { - const auto scale = GetCurrentDpiScale(); - const auto dpi = ::GetDpiForWindow(_window.get()); - // Get the dimensions of the drag borders for the sides of the window. - const auto dragY = ::GetSystemMetricsForDpi(SM_CYDRAG, dpi); - const auto dragX = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi); - const auto xPos = _isMaximized ? _maximizedMargins.cxLeftWidth : dragX; - const auto yPos = _isMaximized ? _maximizedMargins.cyTopHeight : dragY; - - // Create brush for borders, titlebar color. - const auto backgroundBrush = _titlebar.Background(); - const auto backgroundSolidBrush = backgroundBrush.as(); - const auto backgroundColor = backgroundSolidBrush.Color(); - const auto color = RGB(backgroundColor.R, backgroundColor.G, backgroundColor.B); - _backgroundBrush = wil::unique_hbrush(CreateSolidBrush(color)); - - RECT windowRect = {}; - ::GetWindowRect(_window.get(), &windowRect); - const auto cx = windowRect.right - windowRect.left; - const auto cy = windowRect.bottom - windowRect.top; - - // Fill in ONLY the titlebar area. If we paint the _entirety_ of the - // window rect here, the single pixel of the bottom border (set in - // _UpdateFrameMargins) will be drawn, and blend with whatever the - // border color is. - RECT dragBarRect = GetDragAreaRect(); - const auto dragHeight = RECT_HEIGHT(&dragBarRect); - dragBarRect.left = 0; - dragBarRect.right = cx; - dragBarRect.top = 0; - dragBarRect.bottom = dragHeight + yPos; - ::FillRect(hdc.get(), &dragBarRect, _backgroundBrush.get()); - - // Draw the top window border - RECT clientRect = { 0, 0, cx, yPos }; - ::FillRect(hdc.get(), &clientRect, _backgroundBrush.get()); - - // Draw the left window border - clientRect = { 0, 0, xPos, cy }; - ::FillRect(hdc.get(), &clientRect, _backgroundBrush.get()); - - // Draw the bottom window border - clientRect = { 0, cy - yPos, cx, cy }; - ::FillRect(hdc.get(), &clientRect, _backgroundBrush.get()); - - // Draw the right window border - clientRect = { cx - xPos, 0, cx, cy }; - ::FillRect(hdc.get(), &clientRect, _backgroundBrush.get()); - } - return 0; } - case WM_NCLBUTTONDOWN: - case WM_NCLBUTTONUP: - case WM_NCMBUTTONDOWN: - case WM_NCMBUTTONUP: - case WM_NCRBUTTONDOWN: - case WM_NCRBUTTONUP: - case WM_NCXBUTTONDOWN: - case WM_NCXBUTTONUP: + const auto topBorderHeight = _GetTopBorderHeight(); + + if (ps.rcPaint.top < topBorderHeight) { - // If we clicked in the titlebar, raise an event so the app host can - // dispatch an appropriate event. - _DragRegionClickedHandlers(); - break; + RECT rcTopBorder = ps.rcPaint; + rcTopBorder.bottom = topBorderHeight; + + // To show the original top border, we have to paint on top of it with + // the alpha component set to 0. This page recommends to paint the area + // in black using the stock BLACK_BRUSH to do this: + // https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#extending-the-client-frame + ::FillRect(hdc.get(), &rcTopBorder, GetStockBrush(BLACK_BRUSH)); } - case WM_WINDOWPOSCHANGING: + if (ps.rcPaint.bottom > topBorderHeight) { - // Enforce maximum size here instead of WM_GETMINMAXINFO. If we return - // it in WM_GETMINMAXINFO, then it will be enforced when snapping across - // DPI boundaries (bad.) - LPWINDOWPOS lpwpos = reinterpret_cast(lParam); - if (lpwpos == nullptr) - { - break; - } - if (_HandleWindowPosChanging(lpwpos)) + RECT rcRest = ps.rcPaint; + rcRest.top = topBorderHeight; + + const auto backgroundBrush = _titlebar.Background(); + const auto backgroundSolidBrush = backgroundBrush.as(); + const auto backgroundColor = backgroundSolidBrush.Color(); + const auto color = RGB(backgroundColor.R, backgroundColor.G, backgroundColor.B); + + if (!_backgroundBrush || color != _backgroundBrushColor) { - return 0; + // Create brush for titlebar color. + _backgroundBrush = wil::unique_hbrush(CreateSolidBrush(color)); } - else + + // To hide the original title bar, we have to paint on top of it with + // the alpha component set to 255. This is a hack to do it with GDI. + // See NonClientIslandWindow::_UpdateFrameMargins for more information. + HDC opaqueDc; + BP_PAINTPARAMS params = { sizeof(params), BPPF_NOCLIP | BPPF_ERASE }; + HPAINTBUFFER buf = BeginBufferedPaint(hdc.get(), &rcRest, BPBF_TOPDOWNDIB, ¶ms, &opaqueDc); + if (!buf || !opaqueDc) { - break; + winrt::throw_last_error(); } - } - case WM_DPICHANGED: - { - auto lprcNewScale = reinterpret_cast(lParam); - OnSize(RECT_WIDTH(lprcNewScale), RECT_HEIGHT(lprcNewScale)); - break; - } + ::FillRect(opaqueDc, &rcRest, _backgroundBrush.get()); + ::BufferedPaintSetAlpha(buf, NULL, 255); + ::EndBufferedPaint(buf, TRUE); } - return IslandWindow::MessageHandler(message, wParam, lParam); + return 0; } // Method Description: -// - Handle a WM_ACTIVATE message. Called during the creation of the window, and -// used as an opprotunity to get the dimensions of the caption buttons (the -// min, max, close buttons). We'll use these dimensions to help size the -// non-client area of the window. -void NonClientIslandWindow::_HandleActivateWindow() -{ - THROW_IF_FAILED(_UpdateFrameMargins()); -} - -// Method Description: -// - Handle a WM_WINDOWPOSCHANGING message. When the window is changing, or the -// dpi is changing, this handler is triggered to give us a chance to adjust -// the window size and position manually. We use this handler during a maxiize -// to figure out by how much the window will overhang the edges of the -// monitor, and set up some padding to adjust for that. -// Arguments: -// - windowPos: A pointer to a proposed window location and size. Should we wish -// to manually position the window, we could change the values of this struct. +// - This method is called when the window receives the WM_NCCREATE message. // Return Value: -// - true if we handled this message, false otherwise. If we return false, the -// message should instead be handled by DefWindowProc -// Note: -// Largely taken from the conhost WM_WINDOWPOSCHANGING handler. -bool NonClientIslandWindow::_HandleWindowPosChanging(WINDOWPOS* const windowPos) +// - The value returned from the window proc. +[[nodiscard]] LRESULT NonClientIslandWindow::_OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept { - // We only need to apply restrictions if the size is changing. - if (WI_IsFlagSet(windowPos->flags, SWP_NOSIZE)) + const auto ret = IslandWindow::_OnNcCreate(wParam, lParam); + if (ret == FALSE) { - return false; + return ret; } - const auto windowStyle = GetWindowStyle(_window.get()); - const auto isMaximized = WI_IsFlagSet(windowStyle, WS_MAXIMIZE); - const auto isIconified = WI_IsFlagSet(windowStyle, WS_ICONIC); + // Set the frame's theme before it is rendered (WM_NCPAINT) so that it is + // rendered with the correct theme. + _UpdateFrameTheme(); - if (_titlebar) - { - _titlebar.SetWindowVisualState(isMaximized ? winrt::TerminalApp::WindowVisualState::WindowVisualStateMaximized : - isIconified ? winrt::TerminalApp::WindowVisualState::WindowVisualStateIconified : - winrt::TerminalApp::WindowVisualState::WindowVisualStateNormal); - } + return TRUE; +} - // Figure out the suggested dimensions - RECT rcSuggested; - rcSuggested.left = windowPos->x; - rcSuggested.top = windowPos->y; - rcSuggested.right = rcSuggested.left + windowPos->cx; - rcSuggested.bottom = rcSuggested.top + windowPos->cy; - SIZE szSuggested; - szSuggested.cx = RECT_WIDTH(&rcSuggested); - szSuggested.cy = RECT_HEIGHT(&rcSuggested); - - // Figure out the current dimensions for comparison. - RECT rcCurrent = GetWindowRect(); - - // Determine whether we're being resized by someone dragging the edge or - // completely moved around. - bool fIsEdgeResize = false; - { - // We can only be edge resizing if our existing rectangle wasn't empty. - // If it was empty, we're doing the initial create. - if (!IsRectEmpty(&rcCurrent)) - { - // If one or two sides are changing, we're being edge resized. - unsigned int cSidesChanging = 0; - if (rcCurrent.left != rcSuggested.left) - { - cSidesChanging++; - } - if (rcCurrent.right != rcSuggested.right) - { - cSidesChanging++; - } - if (rcCurrent.top != rcSuggested.top) - { - cSidesChanging++; - } - if (rcCurrent.bottom != rcSuggested.bottom) - { - cSidesChanging++; - } - - if (cSidesChanging == 1 || cSidesChanging == 2) - { - fIsEdgeResize = true; - } - } - } +// Method Description: +// - Updates the window frame's theme depending on the application theme (light +// or dark). This doesn't invalidate the old frame so it will not be +// rerendered until the user resizes or focuses/unfocuses the window. +// Return Value: +// - +void NonClientIslandWindow::_UpdateFrameTheme() const +{ + bool isDarkMode; - // If we're about to maximize the window, determine how much we're about to - // overhang by, and adjust for that. - // We need to do this because maximized windows will typically overhang the - // actual monitor bounds by roughly the size of the old "thick: window - // borders. For normal windows, this is fine, but because we're using - // DwmExtendFrameIntoClientArea, that means some of our client content will - // now overhang, and get cut off. - if (isMaximized) + switch (_theme) { - // Find the related monitor, the maximum pixel size, - // and the dpi for the suggested rect. - UINT dpiOfMaximum; - RECT rcMaximum; + case ElementTheme::Light: + isDarkMode = false; + break; + case ElementTheme::Dark: + isDarkMode = true; + break; + default: + isDarkMode = Application::Current().RequestedTheme() == ApplicationTheme::Dark; + break; + } - if (fIsEdgeResize) - { - // If someone's dragging from the edge to resize in one direction, - // we want to make sure we never grow past the current monitor. - rcMaximum = GetMaxWindowRectInPixels(&rcCurrent, &dpiOfMaximum); - } - else - { - // In other circumstances, assume we're snapping around or some - // other jump (TS). Just do whatever we're told using the new - // suggestion as the restriction monitor. - rcMaximum = GetMaxWindowRectInPixels(&rcSuggested, &dpiOfMaximum); - } + LOG_IF_FAILED(ThemeUtils::SetWindowFrameDarkMode(_window.get(), isDarkMode)); +} - const auto suggestedWidth = szSuggested.cx; - const auto suggestedHeight = szSuggested.cy; - - const auto maxWidth = RECT_WIDTH(&rcMaximum); - const auto maxHeight = RECT_HEIGHT(&rcMaximum); - - // Only apply the maximum size restriction if the current DPI matches - // the DPI of the maximum rect. This keeps us from applying the wrong - // restriction if the monitor we're moving to has a different DPI but - // we've yet to get notified of that DPI change. If we do apply it, then - // we'll restrict the console window BEFORE its been resized for the DPI - // change, so we're likely to shrink the window too much or worse yet, - // keep it from moving entirely. We'll get a WM_DPICHANGED, resize the - // window, and then process the restriction in a few window messages. - if (((int)dpiOfMaximum == _currentDpi) && - ((suggestedWidth > maxWidth) || - (suggestedHeight > maxHeight))) - { - RECT frame{}; - // Calculate the maxmized window overhang by getting the size of the window frame. - // We use the style without WS_CAPTION otherwise the caption height is included. - // Only remove WS_DLGFRAME since WS_CAPTION = WS_DLGFRAME | WS_BORDER, - // but WS_BORDER is needed as it modifies the calculation of the width of the frame. - const auto targetStyle = windowStyle & ~WS_DLGFRAME; - AdjustWindowRectExForDpi(&frame, targetStyle, false, GetWindowExStyle(_window.get()), _currentDpi); - - // Frame left and top will be negative - _maximizedMargins.cxLeftWidth = frame.left * -1; - _maximizedMargins.cyTopHeight = frame.top * -1; - _maximizedMargins.cxRightWidth = frame.right; - _maximizedMargins.cyBottomHeight = frame.bottom; - - _isMaximized = true; - THROW_IF_FAILED(_UpdateFrameMargins()); - } - } - else - { - // Clear our maximization state - _maximizedMargins = { 0 }; - - // Immediately after resoring down, don't update our frame margins. If - // you do this here, then a small gap will appear between the titlebar - // and the content, until the window is moved. However, we do need to - // keep this here _in general_ for dragging across DPI boundaries. - if (!_isMaximized) - { - THROW_IF_FAILED(_UpdateFrameMargins()); - } +// Method Description: +// - Called when the app wants to change its theme. We'll update the frame +// theme to match the new theme. +// Arguments: +// - requestedTheme: the ElementTheme to use as the new theme for the UI +// Return Value: +// - +void NonClientIslandWindow::OnApplicationThemeChanged(const ElementTheme& requestedTheme) +{ + IslandWindow::OnApplicationThemeChanged(requestedTheme); - _isMaximized = false; - } - return true; + _theme = requestedTheme; + _UpdateFrameTheme(); } diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 4c1d5cdc5cc..152d92dff63 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -21,48 +21,58 @@ Author(s): #include "IslandWindow.h" #include "../../types/inc/Viewport.hpp" #include -#include +#include class NonClientIslandWindow : public IslandWindow { public: - NonClientIslandWindow() noexcept; + // this is the same for all DPIs + static constexpr const int topBorderVisibleHeight = 1; + + NonClientIslandWindow(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) noexcept; virtual ~NonClientIslandWindow() override; virtual void OnSize(const UINT width, const UINT height) override; [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept override; - MARGINS GetFrameMargins() const noexcept; - void Initialize() override; void OnAppInitialized() override; void SetContent(winrt::Windows::UI::Xaml::UIElement content) override; void SetTitlebarContent(winrt::Windows::UI::Xaml::UIElement content); + void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) override; private: + std::optional _oldIslandPos; + winrt::TerminalApp::TitlebarControl _titlebar{ nullptr }; winrt::Windows::UI::Xaml::UIElement _clientContent{ nullptr }; wil::unique_hbrush _backgroundBrush; - wil::unique_hrgn _dragBarRegion; + COLORREF _backgroundBrushColor; - MARGINS _maximizedMargins = { 0 }; - bool _isMaximized; winrt::Windows::UI::Xaml::Controls::Border _dragBar{ nullptr }; + wil::unique_hrgn _dragBarRegion; - RECT GetDragAreaRect() const noexcept; - - [[nodiscard]] LRESULT HitTestNCA(POINT ptMouse) const noexcept; + winrt::Windows::UI::Xaml::ElementTheme _theme; - [[nodiscard]] HRESULT _UpdateFrameMargins() const noexcept; + bool _isMaximized; - void _HandleActivateWindow(); - bool _HandleWindowPosChanging(WINDOWPOS* const windowPos); - void _UpdateDragRegion(); + int _GetResizeHandleHeight() const noexcept; + RECT _GetDragAreaRect() const noexcept; + int _GetTopBorderHeight() const noexcept; - void OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::SizeChangedEventArgs eventArgs); + [[nodiscard]] LRESULT _OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept override; + [[nodiscard]] LRESULT _OnNcCalcSize(const WPARAM wParam, const LPARAM lParam) noexcept; + [[nodiscard]] LRESULT _OnNcHitTest(POINT ptMouse) const noexcept; + [[nodiscard]] LRESULT _OnPaint() noexcept; + void _OnMaximizeChange() noexcept; + void _OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::SizeChangedEventArgs eventArgs) const; - RECT GetMaxWindowRectInPixels(const RECT* const prcSuggested, _Out_opt_ UINT* pDpiSuggested); + [[nodiscard]] HRESULT _UpdateFrameMargins() const noexcept; + void _UpdateMaximizedState(); + void _UpdateIslandPosition(const UINT windowWidth, const UINT windowHeight); + void _UpdateIslandRegion() const; + void _UpdateFrameTheme() const; }; diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj index f90bcc49a41..045dea33968 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj @@ -38,7 +38,7 @@ "$(OpenConsoleDir)src\cascadia\TerminalCore\lib\Generated Files";%(AdditionalIncludeDirectories); - gdi32.lib;dwmapi.lib;Shcore.lib;%(AdditionalDependencies) + gdi32.lib;dwmapi.lib;Shcore.lib;UxTheme.lib;%(AdditionalDependencies) diff --git a/src/types/ThemeUtils.cpp b/src/types/ThemeUtils.cpp new file mode 100644 index 00000000000..ecc4af6a34f --- /dev/null +++ b/src/types/ThemeUtils.cpp @@ -0,0 +1,19 @@ +#include "precomp.h" +#include "inc/ThemeUtils.h" + +namespace Microsoft::Console::ThemeUtils +{ + // Routine Description: + // - Attempts to enable/disable the dark mode on the frame of a window. + // Arguments: + // - hwnd: handle to the window to change + // - enabled: whether to enable or not the dark mode on the window's frame + // Return Value: + // - S_OK or suitable HRESULT from DWM engines. + [[nodiscard]] HRESULT SetWindowFrameDarkMode(HWND /* hwnd */, bool /* enabled */) noexcept + { + // TODO:GH #3425 implement the new DWM API and change + // src/interactivity/win32/windowtheme.cpp to use it. + return S_OK; + } +} diff --git a/src/types/inc/ThemeUtils.h b/src/types/inc/ThemeUtils.h new file mode 100644 index 00000000000..ffe972301fe --- /dev/null +++ b/src/types/inc/ThemeUtils.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace Microsoft::Console::ThemeUtils +{ + [[nodiscard]] HRESULT SetWindowFrameDarkMode(HWND hwnd, bool enabled) noexcept; +} diff --git a/src/types/lib/types.vcxproj b/src/types/lib/types.vcxproj index b5c48480eff..107319f2e4e 100644 --- a/src/types/lib/types.vcxproj +++ b/src/types/lib/types.vcxproj @@ -12,6 +12,7 @@ + @@ -30,7 +31,9 @@ + + @@ -38,7 +41,6 @@ - @@ -51,4 +53,4 @@ - + \ No newline at end of file diff --git a/src/types/lib/types.vcxproj.filters b/src/types/lib/types.vcxproj.filters index 60c1dfbde5c..c999e57a24d 100644 --- a/src/types/lib/types.vcxproj.filters +++ b/src/types/lib/types.vcxproj.filters @@ -60,9 +60,6 @@ Source Files - - Source Files - Source Files @@ -72,6 +69,9 @@ Source Files + + Source Files + @@ -95,21 +95,12 @@ Header Files - - Header Files - Header Files - - Header Files - Header Files - - Header Files - Header Files @@ -161,8 +152,14 @@ Header Files + + Header Files + + + Header Files + - + \ No newline at end of file diff --git a/src/types/sources.inc b/src/types/sources.inc index a0cc8151ae0..1751d6bb6c6 100644 --- a/src/types/sources.inc +++ b/src/types/sources.inc @@ -41,6 +41,7 @@ SOURCES= \ ..\convert.cpp \ ..\Utf16Parser.cpp \ ..\utils.cpp \ + ..\ThemeUtils.cpp \ INCLUDES= \ $(INCLUDES); \