terminal/src/cascadia/WindowsTerminal/NonClientIslandWindow.cpp
2021-11-16 13:16:04 -06:00

1133 lines
45 KiB
C++

/********************************************************
* *
* Copyright (C) Microsoft. All rights reserved. *
* *
********************************************************/
#include "pch.h"
#include "NonClientIslandWindow.h"
#include "../types/inc/utils.hpp"
#include "TerminalThemeHelpers.h"
using namespace winrt::Windows::UI;
using namespace winrt::Windows::UI::Composition;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Hosting;
using namespace winrt::Windows::Foundation::Numerics;
using namespace ::Microsoft::Console;
using namespace ::Microsoft::Console::Types;
static constexpr int AutohideTaskbarSize = 2;
NonClientIslandWindow::NonClientIslandWindow(const ElementTheme& requestedTheme) noexcept :
IslandWindow{},
_backgroundBrushColor{ 0, 0, 0 },
_theme{ requestedTheme },
_isMaximized{ false }
{
}
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,
GetHandle(),
nullptr,
wil::GetModuleInstanceHandle(),
this));
THROW_HR_IF_NULL(E_UNEXPECTED, _dragBarWindow);
}
LRESULT NonClientIslandWindow::_dragBarNcHitTest(const til::point& pointer)
{
RECT rcParent = GetWindowRect();
// The size of the buttons doesn't change over the life of the application.
const auto buttonWidthInDips{ _titlebar.CaptionButtonWidth() };
// However, the DPI scaling might, so get the updated size of the buttons in pixels
const auto buttonWidthInPixels{ buttonWidthInDips * GetCurrentDpiScale() };
// make sure to account for the width of the window frame!
const til::rectangle nonClientFrame{ GetNonClientFrame(_currentDpi) };
const auto rightBorder{ rcParent.right - nonClientFrame.right<int>() };
// From the right to the left,
// * are we in the close button?
// * the maximize button?
// * the minimize button?
// If we're not, then we're in either the top resize border, or just
// generally in the titlebar.
if ((rightBorder - pointer.x()) < (buttonWidthInPixels))
{
return HTCLOSE;
}
else if ((rightBorder - pointer.x()) < (buttonWidthInPixels * 2))
{
return HTMAXBUTTON;
}
else if ((rightBorder - pointer.x()) < (buttonWidthInPixels * 3))
{
return HTMINBUTTON;
}
else
{
// If we're not on a caption button, then check if we're on the top
// border. If we're not on the top border, then we're just generally in
// the caption area.
const auto resizeBorderHeight = _GetResizeHandleHeight();
const auto isOnResizeBorder = pointer.y() < rcParent.top + resizeBorderHeight;
return isOnResizeBorder ? HTTOP : HTCAPTION;
}
}
// Function Description:
// - The window procedure for the drag bar forwards clicks on its client area to
// its parent as non-client clicks.
// - BODGY: It also _manually_ handles the caption buttons. They exist in the
// titlebar, and work reasonably well with just XAML, if the drag bar isn't
// covering them.
// - However, to get snap layout support (GH#9443), we need to actually return
// HTMAXBUTTON where the maximize button is. If the drag bar doesn't cover the
// caption buttons, then the core input site (which takes up the entirety of
// the XAML island) will steal the WM_NCHITTEST before we get a chance to
// handle it.
// - So, the drag bar covers the caption buttons, and manually handles hovering
// and pressing them when needed. This gives the impression that they're
// getting input as they normally would, even if they're not _really_ getting
// input via XAML.
LRESULT NonClientIslandWindow::_InputSinkMessageHandler(UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept
{
switch (message)
{
case WM_NCHITTEST:
{
// Try to determine what part of the window is being hovered here. This
// is absolutely critical to making sure Snap Layouts (GH#9443) works!
return _dragBarNcHitTest(til::point{ GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) });
}
break;
case WM_NCMOUSEMOVE:
// When we get this message, it's because the mouse moved when it was
// over somewhere we said was the non-client area.
//
// We'll use this to communicate state to the title bar control, so that
// it can update its visuals.
// - If we're over a button, hover it.
// - If we're over _anything else_, stop hovering the buttons.
switch (wparam)
{
case HTTOP:
case HTCAPTION:
{
_titlebar.ReleaseButtons();
// Pass caption-related nonclient messages to the parent window.
// Make sure to do this for the HTTOP, which is the top resize
// border, so we can resize the window on the top.
auto parentWindow{ GetHandle() };
return SendMessage(parentWindow, message, wparam, lparam);
}
case HTMINBUTTON:
case HTMAXBUTTON:
case HTCLOSE:
_titlebar.HoverButton(static_cast<winrt::TerminalApp::CaptionButton>(wparam));
break;
default:
_titlebar.ReleaseButtons();
}
// If we haven't previously asked for mouse tracking, request mouse
// tracking. We need to do this so we can get the WM_NCMOUSELEAVE
// message when the mouse leave the titlebar. Otherwise, we won't always
// get that message (especially if the user moves the mouse _real
// fast_).
if (!_trackingMouse &&
(wparam == HTMINBUTTON || wparam == HTMAXBUTTON || wparam == HTCLOSE))
{
TRACKMOUSEEVENT ev{};
ev.cbSize = sizeof(TRACKMOUSEEVENT);
// TME_NONCLIENT is absolutely critical here. In my experimentation,
// we'd get WM_MOUSELEAVE messages after just a HOVER_DEFAULT
// timeout even though we're not requesting TME_HOVER, which kinda
// ruined the whole point of this.
ev.dwFlags = TME_LEAVE | TME_NONCLIENT;
ev.hwndTrack = _dragBarWindow.get();
ev.dwHoverTime = HOVER_DEFAULT; // we don't _really_ care about this.
LOG_IF_WIN32_BOOL_FALSE(TrackMouseEvent(&ev));
_trackingMouse = true;
}
break;
case WM_NCMOUSELEAVE:
case WM_MOUSELEAVE:
// When the mouse leaves the drag rect, make sure to dismiss any hover.
_titlebar.ReleaseButtons();
_trackingMouse = false;
break;
// NB: *Shouldn't be forwarding these* when they're not over the caption
// because they can inadvertently take action using the system's default
// metrics instead of our own.
case WM_NCLBUTTONDOWN:
case WM_NCLBUTTONDBLCLK:
// Manual handling for mouse clicks in the drag bar. If it's in a
// caption button, then tell the titlebar to "press" the button, which
// should change its visual state.
//
// If it's not in a caption button, then just forward the message along
// to the root HWND. Make sure to do this for the HTTOP, which is the
// top resize border.
switch (wparam)
{
case HTTOP:
case HTCAPTION:
{
// Pass caption-related nonclient messages to the parent window.
auto parentWindow{ GetHandle() };
return SendMessage(parentWindow, message, wparam, lparam);
}
// The buttons won't work as you'd expect; we need to handle those
// ourselves.
case HTMINBUTTON:
case HTMAXBUTTON:
case HTCLOSE:
_titlebar.PressButton(static_cast<winrt::TerminalApp::CaptionButton>(wparam));
break;
}
return 0;
case WM_NCLBUTTONUP:
// Manual handling for mouse RELEASES in the drag bar. If it's in a
// caption button, then manually handle what we'd expect for that button.
//
// If it's not in a caption button, then just forward the message along
// to the root HWND.
switch (wparam)
{
case HTTOP:
case HTCAPTION:
{
// Pass caption-related nonclient messages to the parent window.
// The buttons won't work as you'd expect; we need to handle those ourselves.
auto parentWindow{ GetHandle() };
return SendMessage(parentWindow, message, wparam, lparam);
}
break;
// If we do find a button, then tell the titlebar to raise the same
// event that would be raised if it were "tapped"
case HTMINBUTTON:
case HTMAXBUTTON:
case HTCLOSE:
_titlebar.ReleaseButtons();
_titlebar.ClickButton(static_cast<winrt::TerminalApp::CaptionButton>(wparam));
break;
}
return 0;
// Make sure to pass along right-clicks in this region to our parent window
// - we don't need to handle these.
case WM_NCRBUTTONDOWN:
case WM_NCRBUTTONDBLCLK:
case WM_NCRBUTTONUP:
auto parentWindow{ GetHandle() };
return SendMessage(parentWindow, message, wparam, lparam);
}
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);
}
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
// interop window.
// Arguments:
// - <unused>
// Return Value:
// - <none>
void NonClientIslandWindow::_OnDragBarSizeChanged(winrt::Windows::Foundation::IInspectable /*sender*/,
winrt::Windows::UI::Xaml::SizeChangedEventArgs /*eventArgs*/)
{
_ResizeDragBarWindow();
}
void NonClientIslandWindow::OnAppInitialized()
{
IslandWindow::OnAppInitialized();
}
void NonClientIslandWindow::Initialize()
{
IslandWindow::Initialize();
_UpdateFrameMargins();
// Set up our grid of content. We'll use _rootGrid as our root element.
// There will be two children of this grid - the TitlebarControl, and the
// "client content"
_rootGrid.Children().Clear();
Controls::RowDefinition titlebarRow{};
Controls::RowDefinition contentRow{};
titlebarRow.Height(GridLengthHelper::Auto());
_rootGrid.RowDefinitions().Append(titlebarRow);
_rootGrid.RowDefinitions().Append(contentRow);
// Create our titlebar control
_titlebar = winrt::TerminalApp::TitlebarControl{ reinterpret_cast<uint64_t>(GetHandle()) };
_dragBar = _titlebar.DragBar();
_dragBar.SizeChanged({ this, &NonClientIslandWindow::_OnDragBarSizeChanged });
_rootGrid.SizeChanged({ this, &NonClientIslandWindow::_OnDragBarSizeChanged });
_rootGrid.Children().Append(_titlebar);
Controls::Grid::SetRow(_titlebar, 0);
// GH#3440 - When the titlebar is loaded (officially added to our UI tree),
// then make sure to update it's visual state to reflect if we're in the
// maximized state on launch.
_titlebar.Loaded([this](auto&&, auto&&) { _OnMaximizeChange(); });
}
// Method Description:
// - Set the content of the "client area" of our window to the given content.
// Arguments:
// - content: the new UI element to use as the client content
// Return Value:
// - <none>
void NonClientIslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content)
{
_clientContent = content;
_rootGrid.Children().Append(content);
// SetRow only works on FrameworkElement's, so cast it to a FWE before
// calling. We know that our content is a Grid, so we don't need to worry
// about this.
const auto fwe = content.try_as<winrt::Windows::UI::Xaml::FrameworkElement>();
if (fwe)
{
Controls::Grid::SetRow(fwe, 1);
}
}
// Method Description:
// - Set the content of the "titlebar area" of our window to the given content.
// Arguments:
// - content: the new UI element to use as the titlebar content
// Return Value:
// - <none>
void NonClientIslandWindow::SetTitlebarContent(winrt::Windows::UI::Xaml::UIElement content)
{
_titlebar.Content(content);
// GH#4288 - add a SizeChanged handler to this content. It's possible that
// this element's size will change after the dragbar's. When that happens,
// the drag bar won't send another SizeChanged event, because the dragbar's
// _size_ didn't change, only it's position.
const auto fwe = content.try_as<winrt::Windows::UI::Xaml::FrameworkElement>();
if (fwe)
{
fwe.SizeChanged({ this, &NonClientIslandWindow::_OnDragBarSizeChanged });
}
}
// Method Description:
// - This method computes the height of the little border above the title bar
// and returns it. If the border is disabled, then this method will return 0.
// Return Value:
// - the height of the border above the title bar or 0 if it's disabled
int NonClientIslandWindow::_GetTopBorderHeight() const noexcept
{
// No border when maximized or fullscreen.
// Yet we still need it in the focus mode to allow dragging (GH#7012)
if (_isMaximized || _fullscreen)
{
return 0;
}
return topBorderVisibleHeight;
}
RECT NonClientIslandWindow::_GetDragAreaRect() const noexcept
{
if (_dragBar && _dragBar.Visibility() == Visibility::Visible)
{
const auto scale = GetCurrentDpiScale();
const auto transform = _dragBar.TransformToVisual(_rootGrid);
// GH#9443: Previously, we'd only extend the drag bar from the left of
// the tabs to the right of the caption buttons. Now, we're extending it
// all the way to the right side of the window, covering the caption
// buttons. We'll manually handle input to those buttons, to make it
// seem like they're still getting XAML input. We do this so we can get
// snap layout support for the maximize button.
const auto logicalDragBarRect = winrt::Windows::Foundation::Rect{
0.0f,
0.0f,
static_cast<float>(_rootGrid.ActualWidth()),
static_cast<float>(_dragBar.ActualHeight())
};
const auto clientDragBarRect = transform.TransformBounds(logicalDragBarRect);
RECT dragBarRect = {
static_cast<LONG>(clientDragBarRect.X * scale),
static_cast<LONG>(clientDragBarRect.Y * scale),
static_cast<LONG>((clientDragBarRect.Width + clientDragBarRect.X) * scale),
static_cast<LONG>((clientDragBarRect.Height + clientDragBarRect.Y) * scale),
};
return dragBarRect;
}
return RECT{};
}
// Method Description:
// - Called when the size of the window changes for any reason. Updates the
// XAML island to match our new sizing and also updates the maximize icon
// if the window went from maximized to restored or the opposite.
void NonClientIslandWindow::OnSize(const UINT width, const UINT height)
{
_UpdateMaximizedState();
if (_interopWindowHandle)
{
_UpdateIslandPosition(width, height);
}
// GH#11367: We need to do this,
// otherwise the titlebar may still be partially visible
// when we move between different DPI monitors.
RefreshCurrentDPI();
_UpdateFrameMargins();
}
// Method Description:
// - Checks if the window has been maximized or restored since the last time.
// If it has been maximized or restored, then it updates the _isMaximized
// flags and notifies of the change by calling
// NonClientIslandWindow::_OnMaximizeChange.
void NonClientIslandWindow::_UpdateMaximizedState()
{
const auto windowStyle = GetWindowStyle(_window.get());
const auto newIsMaximized = WI_IsFlagSet(windowStyle, WS_MAXIMIZE);
if (_isMaximized != newIsMaximized)
{
_isMaximized = newIsMaximized;
_OnMaximizeChange();
}
}
// Method Description:
// - Called when the the windows goes from restored to maximized or from
// maximized to restored. Updates the maximize button's icon and the frame
// margins.
void NonClientIslandWindow::_OnMaximizeChange() noexcept
{
if (_titlebar)
{
const auto windowStyle = GetWindowStyle(_window.get());
const auto isIconified = WI_IsFlagSet(windowStyle, WS_ICONIC);
const auto state = _isMaximized ? winrt::TerminalApp::WindowVisualState::WindowVisualStateMaximized :
isIconified ? winrt::TerminalApp::WindowVisualState::WindowVisualStateIconified :
winrt::TerminalApp::WindowVisualState::WindowVisualStateNormal;
try
{
_titlebar.SetWindowVisualState(state);
}
CATCH_LOG();
}
// no frame margin when maximized
_UpdateFrameMargins();
}
// Method Description:
// - Called when the size of the window changes for any reason. Updates the
// sizes of our child XAML Islands to match our new sizing.
void NonClientIslandWindow::_UpdateIslandPosition(const UINT windowWidth, const UINT windowHeight)
{
const auto originalTopHeight = _GetTopBorderHeight();
// GH#7422
// !! BODGY !!
//
// For inexplicable reasons, the top row of pixels on our tabs, new tab
// button, and caption buttons is totally un-clickable. The mouse simply
// refuses to interact with them. So when we're maximized, on certain
// monitor configurations, this results in the top row of pixels not
// reacting to clicks at all. To obey Fitt's Law, we're gonna shift
// the entire island up one pixel. That will result in the top row of pixels
// in the window actually being the _second_ row of pixels for those
// buttons, which will make them clickable. It's perhaps not the right fix,
// but it works.
// _GetTopBorderHeight() returns 0 when we're maximized.
const short topBorderHeight = ::base::saturated_cast<short>((originalTopHeight == 0) ? -1 : originalTopHeight);
const COORD newIslandPos = { 0, topBorderHeight };
winrt::check_bool(SetWindowPos(_interopWindowHandle,
HWND_BOTTOM,
newIslandPos.X,
newIslandPos.Y,
windowWidth,
windowHeight - topBorderHeight,
SWP_SHOWWINDOW | SWP_NOACTIVATE));
// This happens when we go from maximized to restored or the opposite
// because topBorderHeight changes.
if (!_oldIslandPos.has_value() || _oldIslandPos.value() != newIslandPos)
{
// The drag bar's position changed compared to the client area because
// the island moved but we will not be notified about this in the
// 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.
_ResizeDragBarWindow();
_oldIslandPos = { newIslandPos };
}
}
// Method Description:
// - Returns the height of the little space at the top of the window used to
// resize the window.
// Return Value:
// - the height of the window's top resize handle
int NonClientIslandWindow::_GetResizeHandleHeight() const noexcept
{
// there isn't a SM_CYPADDEDBORDER for the Y axis
return ::GetSystemMetricsForDpi(SM_CXPADDEDBORDER, _currentDpi) +
::GetSystemMetricsForDpi(SM_CYSIZEFRAME, _currentDpi);
}
// Method Description:
// - Responds to the WM_NCCALCSIZE message by calculating and creating the new
// window frame.
[[nodiscard]] LRESULT NonClientIslandWindow::_OnNcCalcSize(const WPARAM wParam, const LPARAM lParam) noexcept
{
if (!wParam)
{
return 0;
}
NCCALCSIZE_PARAMS* params = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);
// Store the original top before the default window proc applies the
// default frame.
const auto originalTop = params->rgrc[0].top;
const auto originalSize = params->rgrc[0];
// apply the default frame
const auto ret = DefWindowProc(_window.get(), WM_NCCALCSIZE, wParam, lParam);
if (ret != 0)
{
return ret;
}
auto newSize = params->rgrc[0];
// Re-apply the original top from before the size of the default frame was applied.
newSize.top = originalTop;
// WM_NCCALCSIZE is called before WM_SIZE
_UpdateMaximizedState();
// We don't need this correction when we're fullscreen. We will have the
// WS_POPUP size, so we don't have to worry about borders, and the default
// frame will be fine.
if (_isMaximized && !_fullscreen)
{
// When a window is maximized, its size is actually a little bit more
// than the monitor's work area. The window is positioned and sized in
// such a way that the resize handles are outside of the monitor and
// then the window is clipped to the monitor so that the resize handle
// do not appear because you don't need them (because you can't resize
// a window when it's maximized unless you restore it).
newSize.top += _GetResizeHandleHeight();
}
// GH#1438 - Attempt to detect if there's an autohide taskbar, and if there
// is, reduce our size a bit on the side with the taskbar, so the user can
// still mouse-over the taskbar to reveal it.
// GH#5209 - make sure to use MONITOR_DEFAULTTONEAREST, so that this will
// still find the right monitor even when we're restoring from minimized.
HMONITOR hMon = MonitorFromWindow(_window.get(), MONITOR_DEFAULTTONEAREST);
if (hMon && (_isMaximized || _fullscreen))
{
MONITORINFO monInfo{ 0 };
monInfo.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(hMon, &monInfo);
// First, check if we have an auto-hide taskbar at all:
APPBARDATA autohide{ 0 };
autohide.cbSize = sizeof(autohide);
UINT state = (UINT)SHAppBarMessage(ABM_GETSTATE, &autohide);
if (WI_IsFlagSet(state, ABS_AUTOHIDE))
{
// This helper can be used to determine if there's a auto-hide
// taskbar on the given edge of the monitor we're currently on.
auto hasAutohideTaskbar = [&monInfo](const UINT edge) -> bool {
APPBARDATA data{ 0 };
data.cbSize = sizeof(data);
data.uEdge = edge;
data.rc = monInfo.rcMonitor;
HWND hTaskbar = (HWND)SHAppBarMessage(ABM_GETAUTOHIDEBAREX, &data);
return hTaskbar != nullptr;
};
const bool onTop = hasAutohideTaskbar(ABE_TOP);
const bool onBottom = hasAutohideTaskbar(ABE_BOTTOM);
const bool onLeft = hasAutohideTaskbar(ABE_LEFT);
const bool onRight = hasAutohideTaskbar(ABE_RIGHT);
// If there's a taskbar on any side of the monitor, reduce our size
// a little bit on that edge.
//
// Note to future code archeologists:
// This doesn't seem to work for fullscreen on the primary display.
// However, testing a bunch of other apps with fullscreen modes
// and an auto-hiding taskbar has shown that _none_ of them
// reveal the taskbar from fullscreen mode. This includes Edge,
// Firefox, Chrome, Sublime Text, PowerPoint - none seemed to
// support this.
//
// This does however work fine for maximized.
if (onTop)
{
// Peculiarly, when we're fullscreen,
newSize.top += AutohideTaskbarSize;
}
if (onBottom)
{
newSize.bottom -= AutohideTaskbarSize;
}
if (onLeft)
{
newSize.left += AutohideTaskbarSize;
}
if (onRight)
{
newSize.right -= AutohideTaskbarSize;
}
}
}
params->rgrc[0] = newSize;
return 0;
}
// Method Description:
// - Hit test the frame for resizing and moving.
// Arguments:
// - ptMouse: the mouse point being tested, in absolute (NOT WINDOW) coordinates.
// Return Value:
// - one of the values from
// https://docs.microsoft.com/en-us/windows/desktop/inputdev/wm-nchittest#return-value
// corresponding to the area of the window that was hit
[[nodiscard]] LRESULT NonClientIslandWindow::_OnNcHitTest(POINT ptMouse) const noexcept
{
// This will handle the left, right and bottom parts of the frame because
// we didn't change them.
LPARAM lParam = MAKELONG(ptMouse.x, ptMouse.y);
const auto originalRet = DefWindowProc(_window.get(), WM_NCHITTEST, 0, lParam);
if (originalRet != HTCLIENT)
{
// If we're the quake window, suppress resizing on any side except the
// bottom. I don't believe that this actually works on the top. That's
// handled below.
if (IsQuakeWindow())
{
switch (originalRet)
{
case HTBOTTOMRIGHT:
case HTRIGHT:
case HTTOPRIGHT:
case HTTOP:
case HTTOPLEFT:
case HTLEFT:
case HTBOTTOMLEFT:
return HTCLIENT;
}
}
return originalRet;
}
// At this point, we know that the cursor is inside the client area so it
// has to be either the little border at the top of our custom title bar,
// the drag bar or something else in the XAML island. But the XAML Island
// handles WM_NCHITTEST on its own so actually it cannot be the XAML
// Island. Then it must be the drag bar or the little border at the top
// which the user can use to move or resize the window.
RECT rcWindow;
winrt::check_bool(::GetWindowRect(_window.get(), &rcWindow));
const auto resizeBorderHeight = _GetResizeHandleHeight();
const auto isOnResizeBorder = ptMouse.y < rcWindow.top + resizeBorderHeight;
// the top of the drag bar is used to resize the window
if (!_isMaximized && isOnResizeBorder)
{
// However, if we're the quake window, then just return HTCAPTION so we
// don't get a resize handle on the top.
return IsQuakeWindow() ? HTCAPTION : HTTOP;
}
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.
const auto screenPtLparam{ GetMessagePos() };
const LRESULT hitTest{ SendMessage(GetHandle(), 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(GetHandle(), WM_SETCURSOR, wParam, lParam);
}
// Method Description:
// - Get the dimensions of our non-client area, as a rect where each component
// represents that side.
// - The .left will be a negative number, to represent that the actual side of
// the non-client area is outside the border of our window. It's roughly 8px (
// * DPI scaling) to the left of the visible border.
// - The .right component will be positive, indicating that the nonclient border
// is in the positive-x direction from the edge of our client area.
// - This DOES NOT include our titlebar! It's in the client area for us.
// Arguments:
// - dpi: the scaling that we should use to calculate the border sizes.
// Return Value:
// - a RECT whose components represent the margins of the nonclient area,
// relative to the client area.
RECT NonClientIslandWindow::GetNonClientFrame(UINT dpi) const noexcept
{
const auto windowStyle = static_cast<DWORD>(GetWindowLong(_window.get(), GWL_STYLE));
RECT islandFrame{};
// If we failed to get the correct window size for whatever reason, log
// the error and go on. We'll use whatever the control proposed as the
// size of our window, which will be at least close.
LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&islandFrame, windowStyle, false, 0, dpi));
islandFrame.top = -topBorderVisibleHeight;
return islandFrame;
}
// Method Description:
// - Gets the difference between window and client area size.
// Arguments:
// - dpi: dpi of a monitor on which the window is placed
// Return Value
// - The size difference
SIZE NonClientIslandWindow::GetTotalNonClientExclusiveSize(UINT dpi) const noexcept
{
const auto islandFrame{ GetNonClientFrame(dpi) };
// If we have a titlebar, this is being called after we've initialized, and
// we can just ask that titlebar how big it wants to be.
const auto titleBarHeight = _titlebar ? static_cast<LONG>(_titlebar.ActualHeight()) : 0;
return {
islandFrame.right - islandFrame.left,
islandFrame.bottom - islandFrame.top + titleBarHeight
};
}
// Method Description:
// - Updates the borders of our window frame, using DwmExtendFrameIntoClientArea.
// Arguments:
// - <none>
// Return Value:
// - the HRESULT returned by DwmExtendFrameIntoClientArea.
void NonClientIslandWindow::_UpdateFrameMargins() const noexcept
{
MARGINS margins = { 0, 0, 0, 0 };
// GH#603: When we're in Focus Mode, hide the titlebar, by setting it to a single
// pixel tall. Otherwise, the titlebar will be visible underneath controls with
// vintage opacity set.
//
// We can't set it to all 0's unfortunately.
if (_borderless)
{
margins.cyTopHeight = 1;
}
else if (_GetTopBorderHeight() != 0)
{
RECT frame = {};
winrt::check_bool(::AdjustWindowRectExForDpi(&frame, GetWindowStyle(_window.get()), FALSE, 0, _currentDpi));
// We removed the whole top part of the frame (see handling of
// WM_NCCALCSIZE) so the top border is missing now. We add it back here.
// Note #1: You might wonder why we don't remove just the title bar instead
// of removing the whole top part of the frame and then adding the little
// top border back. I tried to do this but it didn't work: DWM drew the
// whole title bar anyways on top of the window. It seems that DWM only
// wants to draw either nothing or the whole top part of the frame.
// Note #2: For some reason if you try to set the top margin to just the
// top border height (what we want to do), then there is a transparency
// bug when the window is inactive, so I've decided to add the whole top
// part of the frame instead and then we will hide everything that we
// don't need (that is, the whole thing but the little 1 pixel wide border
// at the top) in the WM_PAINT handler. This eliminates the transparency
// bug and it's what a lot of Win32 apps that customize the title bar do
// so it should work fine.
margins.cyTopHeight = -frame.top;
}
// Extend the frame into the client area. microsoft/terminal#2735 - Just log
// the failure here, don't crash. If DWM crashes for any reason, calling
// THROW_IF_FAILED() will cause us to take a trip upstate. Just log, and
// we'll fix ourselves when DWM comes back.
LOG_IF_FAILED(DwmExtendFrameIntoClientArea(_window.get(), &margins));
}
// Method Description:
// - Handle window messages from the message loop.
// Arguments:
// - message: A window message ID identifying the message.
// - wParam: The contents of this parameter depend on the value of the message parameter.
// - lParam: The contents of this parameter depend on the value of the message parameter.
// Return Value:
// - The return value is the result of the message processing and depends on the
// message sent.
[[nodiscard]] LRESULT NonClientIslandWindow::MessageHandler(UINT const message,
WPARAM const wParam,
LPARAM const lParam) 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.
_ResizeDragBarWindow();
return 0;
case WM_NCCALCSIZE:
return _OnNcCalcSize(wParam, lParam);
case WM_NCHITTEST:
return _OnNcHitTest({ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) });
case WM_PAINT:
return _OnPaint();
case WM_NCRBUTTONUP:
// The `DefWindowProc` function doesn't open the system menu for some
// reason so we have to do it ourselves.
if (wParam == HTCAPTION)
{
OpenSystemMenu(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
}
break;
}
return IslandWindow::MessageHandler(message, wParam, lParam);
}
// Method Description:
// - 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
{
if (!_titlebar)
{
return 0;
}
PAINTSTRUCT ps{ 0 };
const auto hdc = wil::BeginPaint(_window.get(), &ps);
if (!hdc)
{
return 0;
}
const auto topBorderHeight = _GetTopBorderHeight();
if (ps.rcPaint.top < topBorderHeight)
{
RECT rcTopBorder = ps.rcPaint;
rcTopBorder.bottom = topBorderHeight;
// To show the original top border, we have to paint on top of it with
// the alpha component set to 0. This page recommends to paint the area
// in black using the stock BLACK_BRUSH to do this:
// https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#extending-the-client-frame
::FillRect(hdc.get(), &rcTopBorder, GetStockBrush(BLACK_BRUSH));
}
if (ps.rcPaint.bottom > topBorderHeight)
{
RECT rcRest = ps.rcPaint;
rcRest.top = topBorderHeight;
const auto backgroundBrush = _titlebar.Background();
const auto backgroundSolidBrush = backgroundBrush.try_as<Media::SolidColorBrush>();
const auto backgroundAcrylicBrush = backgroundBrush.try_as<Media::AcrylicBrush>();
til::color backgroundColor = Colors::Black();
if (backgroundSolidBrush)
{
backgroundColor = backgroundSolidBrush.Color();
}
else if (backgroundAcrylicBrush)
{
backgroundColor = backgroundAcrylicBrush.FallbackColor();
}
if (!_backgroundBrush || backgroundColor != _backgroundBrushColor)
{
// Create brush for titlebar color.
_backgroundBrush = wil::unique_hbrush(CreateSolidBrush(backgroundColor));
}
// To hide the original title bar, we have to paint on top of it with
// the alpha component set to 255. This is a hack to do it with GDI.
// See NonClientIslandWindow::_UpdateFrameMargins for more information.
HDC opaqueDc;
BP_PAINTPARAMS params = { sizeof(params), BPPF_NOCLIP | BPPF_ERASE };
HPAINTBUFFER buf = BeginBufferedPaint(hdc.get(), &rcRest, BPBF_TOPDOWNDIB, &params, &opaqueDc);
if (!buf || !opaqueDc)
{
// MSFT:34673647 - BeginBufferedPaint can fail, but it probably
// shouldn't bring the whole Terminal down with it. So don't
// throw_last_error here.
LOG_LAST_ERROR();
return 0;
}
::FillRect(opaqueDc, &rcRest, _backgroundBrush.get());
::BufferedPaintSetAlpha(buf, nullptr, 255);
::EndBufferedPaint(buf, TRUE);
}
return 0;
}
// Method Description:
// - This method is called when the window receives the WM_NCCREATE message.
// Return Value:
// - The value returned from the window proc.
[[nodiscard]] LRESULT NonClientIslandWindow::_OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept
{
const auto ret = IslandWindow::_OnNcCreate(wParam, lParam);
if (!ret)
{
return FALSE;
}
// This is a hack to make the window borders dark instead of light.
// It must be done before WM_NCPAINT so that the borders are rendered with
// the correct theme.
// For more information, see GH#6620.
LOG_IF_FAILED(TerminalTrySetDarkTheme(_window.get(), true));
return TRUE;
}
// Method Description:
// - Called when the app wants to change its theme. We'll update the frame
// theme to match the new theme.
// Arguments:
// - requestedTheme: the ElementTheme to use as the new theme for the UI
// Return Value:
// - <none>
void NonClientIslandWindow::OnApplicationThemeChanged(const ElementTheme& requestedTheme)
{
IslandWindow::OnApplicationThemeChanged(requestedTheme);
_theme = requestedTheme;
}
// Method Description:
// - Enable or disable borderless mode. When entering borderless mode, we'll
// need to manually hide the entire titlebar.
// - See also IslandWindow::_SetIsBorderless, which does similar, but different work.
// Arguments:
// - borderlessEnabled: If true, we're entering borderless mode. If false, we're leaving.
// Return Value:
// - <none>
void NonClientIslandWindow::_SetIsBorderless(const bool borderlessEnabled)
{
_borderless = borderlessEnabled;
// Explicitly _don't_ call IslandWindow::_SetIsBorderless. That version will
// change the window styles appropriately for the window with the default
// titlebar, but for the tabs-in-titlebar mode, we can just get rid of the
// title bar entirely.
if (_titlebar)
{
_titlebar.Visibility(_IsTitlebarVisible() ? Visibility::Visible : Visibility::Collapsed);
}
// Update the margins when entering/leaving focus mode, so we can prevent
// the titlebar from showing through transparent terminal controls
_UpdateFrameMargins();
// GH#4224 - When the auto-hide taskbar setting is enabled, then we don't
// 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.
_ResizeDragBarWindow();
// Resize the window, with SWP_FRAMECHANGED, to trigger user32 to
// recalculate the non/client areas
const til::rectangle windowPos{ GetWindowRect() };
SetWindowPos(GetHandle(),
HWND_TOP,
windowPos.left<int>(),
windowPos.top<int>(),
windowPos.width<int>(),
windowPos.height<int>(),
SWP_SHOWWINDOW | SWP_FRAMECHANGED | SWP_NOACTIVATE);
}
// Method Description:
// - Enable or disable fullscreen mode. When entering fullscreen mode, we'll
// need to manually hide the entire titlebar.
// - See also IslandWindow::_SetIsFullscreen, which does additional work.
// Arguments:
// - fullscreenEnabled: If true, we're entering fullscreen mode. If false, we're leaving.
// Return Value:
// - <none>
void NonClientIslandWindow::_SetIsFullscreen(const bool fullscreenEnabled)
{
IslandWindow::_SetIsFullscreen(fullscreenEnabled);
if (_titlebar)
{
_titlebar.Visibility(_IsTitlebarVisible() ? Visibility::Visible : Visibility::Collapsed);
}
// GH#4224 - When the auto-hide taskbar setting is enabled, then we don't
// 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.
_ResizeDragBarWindow();
}
// Method Description:
// - Returns true if the titlebar is visible. For things like fullscreen mode,
// borderless mode (aka "focus mode"), this will return false.
// Arguments:
// - <none>
// Return Value:
// - true iff the titlebar is visible
bool NonClientIslandWindow::_IsTitlebarVisible() const
{
return !(_fullscreen || _borderless);
}