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

Replace the HRGN-based titlebar cutout with an overlay window #5485

Merged
merged 23 commits into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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: 5 additions & 1 deletion .github/actions/spell-check/dictionary/apis.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
IMap
ICustom
IMap
IObject
LCID
NCHITTEST
NCLBUTTONDBLCLK
NCRBUTTONDBLCLK
NOREDIRECTIONBITMAP
rfind
SIZENS
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 @@ -649,7 +649,6 @@ DPICHANGE
DPICHANGED
dpix
dpiy
draggable
DRAWFRAME
DRAWITEM
DRAWITEMSTRUCT
Expand Down Expand Up @@ -781,7 +780,6 @@ fdc
fdd
fde
fdopen
fdpi
fdw
fea
fesb
Expand Down Expand Up @@ -1026,7 +1024,6 @@ HPROPSHEETPAGE
HREDRAW
HREF
hresult
hrgn
HRSRC
hscroll
hsl
Expand Down Expand Up @@ -1489,7 +1486,6 @@ nbsp
Nc
NCCALCSIZE
NCCREATE
NCHITTEST'ed
NCLBUTTONDOWN
NCLBUTTONUP
NCMBUTTONDOWN
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
2 changes: 1 addition & 1 deletion src/cascadia/WindowsTerminal/IslandWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class IslandWindow :
IslandWindow() noexcept;
virtual ~IslandWindow() override;

void MakeWindow() noexcept;
virtual void MakeWindow() noexcept;
void Close();
virtual void OnSize(const UINT width, const UINT height);

Expand Down
247 changes: 183 additions & 64 deletions src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
#include "../types/inc/utils.hpp"
#include "TerminalThemeHelpers.h"

extern "C" IMAGE_DOS_HEADER __ImageBase;

using namespace winrt::Windows::UI;
using namespace winrt::Windows::UI::Composition;
using namespace winrt::Windows::UI::Xaml;
Expand All @@ -32,6 +30,135 @@ NonClientIslandWindow::~NonClientIslandWindow()
{
}

static constexpr const wchar_t* dragBarClassName{ L"DRAG_BAR_WINDOW_CLASS" };

[[nodiscard]] LRESULT __stdcall NonClientIslandWindow::_StaticInputSinkWndProc(HWND const window, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
{
WINRT_ASSERT(window);

if (WM_NCCREATE == message)
{
auto cs = reinterpret_cast<CREATESTRUCT*>(lparam);
auto nonClientIslandWindow{ reinterpret_cast<NonClientIslandWindow*>(cs->lpCreateParams) };
SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(nonClientIslandWindow));
// fall through to default window procedure
}
else if (auto nonClientIslandWindow{ reinterpret_cast<NonClientIslandWindow*>(GetWindowLongPtr(window, GWLP_USERDATA)) })
{
return nonClientIslandWindow->_InputSinkMessageHandler(message, wparam, lparam);
}

return DefWindowProc(window, message, wparam, lparam);
}

void NonClientIslandWindow::MakeWindow() noexcept
{
IslandWindow::MakeWindow();

static ATOM dragBarWindowClass{ []() {
WNDCLASSEX wcEx{};
wcEx.cbSize = sizeof(wcEx);
wcEx.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
wcEx.lpszClassName = dragBarClassName;
wcEx.hbrBackground = reinterpret_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
wcEx.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcEx.lpfnWndProc = &NonClientIslandWindow::_StaticInputSinkWndProc;
wcEx.hInstance = wil::GetModuleInstanceHandle();
wcEx.cbWndExtra = sizeof(NonClientIslandWindow*);
return RegisterClassEx(&wcEx);
}() };

// 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.
_dragBarWindow.reset(CreateWindowExW(WS_EX_LAYERED | WS_EX_NOREDIRECTIONBITMAP,
dragBarClassName,
L"",
WS_CHILD,
0,
0,
0,
0,
GetWindowHandle(),
nullptr,
wil::GetModuleInstanceHandle(),
this));
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 __stdcall NonClientIslandWindow::_InputSinkMessageHandler(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())
{
const POINT clientPt{ GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) };
POINT screenPt{ clientPt };
if (ClientToScreen(_dragBarWindow.get(), &screenPt))
{
auto parentWindow{ GetWindowHandle() };

// 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, MAKELPARAM(screenPt.x, screenPt.y)) };
SendMessage(parentWindow, nonClientMessage.value(), hitTest, 0);

return 0;
}
}

return DefWindowProc(_dragBarWindow.get(), message, wparam, lparam);
}

// Method Description:
// - Resizes and shows/hides the drag bar input sink window.
// This window is used to capture clicks on the non-client area.
void NonClientIslandWindow::_ResizeDragBarWindow() noexcept
{
const til::rectangle rect{ _GetDragAreaRect() };
if (_IsTitlebarVisible() && rect.size().area() > 0)
{
SetWindowPos(_dragBarWindow.get(),
HWND_TOP,
rect.left<int>(),
rect.top<int>() + _GetTopBorderHeight(),
rect.width<int>(),
rect.height<int>(),
SWP_NOACTIVATE | SWP_SHOWWINDOW);
SetLayeredWindowAttributes(_dragBarWindow.get(), 0, 255, LWA_ALPHA);
zadjii-msft marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
SetWindowPos(_dragBarWindow.get(), HWND_BOTTOM, 0, 0, 0, 0, SWP_HIDEWINDOW | SWP_NOMOVE | SWP_NOSIZE);
}
}

// 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 +168,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();
_ResizeDragBarWindow();
}

void NonClientIslandWindow::OnAppInitialized()
Expand Down Expand Up @@ -141,7 +268,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 +357,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 +374,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();
_ResizeDragBarWindow();

_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 @@ -475,6 +550,46 @@ int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept
return HTCAPTION;
}

// Method Description:
// - Sets the cursor to the sizing cursor when we hit-test the top sizing border.
// We need to do this because we've covered it up with a child window.
[[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.
Comment on lines +560 to +565
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is mental

const auto screenPtLparam{ GetMessagePos() };
const LRESULT hitTest{ SendMessage(GetWindowHandle(), WM_NCHITTEST, 0, screenPtLparam) };
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 @@ -558,10 +673,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();
_ResizeDragBarWindow();
return 0;
case WM_NCCALCSIZE:
return _OnNcCalcSize(wParam, lParam);
Expand All @@ -575,10 +692,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 @@ -720,7 +839,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();
_ResizeDragBarWindow();
}

// Method Description:
Expand Down
Loading