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 12 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
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
213 changes: 153 additions & 60 deletions src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ using namespace winrt::Windows::Foundation::Numerics;
using namespace ::Microsoft::Console;
using namespace ::Microsoft::Console::Types;

ATOM NonClientIslandWindow::_dragBarWindowClass = 0;

NonClientIslandWindow::NonClientIslandWindow(const ElementTheme& requestedTheme) noexcept :
IslandWindow{},
_backgroundBrushColor{ RGB(0, 0, 0) },
Expand All @@ -31,6 +33,106 @@ 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();

// 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 @@ -40,9 +142,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 @@ -219,7 +321,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 @@ -237,63 +338,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 @@ -399,6 +449,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 @@ -480,10 +569,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 @@ -500,10 +591,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
12 changes: 9 additions & 3 deletions src/cascadia/WindowsTerminal/NonClientIslandWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class NonClientIslandWindow : public IslandWindow
void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) override;

private:
static ATOM _dragBarWindowClass;

std::optional<COORD> _oldIslandPos;

winrt::TerminalApp::TitlebarControl _titlebar{ nullptr };
Expand All @@ -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;
Expand All @@ -69,15 +75,15 @@ 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;

void _UpdateFrameMargins() const noexcept;
void _UpdateMaximizedState();
void _UpdateIslandPosition(const UINT windowWidth, const UINT windowHeight);
void _UpdateIslandRegion() const;
void _UpdateFrameTheme() const;
};