diff --git a/src/cascadia/TerminalApp/App.cpp b/src/cascadia/TerminalApp/App.cpp index 72b6ae388..3126dfbff 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 770e81081..eb0db0dd7 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 c724191ac..b65d419a4 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 8985f2320..960a74589 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 25f3d6f41..899bc56b3 100644 --- a/src/cascadia/WindowsTerminal/BaseWindow.h +++ b/src/cascadia/WindowsTerminal/BaseWindow.h @@ -34,10 +34,8 @@ public: 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 @@ protected: 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 f546cc4dc..d87c92a46 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 374949f0c..6493f0a9c 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -23,19 +23,17 @@ public: [[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 0908481d6..be77c4280 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); + } +} + +// 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) + { + _isMaximized = newIsMaximized; + _OnMaximizeChange(); + } +} + +// 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(); } - const auto scale = GetCurrentDpiScale(); - const auto dpi = ::GetDpiForWindow(_window.get()); + // no frame margin when maximized + THROW_IF_FAILED(_UpdateFrameMargins()); +} - const auto dragY = ::GetSystemMetricsForDpi(SM_CYDRAG, dpi); - const auto dragX = ::GetSystemMetricsForDpi(SM_CXDRAG, dpi); +// 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); - // 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 COORD newIslandPos = { 0, topBorderHeight }; - 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) - { - 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); - } - - // I'm not sure that HWND_BOTTOM does anything differnet than HWND_TOP for us. + // 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. +// - Returns the height of the little space at the top of the window used to +// resize the window. +// Return Value: +// - the height of the window's top resize handle +int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept +{ + // there isn't a SM_CYPADDEDBORDER for the Y axis + return ::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, _currentDpi) + + ::GetSystemMetricsForDpi(SM_CYSIZEFRAME, _currentDpi); +} + +// 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; + } + + NCCALCSIZE_PARAMS* params = reinterpret_cast(lParam); + + // Store the original top before the default window proc applies the + // default frame. + const auto originalTop = params->rgrc[0].top; + + // apply the default frame + auto ret = DefWindowProc(_window.get(), WM_NCCALCSIZE, wParam, lParam); + if (ret != 0) + { + return ret; + } + + auto newTop = originalTop; + + // WM_NCCALCSIZE is called before WM_SIZE + _UpdateMaximizedState(); + + if (_isMaximized) + { + // 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(); + } + + // only modify the top of the frame to remove the title bar + params->rgrc[0].top = newTop; + + return 0; +} + // Method Description: // - Hit test the frame for resizing and moving. // Arguments: // - ptMouse: the mouse point being tested, in absolute (NOT WINDOW) coordinates. // Return Value: // - one of the values from -// https://docs.microsoft.com/en-us/windows/desktop/inputdev/wm-nchittest#return-value +// 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 +[[nodiscard]] LRESULT NonClientIslandWindow::_OnNcHitTest(POINT ptMouse) const noexcept { - // Get the window rectangle. - RECT rcWindow = BaseWindow::GetWindowRect(); - - MARGINS margins = GetFrameMargins(); - - // 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); - - // Determine if the hit test is for resizing. Default middle (1,1). - unsigned short uRow = 1; - unsigned short uCol = 1; - bool fOnResizeBorder = false; - - // Determine if the point is at the top or bottom of the window. - if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + margins.cyTopHeight) + // 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) { - fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top)); - uRow = 0; - } - else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - margins.cyBottomHeight) - { - uRow = 2; + return originalRet; } - // Determine if the point is at the left or right of the window. - if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + margins.cxLeftWidth) + // 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. + + RECT rcWindow; + winrt::check_bool(::GetWindowRect(_window.get(), &rcWindow)); + + 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) { - uCol = 0; // left side - } - else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - margins.cxRightWidth) - { - uCol = 2; // right side + return HTTOP; } - // 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]; -} - -// 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. -// Return Value: -// - A MARGINS struct containing the border dimensions we want. -MARGINS NonClientIslandWindow::GetFrameMargins() 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); - - const auto dragBarRect = GetDragAreaRect(); - const auto nonClientHeight = dragBarRect.bottom - dragBarRect.top; - - MARGINS margins{ 0 }; - margins.cxLeftWidth = windowMarginSides; - margins.cxRightWidth = windowMarginSides; - margins.cyBottomHeight = windowMarginBottom; - margins.cyTopHeight = nonClientHeight + windowMarginBottom; - - return margins; + return HTCAPTION; } // Method Description: @@ -348,106 +382,35 @@ 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 }; + MARGINS margins = {}; + + if (_GetTopBorderHeight() != 0) + { + 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; + } // 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); - - 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) - { - UINT monitorDpiX; - UINT monitorDpiY; - if (SUCCEEDED(GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &monitorDpiX, &monitorDpiY))) - { - *pDpiSuggested = monitorDpiX; - } - else - { - *pDpiSuggested = GetDpiForWindow(_window.get()); - } - } - - return rc; -} - // Method Description: // - Handle window messages from the message loop. // Arguments: @@ -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; - } - - case WM_EXITSIZEMOVE: - { - ForceResize(); - break; - } - + return _OnNcHitTest({ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }); case WM_PAINT: - { - 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: - { - // If we clicked in the titlebar, raise an event so the app host can - // dispatch an appropriate event. - _DragRegionClickedHandlers(); - break; - } - - case WM_WINDOWPOSCHANGING: - { - // 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)) - { - return 0; - } - else - { - break; - } - } - case WM_DPICHANGED: - { - auto lprcNewScale = reinterpret_cast(lParam); - OnSize(RECT_WIDTH(lprcNewScale), RECT_HEIGHT(lprcNewScale)); - break; - } + return _OnPaint(); } return IslandWindow::MessageHandler(message, wParam, lParam); } // 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() +// - 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 { - THROW_IF_FAILED(_UpdateFrameMargins()); + if (!_titlebar) + { + return 0; + } + + PAINTSTRUCT ps{ 0 }; + const auto hdc = wil::BeginPaint(_window.get(), &ps); + if (!hdc) + { + return 0; + } + + const auto topBorderHeight = _GetTopBorderHeight(); + + if (ps.rcPaint.top < topBorderHeight) + { + 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)); + } + + if (ps.rcPaint.bottom > topBorderHeight) + { + 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) + { + // Create brush for titlebar color. + _backgroundBrush = wil::unique_hbrush(CreateSolidBrush(color)); + } + + // 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) + { + winrt::throw_last_error(); + } + ::FillRect(opaqueDc, &rcRest, _backgroundBrush.get()); + ::BufferedPaintSetAlpha(buf, NULL, 255); + ::EndBufferedPaint(buf, TRUE); + } + + return 0; } // 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); - } - - // 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; - } - } - } - - // 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) - { - // Find the related monitor, the maximum pixel size, - // and the dpi for the suggested rect. - UINT dpiOfMaximum; - RECT rcMaximum; - - 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); - } - - 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()); - } - - _isMaximized = false; - } - return true; + return 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; + + switch (_theme) + { + case ElementTheme::Light: + isDarkMode = false; + break; + case ElementTheme::Dark: + isDarkMode = true; + break; + default: + isDarkMode = Application::Current().RequestedTheme() == ApplicationTheme::Dark; + break; + } + + LOG_IF_FAILED(ThemeUtils::SetWindowFrameDarkMode(_window.get(), isDarkMode)); +} + +// 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); + + _theme = requestedTheme; + _UpdateFrameTheme(); } diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 4c1d5cdc5..152d92dff 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; + COLORREF _backgroundBrushColor; + + winrt::Windows::UI::Xaml::Controls::Border _dragBar{ nullptr }; wil::unique_hrgn _dragBarRegion; - MARGINS _maximizedMargins = { 0 }; + winrt::Windows::UI::Xaml::ElementTheme _theme; + bool _isMaximized; - winrt::Windows::UI::Xaml::Controls::Border _dragBar{ nullptr }; - RECT GetDragAreaRect() const noexcept; + int _GetResizeHandleHeight() const noexcept; + RECT _GetDragAreaRect() const noexcept; + int _GetTopBorderHeight() const noexcept; - [[nodiscard]] LRESULT HitTestNCA(POINT ptMouse) const noexcept; + [[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; [[nodiscard]] HRESULT _UpdateFrameMargins() const noexcept; - - void _HandleActivateWindow(); - bool _HandleWindowPosChanging(WINDOWPOS* const windowPos); - void _UpdateDragRegion(); - - void OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::SizeChangedEventArgs eventArgs); - - RECT GetMaxWindowRectInPixels(const RECT* const prcSuggested, _Out_opt_ UINT* pDpiSuggested); + 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 f90bcc49a..045dea339 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 000000000..ecc4af6a3 --- /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 000000000..ffe972301 --- /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 b5c48480e..107319f2e 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 60c1dfbde..c999e57a2 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 a0cc8151a..1751d6bb6 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); \