diff --git a/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest b/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest
index 3be6f3c7bf9..f48f0e43cd5 100644
--- a/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest
+++ b/src/cascadia/ut_app/TerminalApp.Unit.Tests.manifest
@@ -20,6 +20,7 @@
UI Variable, GH #12452 -->
+
diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp
index ed42acb60d5..973504ee703 100644
--- a/src/interactivity/win32/window.cpp
+++ b/src/interactivity/win32/window.cpp
@@ -964,6 +964,30 @@ void Window::s_CalculateWindowRect(const til::size coordWindowInChars,
prectWindow->bottom = prectWindow->top + rectProposed.height();
}
+// Expands a rect by the size of the non-client area (caption bar, resize borders,
+// scroll bars, etc), which depends on the window styles and DPI
+void Window::s_ExpandRectByNonClientSize(HWND const hWnd,
+ UINT dpi,
+ _Inout_ til::rect* const prectWindow)
+{
+ DWORD dwStyle = GetWindowStyle(hWnd);
+ DWORD dwExStyle = GetWindowExStyle(hWnd);
+ BOOL fMenu = FALSE;
+
+ ServiceLocator::LocateWindowMetrics()->AdjustWindowRectEx(
+ prectWindow, dwStyle, fMenu, dwExStyle, dpi);
+
+ // Note: AdjustWindowRectEx does not account for scroll bars :(.
+ if (WI_IsFlagSet(dwStyle, WS_HSCROLL))
+ {
+ prectWindow->bottom += ServiceLocator::LocateHighDpiApi()->GetSystemMetricsForDpi(SM_CYHSCROLL, dpi);
+ }
+ if (WI_IsFlagSet(dwStyle, WS_VSCROLL))
+ {
+ prectWindow->bottom += ServiceLocator::LocateHighDpiApi()->GetSystemMetricsForDpi(SM_CXVSCROLL, dpi);
+ }
+}
+
til::rect Window::GetWindowRect() const noexcept
{
RECT rc{};
diff --git a/src/interactivity/win32/window.hpp b/src/interactivity/win32/window.hpp
index ea678628e1e..19ba8266bea 100644
--- a/src/interactivity/win32/window.hpp
+++ b/src/interactivity/win32/window.hpp
@@ -135,6 +135,7 @@ namespace Microsoft::Console::Interactivity::Win32
void _HandleDrop(const WPARAM wParam) const;
[[nodiscard]] HRESULT _HandlePaint() const;
void _HandleWindowPosChanged(const LPARAM lParam);
+ bool _HandleGetDpiScaledSize(UINT dpiNew, _Inout_ SIZE* pSizeNew) const;
// Accessibility/UI Automation
[[nodiscard]] LRESULT _HandleGetObject(const HWND hwnd,
@@ -173,6 +174,9 @@ namespace Microsoft::Console::Interactivity::Win32
const til::size coordBufferSize,
_In_opt_ HWND const hWnd,
_Inout_ til::rect* const prectWindow);
+ static void s_ExpandRectByNonClientSize(HWND const hWnd,
+ UINT dpi,
+ _Inout_ til::rect* const prectWindow);
static void s_ReinitializeFontsForDPIChange();
diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp
index 0dc8cf3853b..4af0492cdd8 100644
--- a/src/interactivity/win32/windowproc.cpp
+++ b/src/interactivity/win32/windowproc.cpp
@@ -244,43 +244,12 @@ static constexpr TsfDataProvider s_tsfDataProvider;
case WM_GETDPISCALEDSIZE:
{
- // This message will send us the DPI we're about to be changed to.
- // Our goal is to use it to try to figure out the Window Rect that we'll need at that DPI to maintain
- // the same client rendering that we have now.
-
- // First retrieve the new DPI and the current DPI.
- const auto dpiProposed = (WORD)wParam;
-
- // Now we need to get what the font size *would be* if we had this new DPI. We need to ask the renderer about that.
- const auto& fiCurrent = ScreenInfo.GetCurrentFont();
- FontInfoDesired fiDesired(fiCurrent);
- FontInfo fiProposed(L"", 0, 0, { 0, 0 }, 0);
-
- const auto hr = g.pRender->GetProposedFont(dpiProposed, fiDesired, fiProposed);
- // fiProposal will be updated by the renderer for this new font.
- // GetProposedFont can fail if there's no render engine yet.
- // This can happen if we're headless.
- // Just assume that the font is 1x1 in that case.
- const auto coordFontProposed = SUCCEEDED(hr) ? fiProposed.GetSize() : til::size{ 1, 1 };
-
- // Then from that font size, we need to calculate the client area.
- // Then from the client area we need to calculate the window area (using the proposed DPI scalar here as well.)
-
- // Retrieve the additional parameters we need for the math call based on the current window & buffer properties.
- const auto viewport = ScreenInfo.GetViewport();
- auto coordWindowInChars = viewport.Dimensions();
-
- const auto coordBufferSize = ScreenInfo.GetTextBuffer().GetSize().Dimensions();
-
- // Now call the math calculation for our proposed size.
- til::rect rectProposed;
- s_CalculateWindowRect(coordWindowInChars, dpiProposed, coordFontProposed, coordBufferSize, hWnd, &rectProposed);
-
- // Prepare where we're going to keep our final suggestion.
- const auto pSuggestionSize = (SIZE*)lParam;
-
- pSuggestionSize->cx = rectProposed.width();
- pSuggestionSize->cy = rectProposed.height();
+ SIZE* pSizeNew = (SIZE*)lParam;
+ UINT dpiNew = (WORD)wParam;
+ if (!_HandleGetDpiScaledSize(dpiNew, pSizeNew))
+ {
+ return FALSE;
+ }
// Format our final suggestion for consumption.
UnlockConsole();
@@ -884,6 +853,60 @@ void Window::_HandleWindowPosChanged(const LPARAM lParam)
}
}
+// WM_GETDPISCALEDSIZE is sent prior to the window changing DPI, allowing us to
+// choose the size at the new DPI (overriding the default, linearly scaled).
+//
+// This is used to keep the rows and columns from changing when the DPI changes.
+bool Window::_HandleGetDpiScaledSize(UINT dpiNew, _Inout_ SIZE* pSizeNew) const
+{
+ // Get the current DPI and font size.
+ HWND hwnd = GetWindowHandle();
+ UINT dpiCurrent = ServiceLocator::LocateHighDpiApi()->GetDpiForWindow(hwnd);
+ const auto& fontInfoCurrent = GetScreenInfo().GetCurrentFont();
+ til::size fontSizeCurrent = fontInfoCurrent.GetSize();
+
+ // Scale the current font to the new DPI and get the new font size.
+ FontInfoDesired fontInfoDesired(fontInfoCurrent);
+ FontInfo fontInfoNew(L"", 0, 0, { 0, 0 }, 0);
+ if (!SUCCEEDED(ServiceLocator::LocateGlobals().pRender->GetProposedFont(
+ dpiNew, fontInfoDesired, fontInfoNew)))
+ {
+ return false;
+ }
+ til::size fontSizeNew = fontInfoNew.GetSize();
+
+ // The provided size is the window rect, which includes non-client area
+ // (caption bars, resize borders, scroll bars, etc). We want to scale the
+ // client area separately from the non-client area. The client area will be
+ // scaled using the new/old font sizes, so that the size of the grid (rows/
+ // columns) does not change.
+
+ // Subtract the size of the window's current non-client area from the
+ // provided size. This gives us the new client area size at the previous DPI.
+ til::rect rc;
+ s_ExpandRectByNonClientSize(hwnd, dpiCurrent, &rc);
+ pSizeNew->cx -= rc.width();
+ pSizeNew->cy -= rc.height();
+
+ // Scale the size of the client rect by the new/old font sizes.
+ pSizeNew->cx = MulDiv(pSizeNew->cx, fontSizeNew.width, fontSizeCurrent.width);
+ pSizeNew->cy = MulDiv(pSizeNew->cy, fontSizeNew.height, fontSizeCurrent.height);
+
+ // Add the size of the non-client area at the new DPI to the final size,
+ // getting the new window rect (the output of this function).
+ rc = { 0, 0, pSizeNew->cx, pSizeNew->cy };
+ s_ExpandRectByNonClientSize(hwnd, dpiNew, &rc);
+
+ // Write the final size to the out parameter.
+ // If not Maximized/Arranged (snapped), this will determine the size of the
+ // rect in the WM_DPICHANGED message. Otherwise, the provided size is the
+ // normal position (restored, last non-Maximized/Arranged).
+ pSizeNew->cx = rc.width();
+ pSizeNew->cy = rc.height();
+
+ return true;
+}
+
// Routine Description:
// - This helper method for the window procedure will handle the WM_PAINT message
// - It will retrieve the invalid rectangle and dispatch that information to the attached renderer