diff --git a/.github/actions/spell-check/whitelist/c3759587195744a3a2e01e2be4ee44ef1f6ab100.txt b/.github/actions/spell-check/whitelist/c3759587195744a3a2e01e2be4ee44ef1f6ab100.txt new file mode 100644 index 00000000000..72be933dc0e --- /dev/null +++ b/.github/actions/spell-check/whitelist/c3759587195744a3a2e01e2be4ee44ef1f6ab100.txt @@ -0,0 +1,6 @@ +impl +NCLBUTTONDBLCLK +NCRBUTTONDBLCLK +NOREDIRECTIONBITMAP +SIZENS +xy diff --git a/.github/actions/spell-check/whitelist/whitelist.txt b/.github/actions/spell-check/whitelist/whitelist.txt index 6f102a7b72f..001168e62e8 100644 --- a/.github/actions/spell-check/whitelist/whitelist.txt +++ b/.github/actions/spell-check/whitelist/whitelist.txt @@ -663,7 +663,6 @@ DPICHANGE DPICHANGED dpix dpiy -draggable DRAWFRAME DRAWITEM DRAWITEMSTRUCT @@ -1051,7 +1050,6 @@ HPROPSHEETPAGE HREDRAW HREF hresult -hrgn HRSRC hscroll hsl @@ -1134,7 +1132,6 @@ IIo IList ime Imm -Impl implementingtextandtextrange inbox inclusivity @@ -2868,7 +2865,6 @@ xvalue XVIRTUALSCREEN XWalk xxxx -XY yact YAML YCast diff --git a/dep/gsl b/dep/gsl index 7e99e76c976..1212beae777 160000 --- a/dep/gsl +++ b/dep/gsl @@ -1 +1 @@ -Subproject commit 7e99e76c9761d0d0b0848b91f8648830670ee872 +Subproject commit 1212beae777dba02c230ece8c0c0ec12790047ea diff --git a/src/cascadia/TerminalApp/App.xaml b/src/cascadia/TerminalApp/App.xaml index de12e725eb5..a12620b8db8 100644 --- a/src/cascadia/TerminalApp/App.xaml +++ b/src/cascadia/TerminalApp/App.xaml @@ -41,17 +41,11 @@ the MIT License. See LICENSE in the project root for license information. --> - - diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp index bb52c75408e..0cb4ff0b1d6 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp @@ -20,6 +20,8 @@ using namespace ::Microsoft::Console::Types; static constexpr int AutohideTaskbarSize = 2; +ATOM NonClientIslandWindow::_dragBarWindowClass = 0; + NonClientIslandWindow::NonClientIslandWindow(const ElementTheme& requestedTheme) noexcept : IslandWindow{}, _backgroundBrushColor{ RGB(0, 0, 0) }, @@ -32,6 +34,113 @@ NonClientIslandWindow::~NonClientIslandWindow() { } +LRESULT __stdcall NonClientIslandWindow::_DragWindowWndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept +{ + std::optional nonClientMessage{ std::nullopt }; + + // translate WM_ messages on the window to WM_NC* on the top level window + 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()) + { + POINT clientPt = { GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) }; + + POINT screenPt = clientPt; + if (ClientToScreen(window, &screenPt)) + { + const auto parentWindow = GetAncestor(window, GA_PARENT); + WINRT_ASSERT(parentWindow != NULL); + + LRESULT hitTest = SendMessage(parentWindow, WM_NCHITTEST, 0, MAKELPARAM(screenPt.x, screenPt.y)); + + SendMessage(parentWindow, nonClientMessage.value(), hitTest, 0); + + return 0; + } + } + + return DefWindowProc(window, message, wparam, lparam); +} + +// Method Description: +// - Create/re-creates the drag bar window. +// - The drag bar window is a child window of the top level window that is put +// right on top of the drag bar. The XAML island window "steals" our mouse +// messages which makes it hard to implement a custom drag area. By putting +// a window on top of it, we prevent it from "stealing" the mouse messages. +// - We have to recreate it, we can't just move it. Otherwise for some reason +// it doesn't get any window message on the new resized area. +// Arguments: +// - +// Return Value: +// - +void NonClientIslandWindow::_RecreateDragBarWindow() noexcept +{ + constexpr const wchar_t* className = L"DRAG_BAR_WINDOW_CLASS"; + + if (_dragBarWindowClass == 0) + { + WNDCLASS wc{}; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hInstance = reinterpret_cast(&__ImageBase); + wc.lpszClassName = className; + wc.style = CS_DBLCLKS; + wc.lpfnWndProc = _DragWindowWndProc; + _dragBarWindowClass = RegisterClass(&wc); + WINRT_ASSERT(_dragBarWindowClass != 0); + } + + const auto dragBarRect = _GetDragAreaRect(); + + // delete previous window + _dragBarWindow = nullptr; + + if (dragBarRect.right - dragBarRect.left > 0 && + dragBarRect.bottom - dragBarRect.top > 0) + { + // WS_EX_LAYERED is required. If it is not present, then for + // some reason, the window will not receive any mouse input. + const auto ret = CreateWindowEx(WS_EX_LAYERED | WS_EX_NOREDIRECTIONBITMAP, + className, + L"", + WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS, + dragBarRect.left, + dragBarRect.top + _GetTopBorderHeight(), + dragBarRect.right - dragBarRect.left, + dragBarRect.bottom - dragBarRect.top, + GetWindowHandle(), + nullptr, + reinterpret_cast(&__ImageBase), + 0); + WINRT_ASSERT(ret != NULL); + + _dragBarWindow = wil::unique_hwnd(ret); + + // bring it on top of the XAML Islands window + WINRT_ASSERT(SetWindowPos(_dragBarWindow.get(), HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOREDRAW)); + } +} + // Method Description: // - Called when the app's size changes. When that happens, the size of the drag // bar may have changed. If it has, we'll need to update the WindowRgn of the @@ -41,9 +150,9 @@ NonClientIslandWindow::~NonClientIslandWindow() // Return Value: // - void NonClientIslandWindow::_OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable /*sender*/, - winrt::Windows::UI::Xaml::SizeChangedEventArgs /*eventArgs*/) const + winrt::Windows::UI::Xaml::SizeChangedEventArgs /*eventArgs*/) { - _UpdateIslandRegion(); + _RecreateDragBarWindow(); } void NonClientIslandWindow::OnAppInitialized() @@ -141,7 +250,7 @@ int NonClientIslandWindow::_GetTopBorderHeight() const noexcept RECT NonClientIslandWindow::_GetDragAreaRect() const noexcept { - if (_dragBar) + if (_dragBar && _dragBar.Visibility() == Visibility::Visible) { const auto scale = GetCurrentDpiScale(); const auto transform = _dragBar.TransformToVisual(_rootGrid); @@ -230,7 +339,6 @@ void NonClientIslandWindow::_UpdateIslandPosition(const UINT windowWidth, const 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, newIslandPos.X, @@ -248,63 +356,12 @@ void NonClientIslandWindow::_UpdateIslandPosition(const UINT windowWidth, const // 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(); + _RecreateDragBarWindow(); _oldIslandPos = { newIslandPos }; } } -// Method Description: -// - Update the region of our window that is the draggable area. This happens in -// response to a OnDragBarSizeChanged event. We'll calculate the areas of the -// window that we want to display XAML content in, and set the window region -// of our child xaml-island window to that region. That way, the parent window -// will still get NCHITTEST'ed _outside_ the XAML content area, for things -// like dragging and resizing. -// - We won't cut this region out if we're fullscreen/borderless. Instead, we'll -// make sure to update our region to take the entirety of the window. -// Arguments: -// - -// Return Value: -// - -void NonClientIslandWindow::_UpdateIslandRegion() const -{ - if (!_interopWindowHandle || !_dragBar) - { - return; - } - - // If we're showing the titlebar (when we're not fullscreen/borderless), cut - // a region of the window out for the drag bar. Otherwise we want the entire - // window to be given to the XAML island - if (_IsTitlebarVisible()) - { - 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)); - } - else - { - const auto windowRect = GetWindowRect(); - const auto width = windowRect.right - windowRect.left; - const auto height = windowRect.bottom - windowRect.top; - - auto windowRegion = wil::unique_hrgn(CreateRectRgn(0, 0, width, height)); - winrt::check_bool(SetWindowRgn(_interopWindowHandle, windowRegion.get(), true)); - } -} - // Method Description: // - Returns the height of the little space at the top of the window used to // resize the window. @@ -473,6 +530,45 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept return HTCAPTION; } +[[nodiscard]] LRESULT NonClientIslandWindow::_OnSetCursor(WPARAM wParam, LPARAM lParam) const noexcept +{ + if (LOWORD(lParam) == HTCLIENT) + { + // Get the cursor position from the _last message_ and not from + // `GetCursorPos` (which returns the cursor position _at the + // moment_) because if we're lagging behind the cursor's position, + // we still want to get the cursor position that was associated + // with that message at the time it was sent to handle the message + // correctly. + const auto screenPtDword = GetMessagePos(); + POINT screenPt = { GET_X_LPARAM(screenPtDword), GET_Y_LPARAM(screenPtDword) }; + + LRESULT hitTest = SendMessage(GetWindowHandle(), WM_NCHITTEST, 0, MAKELPARAM(screenPt.x, screenPt.y)); + if (hitTest == HTTOP) + { + // We have to set the vertical resize cursor manually on + // the top resize handle because Windows thinks that the + // cursor is on the client area because it asked the asked + // the drag window with `WM_NCHITTEST` and it returned + // `HTCLIENT`. + // We don't want to modify the drag window's `WM_NCHITTEST` + // handling to return `HTTOP` because otherwise, the system + // would resize the drag window instead of the top level + // window! + SetCursor(LoadCursor(nullptr, IDC_SIZENS)); + return TRUE; + } + else + { + // reset cursor + SetCursor(LoadCursor(nullptr, IDC_ARROW)); + return TRUE; + } + } + + return DefWindowProc(GetWindowHandle(), WM_SETCURSOR, wParam, lParam); +} + // Method Description: // - Gets the difference between window and client area size. // Arguments: @@ -556,10 +652,12 @@ void NonClientIslandWindow::_UpdateFrameMargins() const noexcept { switch (message) { + case WM_SETCURSOR: + return _OnSetCursor(wParam, lParam); case WM_DISPLAYCHANGE: // GH#4166: When the DPI of the monitor changes out from underneath us, // resize our drag bar, to reflect its newly scaled size. - _UpdateIslandRegion(); + _RecreateDragBarWindow(); return 0; case WM_NCCALCSIZE: return _OnNcCalcSize(wParam, lParam); @@ -573,10 +671,12 @@ void NonClientIslandWindow::_UpdateFrameMargins() const noexcept } // 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). +// - This method is called when the window receives the WM_PAINT message. +// - It paints the client area with the color of the title bar to hide the +// system's title bar behind the XAML Islands window during a resize. +// Indeed, the XAML Islands window doesn't resize at the same time than +// the top level window +// (see https://github.com/microsoft/microsoft-ui-xaml/issues/759). // Return Value: // - The value returned from the window proc. [[nodiscard]] LRESULT NonClientIslandWindow::_OnPaint() noexcept @@ -718,7 +818,7 @@ void NonClientIslandWindow::_SetIsFullscreen(const bool fullscreenEnabled) // always get another window message to trigger us to remove the drag bar. // So, make sure to update the size of the drag region here, so that it // _definitely_ goes away. - _UpdateIslandRegion(); + _RecreateDragBarWindow(); } // Method Description: diff --git a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h index 6fdec25c827..db213a802df 100644 --- a/src/cascadia/WindowsTerminal/NonClientIslandWindow.h +++ b/src/cascadia/WindowsTerminal/NonClientIslandWindow.h @@ -46,6 +46,8 @@ class NonClientIslandWindow : public IslandWindow void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) override; private: + static ATOM _dragBarWindowClass; + std::optional _oldIslandPos; winrt::TerminalApp::TitlebarControl _titlebar{ nullptr }; @@ -55,12 +57,16 @@ class NonClientIslandWindow : public IslandWindow COLORREF _backgroundBrushColor; winrt::Windows::UI::Xaml::Controls::Border _dragBar{ nullptr }; - wil::unique_hrgn _dragBarRegion; + wil::unique_hwnd _dragBarWindow; winrt::Windows::UI::Xaml::ElementTheme _theme; bool _isMaximized; + [[nodiscard]] static LRESULT __stdcall _DragWindowWndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; + + void _RecreateDragBarWindow() noexcept; + int _GetResizeHandleHeight() const noexcept; RECT _GetDragAreaRect() const noexcept; int _GetTopBorderHeight() const noexcept; @@ -69,8 +75,9 @@ class NonClientIslandWindow : public IslandWindow [[nodiscard]] LRESULT _OnNcCalcSize(const WPARAM wParam, const LPARAM lParam) noexcept; [[nodiscard]] LRESULT _OnNcHitTest(POINT ptMouse) const noexcept; [[nodiscard]] LRESULT _OnPaint() noexcept; + [[nodiscard]] LRESULT _OnSetCursor(WPARAM wParam, LPARAM lParam) const noexcept; void _OnMaximizeChange() noexcept; - void _OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::SizeChangedEventArgs eventArgs) const; + void _OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::SizeChangedEventArgs eventArgs); void _SetIsFullscreen(const bool fFullscreenEnabled) override; bool _IsTitlebarVisible() const; @@ -78,6 +85,5 @@ class NonClientIslandWindow : public IslandWindow void _UpdateFrameMargins() const noexcept; void _UpdateMaximizedState(); void _UpdateIslandPosition(const UINT windowWidth, const UINT windowHeight); - void _UpdateIslandRegion() const; void _UpdateFrameTheme() const; };