404 lines
13 KiB
C++
404 lines
13 KiB
C++
#include "BorderlessWindowChrome.h"
|
|
|
|
#include <algorithm>
|
|
#include <dwmapi.h>
|
|
|
|
namespace XCEngine::UI::Editor::Host {
|
|
|
|
namespace {
|
|
|
|
using ::XCEngine::UI::UIColor;
|
|
using ::XCEngine::UI::UIDrawList;
|
|
using ::XCEngine::UI::UIPoint;
|
|
using ::XCEngine::UI::UIRect;
|
|
|
|
bool IsPointInsideRect(const UIRect& rect, const UIPoint& point) {
|
|
return rect.width > 0.0f &&
|
|
rect.height > 0.0f &&
|
|
point.x >= rect.x &&
|
|
point.x <= rect.x + rect.width &&
|
|
point.y >= rect.y &&
|
|
point.y <= rect.y + rect.height;
|
|
}
|
|
|
|
void AppendMinimizeGlyph(
|
|
UIDrawList& drawList,
|
|
const UIRect& rect,
|
|
const UIColor& color,
|
|
float thickness) {
|
|
const float centerX = rect.x + rect.width * 0.5f;
|
|
const float centerY = rect.y + rect.height * 0.5f;
|
|
const float halfWidth = (std::max)(4.0f, rect.height * 0.22f);
|
|
const float y = centerY + rect.height * 0.12f;
|
|
drawList.AddLine(
|
|
UIPoint(centerX - halfWidth, y),
|
|
UIPoint(centerX + halfWidth, y),
|
|
color,
|
|
thickness);
|
|
}
|
|
|
|
void AppendMaximizeGlyph(
|
|
UIDrawList& drawList,
|
|
const UIRect& rect,
|
|
const UIColor& color,
|
|
float thickness) {
|
|
const float centerX = rect.x + rect.width * 0.5f;
|
|
const float centerY = rect.y + rect.height * 0.5f;
|
|
const float halfExtent = (std::max)(4.0f, rect.height * 0.20f);
|
|
drawList.AddRectOutline(
|
|
UIRect(
|
|
centerX - halfExtent,
|
|
centerY - halfExtent,
|
|
halfExtent * 2.0f,
|
|
halfExtent * 2.0f),
|
|
color,
|
|
thickness);
|
|
}
|
|
|
|
void AppendRestoreGlyph(
|
|
UIDrawList& drawList,
|
|
const UIRect& rect,
|
|
const UIColor& color,
|
|
float thickness) {
|
|
const float centerX = rect.x + rect.width * 0.5f;
|
|
const float centerY = rect.y + rect.height * 0.5f;
|
|
const float halfExtent = (std::max)(4.0f, rect.height * 0.18f);
|
|
const float offset = 2.0f;
|
|
drawList.AddRectOutline(
|
|
UIRect(
|
|
centerX - halfExtent + offset,
|
|
centerY - halfExtent - offset,
|
|
halfExtent * 2.0f,
|
|
halfExtent * 2.0f),
|
|
color,
|
|
thickness);
|
|
drawList.AddRectOutline(
|
|
UIRect(
|
|
centerX - halfExtent - offset,
|
|
centerY - halfExtent + offset,
|
|
halfExtent * 2.0f,
|
|
halfExtent * 2.0f),
|
|
color,
|
|
thickness);
|
|
}
|
|
|
|
void AppendCloseGlyph(
|
|
UIDrawList& drawList,
|
|
const UIRect& rect,
|
|
const UIColor& color,
|
|
float thickness) {
|
|
const float centerX = rect.x + rect.width * 0.5f;
|
|
const float centerY = rect.y + rect.height * 0.5f;
|
|
const float halfWidth = (std::max)(4.0f, rect.height * 0.20f);
|
|
const float halfHeight = halfWidth;
|
|
drawList.AddLine(
|
|
UIPoint(centerX - halfWidth, centerY - halfHeight),
|
|
UIPoint(centerX + halfWidth, centerY + halfHeight),
|
|
color,
|
|
thickness);
|
|
drawList.AddLine(
|
|
UIPoint(centerX + halfWidth, centerY - halfHeight),
|
|
UIPoint(centerX - halfWidth, centerY + halfHeight),
|
|
color,
|
|
thickness);
|
|
}
|
|
|
|
::XCEngine::UI::UIColor ResolveButtonFillColor(
|
|
BorderlessWindowChromeHitTarget target,
|
|
const BorderlessWindowChromeState& state,
|
|
const BorderlessWindowChromePalette& palette) {
|
|
const bool hovered = state.hoveredTarget == target;
|
|
const bool pressed = state.pressedTarget == target;
|
|
if (target == BorderlessWindowChromeHitTarget::CloseButton) {
|
|
if (pressed) {
|
|
return palette.closeButtonPressedColor;
|
|
}
|
|
if (hovered) {
|
|
return palette.closeButtonHoverColor;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
if (pressed) {
|
|
return palette.buttonPressedColor;
|
|
}
|
|
if (hovered) {
|
|
return palette.buttonHoverColor;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
::XCEngine::UI::UIColor ResolveIconColor(
|
|
BorderlessWindowChromeHitTarget target,
|
|
const BorderlessWindowChromeState& state,
|
|
const BorderlessWindowChromePalette& palette) {
|
|
if (target == BorderlessWindowChromeHitTarget::CloseButton &&
|
|
(state.hoveredTarget == target || state.pressedTarget == target)) {
|
|
return palette.closeIconHoverColor;
|
|
}
|
|
|
|
return palette.iconColor;
|
|
}
|
|
|
|
int QuerySystemMetricForDpi(int index, UINT dpi) {
|
|
const HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
|
if (user32 != nullptr) {
|
|
using GetSystemMetricsForDpiFn = int(WINAPI*)(int, UINT);
|
|
const auto getSystemMetricsForDpi =
|
|
reinterpret_cast<GetSystemMetricsForDpiFn>(
|
|
GetProcAddress(user32, "GetSystemMetricsForDpi"));
|
|
if (getSystemMetricsForDpi != nullptr) {
|
|
return getSystemMetricsForDpi(index, dpi);
|
|
}
|
|
}
|
|
|
|
return GetSystemMetrics(index);
|
|
}
|
|
|
|
bool IsWindowAlignedToMonitorWorkArea(HWND hwnd) {
|
|
if (hwnd == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
RECT windowRect = {};
|
|
if (!GetWindowRect(hwnd, &windowRect)) {
|
|
return false;
|
|
}
|
|
|
|
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
|
if (monitor == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
MONITORINFO monitorInfo = {};
|
|
monitorInfo.cbSize = sizeof(monitorInfo);
|
|
if (!GetMonitorInfoW(monitor, &monitorInfo)) {
|
|
return false;
|
|
}
|
|
|
|
const RECT& workArea = monitorInfo.rcWork;
|
|
return windowRect.left == workArea.left &&
|
|
windowRect.top == workArea.top &&
|
|
windowRect.right == workArea.right &&
|
|
windowRect.bottom == workArea.bottom;
|
|
}
|
|
|
|
void ApplyDwmBoolWindowAttribute(HWND hwnd, DWORD attribute, BOOL value) {
|
|
if (hwnd == nullptr) {
|
|
return;
|
|
}
|
|
|
|
using DwmSetWindowAttributeFn = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD);
|
|
static const auto setWindowAttribute = []() -> DwmSetWindowAttributeFn {
|
|
HMODULE dwmapi = GetModuleHandleW(L"dwmapi.dll");
|
|
if (dwmapi == nullptr) {
|
|
dwmapi = LoadLibraryW(L"dwmapi.dll");
|
|
}
|
|
if (dwmapi == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
return reinterpret_cast<DwmSetWindowAttributeFn>(
|
|
GetProcAddress(dwmapi, "DwmSetWindowAttribute"));
|
|
}();
|
|
|
|
if (setWindowAttribute != nullptr) {
|
|
setWindowAttribute(hwnd, attribute, &value, sizeof(value));
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
BorderlessWindowChromeLayout BuildBorderlessWindowChromeLayout(
|
|
const UIRect& titleBarRect,
|
|
float leadingOccupiedRight,
|
|
const BorderlessWindowChromeMetrics& metrics) {
|
|
BorderlessWindowChromeLayout layout = {};
|
|
layout.titleBarRect = titleBarRect;
|
|
if (titleBarRect.width <= 0.0f || titleBarRect.height <= 0.0f) {
|
|
return layout;
|
|
}
|
|
|
|
const float buttonWidth = (std::max)(metrics.buttonWidth, 0.0f);
|
|
const float buttonX3 = titleBarRect.x + titleBarRect.width - metrics.buttonInsetX - buttonWidth;
|
|
const float buttonX2 = buttonX3 - buttonWidth;
|
|
const float buttonX1 = buttonX2 - buttonWidth;
|
|
|
|
layout.minimizeButtonRect = UIRect(buttonX1, titleBarRect.y, buttonWidth, titleBarRect.height);
|
|
layout.maximizeRestoreButtonRect = UIRect(buttonX2, titleBarRect.y, buttonWidth, titleBarRect.height);
|
|
layout.closeButtonRect = UIRect(buttonX3, titleBarRect.y, buttonWidth, titleBarRect.height);
|
|
|
|
const float dragLeft =
|
|
(std::max)(titleBarRect.x, leadingOccupiedRight + metrics.dragPaddingLeft);
|
|
const float dragRight =
|
|
(std::max)(dragLeft, layout.minimizeButtonRect.x - metrics.dragPaddingRight);
|
|
layout.dragRect = UIRect(
|
|
dragLeft,
|
|
titleBarRect.y,
|
|
(std::max)(0.0f, dragRight - dragLeft),
|
|
titleBarRect.height);
|
|
return layout;
|
|
}
|
|
|
|
BorderlessWindowChromeHitTarget HitTestBorderlessWindowChrome(
|
|
const BorderlessWindowChromeLayout& layout,
|
|
const UIPoint& point) {
|
|
if (IsPointInsideRect(layout.closeButtonRect, point)) {
|
|
return BorderlessWindowChromeHitTarget::CloseButton;
|
|
}
|
|
if (IsPointInsideRect(layout.maximizeRestoreButtonRect, point)) {
|
|
return BorderlessWindowChromeHitTarget::MaximizeRestoreButton;
|
|
}
|
|
if (IsPointInsideRect(layout.minimizeButtonRect, point)) {
|
|
return BorderlessWindowChromeHitTarget::MinimizeButton;
|
|
}
|
|
if (IsPointInsideRect(layout.dragRect, point)) {
|
|
return BorderlessWindowChromeHitTarget::DragRegion;
|
|
}
|
|
return BorderlessWindowChromeHitTarget::None;
|
|
}
|
|
|
|
void AppendBorderlessWindowChrome(
|
|
UIDrawList& drawList,
|
|
const BorderlessWindowChromeLayout& layout,
|
|
const BorderlessWindowChromeState& state,
|
|
bool maximized,
|
|
const BorderlessWindowChromePalette& palette,
|
|
const BorderlessWindowChromeMetrics& metrics) {
|
|
const struct ButtonEntry {
|
|
BorderlessWindowChromeHitTarget target;
|
|
UIRect rect;
|
|
} buttons[] = {
|
|
{ BorderlessWindowChromeHitTarget::MinimizeButton, layout.minimizeButtonRect },
|
|
{ BorderlessWindowChromeHitTarget::MaximizeRestoreButton, layout.maximizeRestoreButtonRect },
|
|
{ BorderlessWindowChromeHitTarget::CloseButton, layout.closeButtonRect }
|
|
};
|
|
|
|
for (const ButtonEntry& button : buttons) {
|
|
const UIColor fill = ResolveButtonFillColor(button.target, state, palette);
|
|
if (fill.a > 0.0f) {
|
|
drawList.AddFilledRect(button.rect, fill);
|
|
}
|
|
|
|
const UIColor iconColor = ResolveIconColor(button.target, state, palette);
|
|
switch (button.target) {
|
|
case BorderlessWindowChromeHitTarget::MinimizeButton:
|
|
AppendMinimizeGlyph(
|
|
drawList,
|
|
button.rect,
|
|
iconColor,
|
|
metrics.iconThickness);
|
|
break;
|
|
case BorderlessWindowChromeHitTarget::MaximizeRestoreButton:
|
|
if (maximized) {
|
|
AppendRestoreGlyph(
|
|
drawList,
|
|
button.rect,
|
|
iconColor,
|
|
metrics.iconThickness);
|
|
} else {
|
|
AppendMaximizeGlyph(
|
|
drawList,
|
|
button.rect,
|
|
iconColor,
|
|
metrics.iconThickness);
|
|
}
|
|
break;
|
|
case BorderlessWindowChromeHitTarget::CloseButton:
|
|
AppendCloseGlyph(
|
|
drawList,
|
|
button.rect,
|
|
iconColor,
|
|
metrics.iconThickness);
|
|
break;
|
|
case BorderlessWindowChromeHitTarget::DragRegion:
|
|
case BorderlessWindowChromeHitTarget::None:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void EnableBorderlessWindowShadow(HWND hwnd) {
|
|
if (hwnd == nullptr) {
|
|
return;
|
|
}
|
|
|
|
using DwmExtendFrameIntoClientAreaFn = HRESULT(WINAPI*)(HWND, const MARGINS*);
|
|
static const auto extendFrameIntoClientArea = []() -> DwmExtendFrameIntoClientAreaFn {
|
|
HMODULE dwmapi = GetModuleHandleW(L"dwmapi.dll");
|
|
if (dwmapi == nullptr) {
|
|
dwmapi = LoadLibraryW(L"dwmapi.dll");
|
|
}
|
|
if (dwmapi == nullptr) {
|
|
return nullptr;
|
|
}
|
|
return reinterpret_cast<DwmExtendFrameIntoClientAreaFn>(
|
|
GetProcAddress(dwmapi, "DwmExtendFrameIntoClientArea"));
|
|
}();
|
|
if (extendFrameIntoClientArea != nullptr) {
|
|
const bool maximized = IsZoomed(hwnd) || IsWindowAlignedToMonitorWorkArea(hwnd);
|
|
const MARGINS margins = maximized
|
|
? MARGINS{ 0, 0, 0, 0 }
|
|
: MARGINS{ 1, 1, 1, 1 };
|
|
extendFrameIntoClientArea(hwnd, &margins);
|
|
}
|
|
}
|
|
|
|
void RefreshBorderlessWindowDwmDecorations(HWND hwnd) {
|
|
if (hwnd == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// Borderless host cannot participate in compositor-driven minimize/maximize
|
|
// transitions without Windows stretching the last presented client frame.
|
|
// Disable those transitions for this window, then refresh the shadow state.
|
|
ApplyDwmBoolWindowAttribute(hwnd, DWMWA_TRANSITIONS_FORCEDISABLED, TRUE);
|
|
EnableBorderlessWindowShadow(hwnd);
|
|
}
|
|
|
|
bool HandleBorderlessWindowGetMinMaxInfo(HWND hwnd, LPARAM lParam) {
|
|
if (hwnd == nullptr || lParam == 0) {
|
|
return false;
|
|
}
|
|
|
|
auto* minMaxInfo = reinterpret_cast<MINMAXINFO*>(lParam);
|
|
if (minMaxInfo == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
|
if (monitor == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
MONITORINFO monitorInfo = {};
|
|
monitorInfo.cbSize = sizeof(monitorInfo);
|
|
if (!GetMonitorInfoW(monitor, &monitorInfo)) {
|
|
return false;
|
|
}
|
|
|
|
const RECT& workArea = monitorInfo.rcWork;
|
|
const RECT& monitorArea = monitorInfo.rcMonitor;
|
|
minMaxInfo->ptMaxPosition.x = workArea.left - monitorArea.left;
|
|
minMaxInfo->ptMaxPosition.y = workArea.top - monitorArea.top;
|
|
minMaxInfo->ptMaxSize.x = workArea.right - workArea.left;
|
|
minMaxInfo->ptMaxSize.y = workArea.bottom - workArea.top;
|
|
minMaxInfo->ptMaxTrackSize = minMaxInfo->ptMaxSize;
|
|
return true;
|
|
}
|
|
|
|
LRESULT HandleBorderlessWindowNcCalcSize(
|
|
HWND hwnd,
|
|
WPARAM wParam,
|
|
LPARAM lParam,
|
|
UINT dpi) {
|
|
(void)hwnd;
|
|
(void)wParam;
|
|
(void)lParam;
|
|
(void)dpi;
|
|
return 0;
|
|
}
|
|
|
|
} // namespace XCEngine::UI::Editor::Host
|