diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 83ca55178..c9ce34e86 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -43,12 +43,14 @@ fullkbd futex GETDESKWALLPAPER GETHIGHCONTRAST +GETMOUSEHOVERTIME Hashtable HIGHCONTRASTON HIGHCONTRASTW hotkeys href hrgn +HTCLOSE IActivation IApp IAppearance @@ -93,11 +95,14 @@ MENUINFO MENUITEMINFOW memicmp mptt +MOUSELEAVE mov msappx MULTIPLEUSE NCHITTEST NCLBUTTONDBLCLK +NCMOUSELEAVE +NCMOUSEMOVE NCRBUTTONDBLCLK NIF NIN @@ -163,9 +168,11 @@ TASKBARCREATED TBPF THEMECHANGED tlg +TME tmp tolower toupper +TRACKMOUSEEVENT TTask TVal UChar diff --git a/src/cascadia/TerminalApp/MinMaxCloseControl.cpp b/src/cascadia/TerminalApp/MinMaxCloseControl.cpp index 0072b2093..24a47b0f8 100644 --- a/src/cascadia/TerminalApp/MinMaxCloseControl.cpp +++ b/src/cascadia/TerminalApp/MinMaxCloseControl.cpp @@ -1,25 +1,67 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// -// MinMaxCloseControl.xaml.cpp -// Implementation of the MinMaxCloseControl class -// #include "pch.h" #include "MinMaxCloseControl.h" - #include "MinMaxCloseControl.g.cpp" - #include using namespace winrt::Windows::UI::Xaml; namespace winrt::TerminalApp::implementation { + static void closeToolTipForButton(const Controls::Button& button) + { + if (auto tt{ Controls::ToolTipService::GetToolTip(button) }) + { + if (auto tooltip{ tt.try_as() }) + { + tooltip.IsOpen(false); + } + } + } + MinMaxCloseControl::MinMaxCloseControl() { + // Get our dispatcher. This will get us the same dispatcher as + // Dispatcher(), but it's a DispatcherQueue, so we can use it with + // ThrottledFunc + auto dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); + InitializeComponent(); + + // Get the tooltip hover time from the system, or default to 400ms + // (which should be the default, see: + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-trackmouseevent#remarks) + unsigned int hoverTimeoutMillis{ 400 }; + LOG_IF_WIN32_BOOL_FALSE(SystemParametersInfoW(SPI_GETMOUSEHOVERTIME, 0, &hoverTimeoutMillis, 0)); + const auto toolTipInterval = std::chrono::milliseconds(hoverTimeoutMillis); + + // Create a ThrottledFunc for opening the tooltip after the hover + // timeout. If we hover another button, we should make sure to call + // Run() with the new button. Calling `_displayToolTip.Run(nullptr)`, + // which will cause us to not display a tooltip, which is used when we + // leave the control entirely. + _displayToolTip = std::make_shared>( + dispatcher, + toolTipInterval, + [weakThis = get_weak()](Controls::Button button) { + // If we provide a button, then open the tooltip on that button. + // We can "dismiss" this throttled func by calling it with null, + // which will cause us to do nothing at the end of the timeout + // instead. + if (button) + { + if (auto tt{ Controls::ToolTipService::GetToolTip(button) }) + { + if (auto tooltip{ tt.try_as() }) + { + tooltip.IsOpen(true); + } + } + } + }); } // These event handlers simply forward each buttons click events up to the @@ -95,4 +137,104 @@ namespace winrt::TerminalApp::implementation break; } } + + // Method Description: + // - Called when the mouse hovers a button. + // - Transition that button to `PointerOver` + // - run the throttled func with this button, to display the tooltip after + // a timeout + // - dismiss any open tooltips on other buttons. + // Arguments: + // - button: the button that was hovered + void MinMaxCloseControl::HoverButton(CaptionButton button) + { + // Keep track of the button that's been pressed. we get a mouse move + // message when we open the tooltip. If we move the mouse on top of this + // button, that we've already pressed, then no need to move to the + // "hovered" state, we should stay in the pressed state. + if (_lastPressedButton && _lastPressedButton.value() == button) + { + return; + } + + switch (button) + { + // Make sure to use true for the useTransitions parameter, to + // animate the fade in/out transition between colors. + case CaptionButton::Minimize: + VisualStateManager::GoToState(MinimizeButton(), L"PointerOver", true); + VisualStateManager::GoToState(MaximizeButton(), L"Normal", true); + VisualStateManager::GoToState(CloseButton(), L"Normal", true); + + _displayToolTip->Run(MinimizeButton()); + closeToolTipForButton(MaximizeButton()); + closeToolTipForButton(CloseButton()); + break; + case CaptionButton::Maximize: + VisualStateManager::GoToState(MinimizeButton(), L"Normal", true); + VisualStateManager::GoToState(MaximizeButton(), L"PointerOver", true); + VisualStateManager::GoToState(CloseButton(), L"Normal", true); + + closeToolTipForButton(MinimizeButton()); + _displayToolTip->Run(MaximizeButton()); + closeToolTipForButton(CloseButton()); + break; + case CaptionButton::Close: + VisualStateManager::GoToState(MinimizeButton(), L"Normal", true); + VisualStateManager::GoToState(MaximizeButton(), L"Normal", true); + VisualStateManager::GoToState(CloseButton(), L"PointerOver", true); + + closeToolTipForButton(MinimizeButton()); + closeToolTipForButton(MaximizeButton()); + _displayToolTip->Run(CloseButton()); + break; + } + } + + // Method Description: + // - Called when the mouse presses down on a button. NOT when it is + // released. That's handled one level above, in + // TitleBarControl::ReleaseButtons + // - Transition that button to `Pressed` + // Arguments: + // - button: the button that was pressed + void MinMaxCloseControl::PressButton(CaptionButton button) + { + switch (button) + { + case CaptionButton::Minimize: + VisualStateManager::GoToState(MinimizeButton(), L"Pressed", true); + VisualStateManager::GoToState(MaximizeButton(), L"Normal", true); + VisualStateManager::GoToState(CloseButton(), L"Normal", true); + break; + case CaptionButton::Maximize: + VisualStateManager::GoToState(MinimizeButton(), L"Normal", true); + VisualStateManager::GoToState(MaximizeButton(), L"Pressed", true); + VisualStateManager::GoToState(CloseButton(), L"Normal", true); + break; + case CaptionButton::Close: + VisualStateManager::GoToState(MinimizeButton(), L"Normal", true); + VisualStateManager::GoToState(MaximizeButton(), L"Normal", true); + VisualStateManager::GoToState(CloseButton(), L"Pressed", true); + break; + } + _lastPressedButton = button; + } + + // Method Description: + // - Called when buttons are no longer hovered or pressed. Return them all + // to the normal state, and dismiss the tooltips. + void MinMaxCloseControl::ReleaseButtons() + { + _displayToolTip->Run(nullptr); + VisualStateManager::GoToState(MinimizeButton(), L"Normal", true); + VisualStateManager::GoToState(MaximizeButton(), L"Normal", true); + VisualStateManager::GoToState(CloseButton(), L"Normal", true); + + closeToolTipForButton(MinimizeButton()); + closeToolTipForButton(MaximizeButton()); + closeToolTipForButton(CloseButton()); + + _lastPressedButton = std::nullopt; + } } diff --git a/src/cascadia/TerminalApp/MinMaxCloseControl.h b/src/cascadia/TerminalApp/MinMaxCloseControl.h index 0a8d66654..0c336f256 100644 --- a/src/cascadia/TerminalApp/MinMaxCloseControl.h +++ b/src/cascadia/TerminalApp/MinMaxCloseControl.h @@ -6,11 +6,9 @@ #pragma once -#include "winrt/Windows.UI.Xaml.h" -#include "winrt/Windows.UI.Xaml.Markup.h" -#include "winrt/Windows.UI.Xaml.Interop.h" #include "MinMaxCloseControl.g.h" #include "../../cascadia/inc/cppwinrt_utils.h" +#include namespace winrt::TerminalApp::implementation { @@ -20,6 +18,10 @@ namespace winrt::TerminalApp::implementation void SetWindowVisualState(WindowVisualState visualState); + void HoverButton(CaptionButton button); + void PressButton(CaptionButton button); + void ReleaseButtons(); + void _MinimizeClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); void _MaximizeClick(winrt::Windows::Foundation::IInspectable const& sender, @@ -30,6 +32,9 @@ namespace winrt::TerminalApp::implementation TYPED_EVENT(MinimizeClick, TerminalApp::MinMaxCloseControl, winrt::Windows::UI::Xaml::RoutedEventArgs); TYPED_EVENT(MaximizeClick, TerminalApp::MinMaxCloseControl, winrt::Windows::UI::Xaml::RoutedEventArgs); TYPED_EVENT(CloseClick, TerminalApp::MinMaxCloseControl, winrt::Windows::UI::Xaml::RoutedEventArgs); + + std::shared_ptr> _displayToolTip{ nullptr }; + std::optional _lastPressedButton{ std::nullopt }; }; } diff --git a/src/cascadia/TerminalApp/MinMaxCloseControl.idl b/src/cascadia/TerminalApp/MinMaxCloseControl.idl index eb8fbd16e..61600fe27 100644 --- a/src/cascadia/TerminalApp/MinMaxCloseControl.idl +++ b/src/cascadia/TerminalApp/MinMaxCloseControl.idl @@ -11,6 +11,10 @@ namespace TerminalApp void SetWindowVisualState(WindowVisualState visualState); + void HoverButton(CaptionButton button); + void PressButton(CaptionButton button); + void ReleaseButtons(); + event Windows.Foundation.TypedEventHandler MinimizeClick; event Windows.Foundation.TypedEventHandler MaximizeClick; event Windows.Foundation.TypedEventHandler CloseClick; diff --git a/src/cascadia/TerminalApp/MinMaxCloseControl.xaml b/src/cascadia/TerminalApp/MinMaxCloseControl.xaml index 105de2226..684831586 100644 --- a/src/cascadia/TerminalApp/MinMaxCloseControl.xaml +++ b/src/cascadia/TerminalApp/MinMaxCloseControl.xaml @@ -220,7 +220,7 @@ diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index d71ec4535..d3e495741 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -411,6 +411,9 @@ Close + + Close + Maximize @@ -420,6 +423,9 @@ Minimize + + Minimize + About diff --git a/src/cascadia/TerminalApp/TitlebarControl.cpp b/src/cascadia/TerminalApp/TitlebarControl.cpp index 05806726c..4f3da96b5 100644 --- a/src/cascadia/TerminalApp/TitlebarControl.cpp +++ b/src/cascadia/TerminalApp/TitlebarControl.cpp @@ -1,7 +1,6 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -// TitlebarControl.xaml.cpp // Implementation of the TitlebarControl class // @@ -24,6 +23,14 @@ namespace winrt::TerminalApp::implementation MinMaxCloseControl().CloseClick({ this, &TitlebarControl::Close_Click }); } + double TitlebarControl::CaptionButtonWidth() + { + // Divide by three, since we know there are only three buttons. When + // Windows 12 comes along and adds another, we can update this /s + static double width{ MinMaxCloseControl().ActualWidth() / 3.0 }; + return width; + } + IInspectable TitlebarControl::Content() { return ContentRoot().Content(); @@ -93,4 +100,48 @@ namespace winrt::TerminalApp::implementation { MinMaxCloseControl().SetWindowVisualState(visualState); } + + // GH#9443: HoverButton, PressButton, ClickButton and ReleaseButtons are all + // used to manually interact with the buttons, in the same way that XAML + // would normally send events. + + void TitlebarControl::HoverButton(CaptionButton button) + { + MinMaxCloseControl().HoverButton(button); + } + void TitlebarControl::PressButton(CaptionButton button) + { + MinMaxCloseControl().PressButton(button); + } + winrt::fire_and_forget TitlebarControl::ClickButton(CaptionButton button) + { + // GH#8587: Handle this on the _next_ pass of the UI thread. If we + // handle this immediately, then we'll accidentally leave the button in + // the "Hovered" state when we minimize. This will leave the button + // visibly hovered in the taskbar preview for our window. + auto weakThis{ get_weak() }; + co_await MinMaxCloseControl().Dispatcher(); + if (auto self{ weakThis.get() }) + { + // Just handle this in the same way we would if the button were + // clicked normally. + switch (button) + { + case CaptionButton::Minimize: + Minimize_Click(nullptr, nullptr); + break; + case CaptionButton::Maximize: + Maximize_Click(nullptr, nullptr); + break; + case CaptionButton::Close: + Close_Click(nullptr, nullptr); + break; + } + } + } + void TitlebarControl::ReleaseButtons() + { + MinMaxCloseControl().ReleaseButtons(); + } + } diff --git a/src/cascadia/TerminalApp/TitlebarControl.h b/src/cascadia/TerminalApp/TitlebarControl.h index a30f347d8..02c1c37a9 100644 --- a/src/cascadia/TerminalApp/TitlebarControl.h +++ b/src/cascadia/TerminalApp/TitlebarControl.h @@ -1,14 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// -// Declaration of the MainUserControl class. -// #pragma once -#include "winrt/Windows.UI.Xaml.h" -#include "winrt/Windows.UI.Xaml.Markup.h" -#include "winrt/Windows.UI.Xaml.Interop.h" #include "TitlebarControl.g.h" namespace winrt::TerminalApp::implementation @@ -17,6 +11,12 @@ namespace winrt::TerminalApp::implementation { TitlebarControl(uint64_t handle); + void HoverButton(CaptionButton button); + void PressButton(CaptionButton button); + winrt::fire_and_forget ClickButton(CaptionButton button); + void ReleaseButtons(); + double CaptionButtonWidth(); + IInspectable Content(); void Content(IInspectable content); diff --git a/src/cascadia/TerminalApp/TitlebarControl.idl b/src/cascadia/TerminalApp/TitlebarControl.idl index 104f7a2f2..53e4be947 100644 --- a/src/cascadia/TerminalApp/TitlebarControl.idl +++ b/src/cascadia/TerminalApp/TitlebarControl.idl @@ -10,11 +10,25 @@ namespace TerminalApp WindowVisualStateIconified }; + // For simplicity, make sure that these are the same values as the ones used + // by messages like WM_NCHITTEST + enum CaptionButton { + Minimize = 8, // HTMINBUTTON + Maximize = 9, // HTMAXBUTTON + Close = 20 // HTCLOSE + }; + [default_interface] runtimeclass TitlebarControl : Windows.UI.Xaml.Controls.Grid { TitlebarControl(UInt64 parentWindowHandle); void SetWindowVisualState(WindowVisualState visualState); + void HoverButton(CaptionButton button); + void PressButton(CaptionButton button); + void ClickButton(CaptionButton button); + void ReleaseButtons(); + Double CaptionButtonWidth { get; }; + IInspectable Content; Windows.UI.Xaml.Controls.Border DragBar { get; }; } diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index 7eee80e77..50c68fd86 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -87,51 +87,203 @@ void NonClientIslandWindow::MakeWindow() noexcept THROW_HR_IF_NULL(E_UNEXPECTED, _dragBarWindow); } -// Function Description: -// - The window procedure for the drag bar forwards clicks on its client area to its parent as non-client clicks. -LRESULT NonClientIslandWindow::_InputSinkMessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept +LRESULT NonClientIslandWindow::_dragBarNcHitTest(const til::point& pointer) { - std::optional nonClientMessage{ std::nullopt }; + RECT rcParent = GetWindowRect(); + // The size of the buttons doesn't change over the life of the application. + const auto buttonWidthInDips{ _titlebar.CaptionButtonWidth() }; - // translate WM_ messages on the window to WM_NC* on the top level window + // However, the DPI scaling might, so get the updated size of the buttons in pixels + const auto buttonWidthInPixels{ buttonWidthInDips * GetCurrentDpiScale() }; + + // make sure to account for the width of the window frame! + const til::rectangle nonClientFrame{ GetNonClientFrame(_currentDpi) }; + const auto rightBorder{ rcParent.right - nonClientFrame.right() }; + // From the right to the left, + // * are we in the close button? + // * the maximize button? + // * the minimize button? + // If we're not, then we're in either the top resize border, or just + // generally in the titlebar. + if ((rightBorder - pointer.x()) < (buttonWidthInPixels)) + { + return HTCLOSE; + } + else if ((rightBorder - pointer.x()) < (buttonWidthInPixels * 2)) + { + return HTMAXBUTTON; + } + else if ((rightBorder - pointer.x()) < (buttonWidthInPixels * 3)) + { + return HTMINBUTTON; + } + else + { + // If we're not on a caption button, then check if we're on the top + // border. If we're not on the top border, then we're just generally in + // the caption area. + const auto resizeBorderHeight = _GetResizeHandleHeight(); + const auto isOnResizeBorder = pointer.y() < rcParent.top + resizeBorderHeight; + + return isOnResizeBorder ? HTTOP : HTCAPTION; + } +} + +// Function Description: +// - The window procedure for the drag bar forwards clicks on its client area to +// its parent as non-client clicks. +// - BODGY: It also _manually_ handles the caption buttons. They exist in the +// titlebar, and work reasonably well with just XAML, if the drag bar isn't +// covering them. +// - However, to get snap layout support (GH#9443), we need to actually return +// HTMAXBUTTON where the maximize button is. If the drag bar doesn't cover the +// caption buttons, then the core input site (which takes up the entirety of +// the XAML island) will steal the WM_NCHITTEST before we get a chance to +// handle it. +// - So, the drag bar covers the caption buttons, and manually handles hovering +// and pressing them when needed. This gives the impression that they're +// getting input as they normally would, even if they're not _really_ getting +// input via XAML. +LRESULT NonClientIslandWindow::_InputSinkMessageHandler(UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept +{ switch (message) { - case WM_LBUTTONDOWN: - nonClientMessage = WM_NCLBUTTONDOWN; - break; - case WM_LBUTTONDBLCLK: - nonClientMessage = WM_NCLBUTTONDBLCLK; - break; - case WM_LBUTTONUP: - nonClientMessage = WM_NCLBUTTONUP; - break; - case WM_RBUTTONDOWN: - nonClientMessage = WM_NCRBUTTONDOWN; - break; - case WM_RBUTTONDBLCLK: - nonClientMessage = WM_NCRBUTTONDBLCLK; - break; - case WM_RBUTTONUP: - nonClientMessage = WM_NCRBUTTONUP; - break; - } - - if (nonClientMessage.has_value()) + case WM_NCHITTEST: { - const POINT clientPt{ GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) }; - POINT screenPt{ clientPt }; - if (ClientToScreen(_dragBarWindow.get(), &screenPt)) + // Try to determine what part of the window is being hovered here. This + // is absolutely critical to making sure Snap Layouts (GH#9443) works! + return _dragBarNcHitTest(til::point{ GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) }); + } + break; + + case WM_NCMOUSEMOVE: + // When we get this message, it's because the mouse moved when it was + // over somewhere we said was the non-client area. + // + // We'll use this to communicate state to the title bar control, so that + // it can update its visuals. + // - If we're over a button, hover it. + // - If we're over _anything else_, stop hovering the buttons. + switch (wparam) { + case HTTOP: + case HTCAPTION: + { + _titlebar.ReleaseButtons(); + + // Pass caption-related nonclient messages to the parent window. + // Make sure to do this for the HTTOP, which is the top resize + // border, so we can resize the window on the top. auto parentWindow{ GetHandle() }; - - const LPARAM newLparam = MAKELPARAM(screenPt.x, screenPt.y); - // Hit test the parent window at the screen coordinates the user clicked in the drag input sink window, - // then pass that click through as an NC click in that location. - const LRESULT hitTest{ SendMessage(parentWindow, WM_NCHITTEST, 0, newLparam) }; - SendMessage(parentWindow, nonClientMessage.value(), hitTest, newLparam); - - return 0; + return SendMessage(parentWindow, message, wparam, lparam); } + case HTMINBUTTON: + case HTMAXBUTTON: + case HTCLOSE: + _titlebar.HoverButton(static_cast(wparam)); + break; + default: + _titlebar.ReleaseButtons(); + } + + // If we haven't previously asked for mouse tracking, request mouse + // tracking. We need to do this so we can get the WM_NCMOUSELEAVE + // message when the mouse leave the titlebar. Otherwise, we won't always + // get that message (especially if the user moves the mouse _real + // fast_). + if (!_trackingMouse && + (wparam == HTMINBUTTON || wparam == HTMAXBUTTON || wparam == HTCLOSE)) + { + TRACKMOUSEEVENT ev{}; + ev.cbSize = sizeof(TRACKMOUSEEVENT); + // TME_NONCLIENT is absolutely critical here. In my experimentation, + // we'd get WM_MOUSELEAVE messages after just a HOVER_DEFAULT + // timeout even though we're not requesting TME_HOVER, which kinda + // ruined the whole point of this. + ev.dwFlags = TME_LEAVE | TME_NONCLIENT; + ev.hwndTrack = _dragBarWindow.get(); + ev.dwHoverTime = HOVER_DEFAULT; // we don't _really_ care about this. + LOG_IF_WIN32_BOOL_FALSE(TrackMouseEvent(&ev)); + _trackingMouse = true; + } + break; + + case WM_NCMOUSELEAVE: + case WM_MOUSELEAVE: + // When the mouse leaves the drag rect, make sure to dismiss any hover. + _titlebar.ReleaseButtons(); + _trackingMouse = false; + break; + + // NB: *Shouldn't be forwarding these* when they're not over the caption + // because they can inadvertently take action using the system's default + // metrics instead of our own. + case WM_NCLBUTTONDOWN: + case WM_NCLBUTTONDBLCLK: + // Manual handling for mouse clicks in the drag bar. If it's in a + // caption button, then tell the titlebar to "press" the button, which + // should change its visual state. + // + // If it's not in a caption button, then just forward the message along + // to the root HWND. Make sure to do this for the HTTOP, which is the + // top resize border. + switch (wparam) + { + case HTTOP: + case HTCAPTION: + { + // Pass caption-related nonclient messages to the parent window. + auto parentWindow{ GetHandle() }; + return SendMessage(parentWindow, message, wparam, lparam); + } + // The buttons won't work as you'd expect; we need to handle those + // ourselves. + case HTMINBUTTON: + case HTMAXBUTTON: + case HTCLOSE: + _titlebar.PressButton(static_cast(wparam)); + break; + } + return 0; + + case WM_NCLBUTTONUP: + // Manual handling for mouse RELEASES in the drag bar. If it's in a + // caption button, then manually handle what we'd expect for that button. + // + // If it's not in a caption button, then just forward the message along + // to the root HWND. + switch (wparam) + { + case HTTOP: + case HTCAPTION: + { + // Pass caption-related nonclient messages to the parent window. + // The buttons won't work as you'd expect; we need to handle those ourselves. + auto parentWindow{ GetHandle() }; + return SendMessage(parentWindow, message, wparam, lparam); + } + break; + + // If we do find a button, then tell the titlebar to raise the same + // event that would be raised if it were "tapped" + case HTMINBUTTON: + case HTMAXBUTTON: + case HTCLOSE: + _titlebar.ReleaseButtons(); + _titlebar.ClickButton(static_cast(wparam)); + break; + } + return 0; + + // Make sure to pass along right-clicks in this region to our parent window + // - we don't need to handle these. + case WM_NCRBUTTONDOWN: + case WM_NCRBUTTONDBLCLK: + case WM_NCRBUTTONUP: + auto parentWindow{ GetHandle() }; + return SendMessage(parentWindow, message, wparam, lparam); } return DefWindowProc(_dragBarWindow.get(), message, wparam, lparam); @@ -279,10 +431,17 @@ RECT NonClientIslandWindow::_GetDragAreaRect() const noexcept { const auto scale = GetCurrentDpiScale(); const auto transform = _dragBar.TransformToVisual(_rootGrid); + + // GH#9443: Previously, we'd only extend the drag bar from the left of + // the tabs to the right of the caption buttons. Now, we're extending it + // all the way to the right side of the window, covering the caption + // buttons. We'll manually handle input to those buttons, to make it + // seem like they're still getting XAML input. We do this so we can get + // snap layout support for the maximize button. const auto logicalDragBarRect = winrt::Windows::Foundation::Rect{ 0.0f, 0.0f, - static_cast(_dragBar.ActualWidth()), + static_cast(_rootGrid.ActualWidth()), static_cast(_dragBar.ActualHeight()) }; const auto clientDragBarRect = transform.TransformBounds(logicalDragBarRect); @@ -550,6 +709,7 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept // 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) { // If we're the quake window, suppress resizing on any side except the diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index cae2bc0da..879da45f6 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -62,6 +62,7 @@ private: winrt::Windows::UI::Xaml::ElementTheme _theme; bool _isMaximized; + bool _trackingMouse{ false }; [[nodiscard]] static LRESULT __stdcall _StaticInputSinkWndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; [[nodiscard]] LRESULT _InputSinkMessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; @@ -71,6 +72,7 @@ private: int _GetResizeHandleHeight() const noexcept; RECT _GetDragAreaRect() const noexcept; int _GetTopBorderHeight() const noexcept; + LRESULT _dragBarNcHitTest(const til::point& pointer); [[nodiscard]] LRESULT _OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept override; [[nodiscard]] LRESULT _OnNcCalcSize(const WPARAM wParam, const LPARAM lParam) noexcept;