Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kill HRGN #4778

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
impl
NCLBUTTONDBLCLK
NCRBUTTONDBLCLK
NOREDIRECTIONBITMAP
SIZENS
xy
4 changes: 0 additions & 4 deletions .github/actions/spell-check/whitelist/whitelist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,6 @@ DPICHANGE
DPICHANGED
dpix
dpiy
draggable
DRAWFRAME
DRAWITEM
DRAWITEMSTRUCT
Expand Down Expand Up @@ -1051,7 +1050,6 @@ HPROPSHEETPAGE
HREDRAW
HREF
hresult
hrgn
HRSRC
hscroll
hsl
Expand Down Expand Up @@ -1134,7 +1132,6 @@ IIo
IList
ime
Imm
Impl
implementingtextandtextrange
inbox
inclusivity
Expand Down Expand Up @@ -2868,7 +2865,6 @@ xvalue
XVIRTUALSCREEN
XWalk
xxxx
XY
yact
YAML
YCast
Expand Down
6 changes: 0 additions & 6 deletions src/cascadia/TerminalApp/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,11 @@ the MIT License. See LICENSE in the project root for license information. -->
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<!-- Define resources for Dark mode here -->
<!-- The TabViewBackground is used on a control (DragBar, TitleBarControl) whose color is propagated to GDI.
The default background is black or white with an alpha component, as it's intended to be layered on top of
another control. Unfortunately, GDI cannot handle this: we need to either render the XAML to a surface and
sample the pixels out of it, or premultiply the alpha into the background. For obvious reasons, we've chosen
the latter. -->
<SolidColorBrush x:Key="TabViewBackground" Color="#FF333333" />
</ResourceDictionary>

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

Expand Down
224 changes: 162 additions & 62 deletions src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) },
Expand All @@ -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<UINT> 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:
// - <none>
// Return Value:
// - <none>
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<HINSTANCE>(&__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<HINSTANCE>(&__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
Expand All @@ -41,9 +150,9 @@ NonClientIslandWindow::~NonClientIslandWindow()
// Return Value:
// - <none>
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()
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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:
// - <none>
// Return Value:
// - <none>
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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Loading