Skip to content

Commit 214163e

Browse files
authored
Replace the HRGN-based titlebar cutout with an overlay window (#5485)
Also known as "Kill HRGN II: Kills Regions Dead (#5485)" Copying the description from @greg904 in #4778. --- 8< --- My understanding is that the XAML framework uses another way of getting mouse input that doesn't work with `WM_SYSCOMMAND` with `SC_MOVE`. It looks like it "steals" our mouse messages like `WM_LBUTTONDOWN`. Before, we were cutting (with `HRGN`s) the drag bar part of the XAML islands window in order to catch mouse messages and be able to implement the drag bar that can move the window. However this "cut" doesn't only apply to input (mouse messages) but also to the graphics so we had to paint behind with the same color as the drag bar using GDI to hide the fact that we were cutting the window. The main issue with this is that we have to replicate exactly the rendering on the XAML drag bar using GDI and this is bad because: 1. it's hard to keep track of the right color: if a dialog is open, it will cover the whole window including the drag bar with a transparent white layer and it's hard to keep track of those things. 2. we can't do acrylic with GDI So I found another method, which is to instead put a "drag window" exactly where the drag bar is, but on top of the XAML islands window (in Z order). I've found that this lets us receive the `WM_LBUTTONDOWN` messages. --- >8 --- Dustin's notes: I've based this on the implementation of the input sink window in the UWP application frame host. Tested manually in all configurations (debug, release) with snap, drag, move, double-click and double-click on the resize handle. Tested at 200% scale. Closes #4744 Closes #2100 Closes #4778 (superseded.)
1 parent 79959db commit 214163e

File tree

6 files changed

+198
-79
lines changed

6 files changed

+198
-79
lines changed
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
IMap
21
ICustom
2+
IMap
33
IObject
44
LCID
55
NCHITTEST
6+
NCLBUTTONDBLCLK
7+
NCRBUTTONDBLCLK
8+
NOREDIRECTIONBITMAP
69
rfind
10+
SIZENS

.github/actions/spell-check/whitelist/whitelist.txt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,6 @@ DPICHANGE
649649
DPICHANGED
650650
dpix
651651
dpiy
652-
draggable
653652
DRAWFRAME
654653
DRAWITEM
655654
DRAWITEMSTRUCT
@@ -781,7 +780,6 @@ fdc
781780
fdd
782781
fde
783782
fdopen
784-
fdpi
785783
fdw
786784
fea
787785
fesb
@@ -1026,7 +1024,6 @@ HPROPSHEETPAGE
10261024
HREDRAW
10271025
HREF
10281026
hresult
1029-
hrgn
10301027
HRSRC
10311028
hscroll
10321029
hsl
@@ -1489,7 +1486,6 @@ nbsp
14891486
Nc
14901487
NCCALCSIZE
14911488
NCCREATE
1492-
NCHITTEST'ed
14931489
NCLBUTTONDOWN
14941490
NCLBUTTONUP
14951491
NCMBUTTONDOWN

src/cascadia/TerminalApp/App.xaml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,11 @@ the MIT License. See LICENSE in the project root for license information. -->
4141
<ResourceDictionary.ThemeDictionaries>
4242
<ResourceDictionary x:Key="Dark">
4343
<!-- Define resources for Dark mode here -->
44-
<!-- The TabViewBackground is used on a control (DragBar, TitleBarControl) whose color is propagated to GDI.
45-
The default background is black or white with an alpha component, as it's intended to be layered on top of
46-
another control. Unfortunately, GDI cannot handle this: we need to either render the XAML to a surface and
47-
sample the pixels out of it, or premultiply the alpha into the background. For obvious reasons, we've chosen
48-
the latter. -->
4944
<SolidColorBrush x:Key="TabViewBackground" Color="#FF333333" />
5045
</ResourceDictionary>
5146

5247
<ResourceDictionary x:Key="Light">
5348
<!-- Define resources for Light mode here -->
54-
<!-- See note about premultiplication above. -->
5549
<SolidColorBrush x:Key="TabViewBackground" Color="#FFCCCCCC" />
5650
</ResourceDictionary>
5751

src/cascadia/WindowsTerminal/IslandWindow.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class IslandWindow :
1717
IslandWindow() noexcept;
1818
virtual ~IslandWindow() override;
1919

20-
void MakeWindow() noexcept;
20+
virtual void MakeWindow() noexcept;
2121
void Close();
2222
virtual void OnSize(const UINT width, const UINT height);
2323

src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp

Lines changed: 183 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
#include "../types/inc/utils.hpp"
99
#include "TerminalThemeHelpers.h"
1010

11-
extern "C" IMAGE_DOS_HEADER __ImageBase;
12-
1311
using namespace winrt::Windows::UI;
1412
using namespace winrt::Windows::UI::Composition;
1513
using namespace winrt::Windows::UI::Xaml;
@@ -32,6 +30,135 @@ NonClientIslandWindow::~NonClientIslandWindow()
3230
{
3331
}
3432

33+
static constexpr const wchar_t* dragBarClassName{ L"DRAG_BAR_WINDOW_CLASS" };
34+
35+
[[nodiscard]] LRESULT __stdcall NonClientIslandWindow::_StaticInputSinkWndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
36+
{
37+
WINRT_ASSERT(window);
38+
39+
if (WM_NCCREATE == message)
40+
{
41+
auto cs = reinterpret_cast<CREATESTRUCT*>(lparam);
42+
auto nonClientIslandWindow{ reinterpret_cast<NonClientIslandWindow*>(cs->lpCreateParams) };
43+
SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(nonClientIslandWindow));
44+
// fall through to default window procedure
45+
}
46+
else if (auto nonClientIslandWindow{ reinterpret_cast<NonClientIslandWindow*>(GetWindowLongPtr(window, GWLP_USERDATA)) })
47+
{
48+
return nonClientIslandWindow->_InputSinkMessageHandler(message, wparam, lparam);
49+
}
50+
51+
return DefWindowProc(window, message, wparam, lparam);
52+
}
53+
54+
void NonClientIslandWindow::MakeWindow() noexcept
55+
{
56+
IslandWindow::MakeWindow();
57+
58+
static ATOM dragBarWindowClass{ []() {
59+
WNDCLASSEX wcEx{};
60+
wcEx.cbSize = sizeof(wcEx);
61+
wcEx.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
62+
wcEx.lpszClassName = dragBarClassName;
63+
wcEx.hbrBackground = reinterpret_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
64+
wcEx.hCursor = LoadCursor(nullptr, IDC_ARROW);
65+
wcEx.lpfnWndProc = &NonClientIslandWindow::_StaticInputSinkWndProc;
66+
wcEx.hInstance = wil::GetModuleInstanceHandle();
67+
wcEx.cbWndExtra = sizeof(NonClientIslandWindow*);
68+
return RegisterClassEx(&wcEx);
69+
}() };
70+
71+
// The drag bar window is a child window of the top level window that is put
72+
// right on top of the drag bar. The XAML island window "steals" our mouse
73+
// messages which makes it hard to implement a custom drag area. By putting
74+
// a window on top of it, we prevent it from "stealing" the mouse messages.
75+
_dragBarWindow.reset(CreateWindowExW(WS_EX_LAYERED | WS_EX_NOREDIRECTIONBITMAP,
76+
dragBarClassName,
77+
L"",
78+
WS_CHILD,
79+
0,
80+
0,
81+
0,
82+
0,
83+
GetWindowHandle(),
84+
nullptr,
85+
wil::GetModuleInstanceHandle(),
86+
this));
87+
THROW_HR_IF_NULL(E_UNEXPECTED, _dragBarWindow);
88+
}
89+
90+
// Function Description:
91+
// - The window procedure for the drag bar forwards clicks on its client area to its parent as non-client clicks.
92+
LRESULT __stdcall NonClientIslandWindow::_InputSinkMessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
93+
{
94+
std::optional<UINT> nonClientMessage{ std::nullopt };
95+
96+
// translate WM_ messages on the window to WM_NC* on the top level window
97+
switch (message)
98+
{
99+
case WM_LBUTTONDOWN:
100+
nonClientMessage = WM_NCLBUTTONDOWN;
101+
break;
102+
case WM_LBUTTONDBLCLK:
103+
nonClientMessage = WM_NCLBUTTONDBLCLK;
104+
break;
105+
case WM_LBUTTONUP:
106+
nonClientMessage = WM_NCLBUTTONUP;
107+
break;
108+
case WM_RBUTTONDOWN:
109+
nonClientMessage = WM_NCRBUTTONDOWN;
110+
break;
111+
case WM_RBUTTONDBLCLK:
112+
nonClientMessage = WM_NCRBUTTONDBLCLK;
113+
break;
114+
case WM_RBUTTONUP:
115+
nonClientMessage = WM_NCRBUTTONUP;
116+
break;
117+
}
118+
119+
if (nonClientMessage.has_value())
120+
{
121+
const POINT clientPt{ GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) };
122+
POINT screenPt{ clientPt };
123+
if (ClientToScreen(_dragBarWindow.get(), &screenPt))
124+
{
125+
auto parentWindow{ GetWindowHandle() };
126+
127+
// Hit test the parent window at the screen coordinates the user clicked in the drag input sink window,
128+
// then pass that click through as an NC click in that location.
129+
const LRESULT hitTest{ SendMessage(parentWindow, WM_NCHITTEST, 0, MAKELPARAM(screenPt.x, screenPt.y)) };
130+
SendMessage(parentWindow, nonClientMessage.value(), hitTest, 0);
131+
132+
return 0;
133+
}
134+
}
135+
136+
return DefWindowProc(_dragBarWindow.get(), message, wparam, lparam);
137+
}
138+
139+
// Method Description:
140+
// - Resizes and shows/hides the drag bar input sink window.
141+
// This window is used to capture clicks on the non-client area.
142+
void NonClientIslandWindow::_ResizeDragBarWindow() noexcept
143+
{
144+
const til::rectangle rect{ _GetDragAreaRect() };
145+
if (_IsTitlebarVisible() && rect.size().area() > 0)
146+
{
147+
SetWindowPos(_dragBarWindow.get(),
148+
HWND_TOP,
149+
rect.left<int>(),
150+
rect.top<int>() + _GetTopBorderHeight(),
151+
rect.width<int>(),
152+
rect.height<int>(),
153+
SWP_NOACTIVATE | SWP_SHOWWINDOW);
154+
SetLayeredWindowAttributes(_dragBarWindow.get(), 0, 255, LWA_ALPHA);
155+
}
156+
else
157+
{
158+
SetWindowPos(_dragBarWindow.get(), HWND_BOTTOM, 0, 0, 0, 0, SWP_HIDEWINDOW | SWP_NOMOVE | SWP_NOSIZE);
159+
}
160+
}
161+
35162
// Method Description:
36163
// - Called when the app's size changes. When that happens, the size of the drag
37164
// bar may have changed. If it has, we'll need to update the WindowRgn of the
@@ -41,9 +168,9 @@ NonClientIslandWindow::~NonClientIslandWindow()
41168
// Return Value:
42169
// - <none>
43170
void NonClientIslandWindow::_OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable /*sender*/,
44-
winrt::Windows::UI::Xaml::SizeChangedEventArgs /*eventArgs*/) const
171+
winrt::Windows::UI::Xaml::SizeChangedEventArgs /*eventArgs*/)
45172
{
46-
_UpdateIslandRegion();
173+
_ResizeDragBarWindow();
47174
}
48175

49176
void NonClientIslandWindow::OnAppInitialized()
@@ -141,7 +268,7 @@ int NonClientIslandWindow::_GetTopBorderHeight() const noexcept
141268

142269
RECT NonClientIslandWindow::_GetDragAreaRect() const noexcept
143270
{
144-
if (_dragBar)
271+
if (_dragBar && _dragBar.Visibility() == Visibility::Visible)
145272
{
146273
const auto scale = GetCurrentDpiScale();
147274
const auto transform = _dragBar.TransformToVisual(_rootGrid);
@@ -230,7 +357,6 @@ void NonClientIslandWindow::_UpdateIslandPosition(const UINT windowWidth, const
230357

231358
const COORD newIslandPos = { 0, topBorderHeight };
232359

233-
// I'm not sure that HWND_BOTTOM does anything different than HWND_TOP for us.
234360
winrt::check_bool(SetWindowPos(_interopWindowHandle,
235361
HWND_BOTTOM,
236362
newIslandPos.X,
@@ -248,63 +374,12 @@ void NonClientIslandWindow::_UpdateIslandPosition(const UINT windowWidth, const
248374
// NonClientIslandWindow::OnDragBarSizeChanged method because this
249375
// method is only called when the position of the drag bar changes
250376
// **inside** the island which is not the case here.
251-
_UpdateIslandRegion();
377+
_ResizeDragBarWindow();
252378

253379
_oldIslandPos = { newIslandPos };
254380
}
255381
}
256382

257-
// Method Description:
258-
// - Update the region of our window that is the draggable area. This happens in
259-
// response to a OnDragBarSizeChanged event. We'll calculate the areas of the
260-
// window that we want to display XAML content in, and set the window region
261-
// of our child xaml-island window to that region. That way, the parent window
262-
// will still get NCHITTEST'ed _outside_ the XAML content area, for things
263-
// like dragging and resizing.
264-
// - We won't cut this region out if we're fullscreen/borderless. Instead, we'll
265-
// make sure to update our region to take the entirety of the window.
266-
// Arguments:
267-
// - <none>
268-
// Return Value:
269-
// - <none>
270-
void NonClientIslandWindow::_UpdateIslandRegion() const
271-
{
272-
if (!_interopWindowHandle || !_dragBar)
273-
{
274-
return;
275-
}
276-
277-
// If we're showing the titlebar (when we're not fullscreen/borderless), cut
278-
// a region of the window out for the drag bar. Otherwise we want the entire
279-
// window to be given to the XAML island
280-
if (_IsTitlebarVisible())
281-
{
282-
RECT rcIsland;
283-
winrt::check_bool(::GetWindowRect(_interopWindowHandle, &rcIsland));
284-
const auto islandWidth = rcIsland.right - rcIsland.left;
285-
const auto islandHeight = rcIsland.bottom - rcIsland.top;
286-
const auto totalRegion = wil::unique_hrgn(CreateRectRgn(0, 0, islandWidth, islandHeight));
287-
288-
const auto rcDragBar = _GetDragAreaRect();
289-
const auto dragBarRegion = wil::unique_hrgn(CreateRectRgn(rcDragBar.left, rcDragBar.top, rcDragBar.right, rcDragBar.bottom));
290-
291-
// island region = total region - drag bar region
292-
const auto islandRegion = wil::unique_hrgn(CreateRectRgn(0, 0, 0, 0));
293-
winrt::check_bool(CombineRgn(islandRegion.get(), totalRegion.get(), dragBarRegion.get(), RGN_DIFF));
294-
295-
winrt::check_bool(SetWindowRgn(_interopWindowHandle, islandRegion.get(), true));
296-
}
297-
else
298-
{
299-
const auto windowRect = GetWindowRect();
300-
const auto width = windowRect.right - windowRect.left;
301-
const auto height = windowRect.bottom - windowRect.top;
302-
303-
auto windowRegion = wil::unique_hrgn(CreateRectRgn(0, 0, width, height));
304-
winrt::check_bool(SetWindowRgn(_interopWindowHandle, windowRegion.get(), true));
305-
}
306-
}
307-
308383
// Method Description:
309384
// - Returns the height of the little space at the top of the window used to
310385
// resize the window.
@@ -475,6 +550,46 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept
475550
return HTCAPTION;
476551
}
477552

553+
// Method Description:
554+
// - Sets the cursor to the sizing cursor when we hit-test the top sizing border.
555+
// We need to do this because we've covered it up with a child window.
556+
[[nodiscard]] LRESULT NonClientIslandWindow::_OnSetCursor(WPARAM wParam, LPARAM lParam) const noexcept
557+
{
558+
if (LOWORD(lParam) == HTCLIENT)
559+
{
560+
// Get the cursor position from the _last message_ and not from
561+
// `GetCursorPos` (which returns the cursor position _at the
562+
// moment_) because if we're lagging behind the cursor's position,
563+
// we still want to get the cursor position that was associated
564+
// with that message at the time it was sent to handle the message
565+
// correctly.
566+
const auto screenPtLparam{ GetMessagePos() };
567+
const LRESULT hitTest{ SendMessage(GetWindowHandle(), WM_NCHITTEST, 0, screenPtLparam) };
568+
if (hitTest == HTTOP)
569+
{
570+
// We have to set the vertical resize cursor manually on
571+
// the top resize handle because Windows thinks that the
572+
// cursor is on the client area because it asked the asked
573+
// the drag window with `WM_NCHITTEST` and it returned
574+
// `HTCLIENT`.
575+
// We don't want to modify the drag window's `WM_NCHITTEST`
576+
// handling to return `HTTOP` because otherwise, the system
577+
// would resize the drag window instead of the top level
578+
// window!
579+
SetCursor(LoadCursor(nullptr, IDC_SIZENS));
580+
return TRUE;
581+
}
582+
else
583+
{
584+
// reset cursor
585+
SetCursor(LoadCursor(nullptr, IDC_ARROW));
586+
return TRUE;
587+
}
588+
}
589+
590+
return DefWindowProc(GetWindowHandle(), WM_SETCURSOR, wParam, lParam);
591+
}
592+
478593
// Method Description:
479594
// - Gets the difference between window and client area size.
480595
// Arguments:
@@ -558,10 +673,12 @@ void NonClientIslandWindow::_UpdateFrameMargins() const noexcept
558673
{
559674
switch (message)
560675
{
676+
case WM_SETCURSOR:
677+
return _OnSetCursor(wParam, lParam);
561678
case WM_DISPLAYCHANGE:
562679
// GH#4166: When the DPI of the monitor changes out from underneath us,
563680
// resize our drag bar, to reflect its newly scaled size.
564-
_UpdateIslandRegion();
681+
_ResizeDragBarWindow();
565682
return 0;
566683
case WM_NCCALCSIZE:
567684
return _OnNcCalcSize(wParam, lParam);
@@ -575,10 +692,12 @@ void NonClientIslandWindow::_UpdateFrameMargins() const noexcept
575692
}
576693

577694
// Method Description:
578-
// - This method is called when the window receives the WM_PAINT message. It
579-
// paints the background of the window to the color of the drag bar because
580-
// the drag bar cannot be painted on the window by the XAML Island (see
581-
// NonClientIslandWindow::_UpdateIslandRegion).
695+
// - This method is called when the window receives the WM_PAINT message.
696+
// - It paints the client area with the color of the title bar to hide the
697+
// system's title bar behind the XAML Islands window during a resize.
698+
// Indeed, the XAML Islands window doesn't resize at the same time than
699+
// the top level window
700+
// (see https://github.com/microsoft/microsoft-ui-xaml/issues/759).
582701
// Return Value:
583702
// - The value returned from the window proc.
584703
[[nodiscard]] LRESULT NonClientIslandWindow::_OnPaint() noexcept
@@ -720,7 +839,7 @@ void NonClientIslandWindow::_SetIsFullscreen(const bool fullscreenEnabled)
720839
// always get another window message to trigger us to remove the drag bar.
721840
// So, make sure to update the size of the drag region here, so that it
722841
// _definitely_ goes away.
723-
_UpdateIslandRegion();
842+
_ResizeDragBarWindow();
724843
}
725844

726845
// Method Description:

0 commit comments

Comments
 (0)