#include "BorderlessWindowChrome.h" #include #include #include namespace XCEngine::UI::Editor::Host { namespace { 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; } } // namespace BorderlessWindowChromeLayout BuildBorderlessWindowChromeLayout( const UIRect& titleBarRect, float leadingOccupiedRight, bool includePinButton, 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 rightEdge = titleBarRect.x + titleBarRect.width - metrics.buttonInsetX; const float closeButtonX = rightEdge - buttonWidth; const float maximizeButtonX = closeButtonX - buttonWidth; const float minimizeButtonX = maximizeButtonX - buttonWidth; const float pinButtonX = minimizeButtonX - buttonWidth; if (includePinButton) { layout.pinButtonRect = UIRect(pinButtonX, titleBarRect.y, buttonWidth, titleBarRect.height); } layout.minimizeButtonRect = UIRect(minimizeButtonX, titleBarRect.y, buttonWidth, titleBarRect.height); layout.maximizeRestoreButtonRect = UIRect(maximizeButtonX, titleBarRect.y, buttonWidth, titleBarRect.height); layout.closeButtonRect = UIRect(closeButtonX, titleBarRect.y, buttonWidth, titleBarRect.height); const float dragLeft = (std::max)(titleBarRect.x, leadingOccupiedRight + metrics.dragPaddingLeft); const float buttonClusterLeft = includePinButton ? layout.pinButtonRect.x : layout.minimizeButtonRect.x; const float dragRight = (std::max)(dragLeft, buttonClusterLeft - 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.pinButtonRect, point)) { return BorderlessWindowChromeHitTarget::PinButton; } if (IsPointInsideRect(layout.dragRect, point)) { return BorderlessWindowChromeHitTarget::DragRegion; } return BorderlessWindowChromeHitTarget::None; } BorderlessWindowChromeButtonBehavior ResolveBorderlessWindowChromeButtonBehavior( BorderlessWindowChromeHitTarget target) { switch (target) { case BorderlessWindowChromeHitTarget::PinButton: return BorderlessWindowChromeButtonBehavior::Toggle; case BorderlessWindowChromeHitTarget::MinimizeButton: case BorderlessWindowChromeHitTarget::MaximizeRestoreButton: return BorderlessWindowChromeButtonBehavior::Action; case BorderlessWindowChromeHitTarget::CloseButton: return BorderlessWindowChromeButtonBehavior::DestructiveAction; case BorderlessWindowChromeHitTarget::DragRegion: case BorderlessWindowChromeHitTarget::None: default: return BorderlessWindowChromeButtonBehavior::None; } } } // namespace XCEngine::UI::Editor::Host namespace XCEngine::UI::Editor::Host { namespace { using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; constexpr UIColor kTransparentColor(0.0f, 0.0f, 0.0f, 0.0f); float ResolveGlyphBoxSize(const UIRect& rect, float ratio, float minSize) { return (std::max)(minSize, static_cast(std::round(rect.height * ratio))); } 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 = ResolveGlyphBoxSize(rect, 0.38f, 10.0f) * 0.5f; const float y = centerY; 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 boxSize = ResolveGlyphBoxSize(rect, 0.32f, 9.0f); const float halfExtent = boxSize * 0.5f; const float left = centerX - halfExtent; const float top = centerY - halfExtent; const float right = left + boxSize; const float bottom = top + boxSize; drawList.AddLine(UIPoint(left, top), UIPoint(right, top), color, thickness); drawList.AddLine(UIPoint(left, top), UIPoint(left, bottom), color, thickness); drawList.AddLine(UIPoint(right, top), UIPoint(right, bottom), color, thickness); drawList.AddLine(UIPoint(left, bottom), UIPoint(right, bottom), 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 boxSize = ResolveGlyphBoxSize(rect, 0.29f, 8.0f); const float halfExtent = boxSize * 0.5f; const float offset = 1.0f; const float backLeft = centerX - halfExtent + offset; const float backTop = centerY - halfExtent - offset; const float backRight = backLeft + boxSize; const float backBottom = backTop + boxSize; const float frontLeft = centerX - halfExtent - offset; const float frontTop = centerY - halfExtent + offset; const float frontRight = frontLeft + boxSize; const float frontBottom = frontTop + boxSize; drawList.AddLine(UIPoint(backLeft, backTop), UIPoint(backRight, backTop), color, thickness); drawList.AddLine(UIPoint(backLeft, backTop), UIPoint(backLeft, frontTop), color, thickness); drawList.AddLine(UIPoint(backRight, backTop), UIPoint(backRight, backBottom), color, thickness); drawList.AddLine(UIPoint(frontRight, backBottom), UIPoint(backRight, backBottom), color, thickness); drawList.AddLine(UIPoint(frontLeft, frontTop), UIPoint(frontRight, frontTop), color, thickness); drawList.AddLine(UIPoint(frontLeft, frontTop), UIPoint(frontLeft, frontBottom), color, thickness); drawList.AddLine(UIPoint(frontRight, frontTop), UIPoint(frontRight, frontBottom), color, thickness); drawList.AddLine(UIPoint(frontLeft, frontBottom), UIPoint(frontRight, frontBottom), 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 = ResolveGlyphBoxSize(rect, 0.29f, 8.0f) * 0.5f; 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); } void AppendPinGlyph( 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 iconSize = ResolveGlyphBoxSize(rect, 0.34f, 10.0f); const float headRadius = iconSize * 0.18f; const float headCenterY = centerY - iconSize * 0.30f; const float collarTopY = headCenterY + headRadius; const float collarBottomY = collarTopY + iconSize * 0.12f; const float shoulderHalfWidth = iconSize * 0.34f; const float bodyBottomY = centerY + iconSize * 0.06f; const float needleBottomY = centerY + iconSize * 0.44f; drawList.AddFilledCircle(UIPoint(centerX, headCenterY), headRadius, color); drawList.AddLine( UIPoint(centerX, collarTopY), UIPoint(centerX, collarBottomY), color, thickness); drawList.AddFilledTriangle( UIPoint(centerX - shoulderHalfWidth, collarBottomY), UIPoint(centerX + shoulderHalfWidth, collarBottomY), UIPoint(centerX, bodyBottomY), color); drawList.AddLine( UIPoint(centerX, bodyBottomY), UIPoint(centerX, needleBottomY), color, thickness); } UIColor ResolveButtonFillColor( BorderlessWindowChromeHitTarget target, const BorderlessWindowChromeState& state, const BorderlessWindowChromePalette& palette) { const BorderlessWindowChromeButtonBehavior behavior = ResolveBorderlessWindowChromeButtonBehavior(target); const bool hovered = state.hoveredTarget == target; const bool pressed = state.pressedTarget == target; if (behavior == BorderlessWindowChromeButtonBehavior::DestructiveAction) { if (pressed) { return palette.closeButtonPressedColor; } if (hovered) { return palette.closeButtonHoverColor; } return kTransparentColor; } if (behavior == BorderlessWindowChromeButtonBehavior::Toggle || behavior == BorderlessWindowChromeButtonBehavior::Action) { if (pressed) { return palette.buttonPressedColor; } if (hovered) { return palette.buttonHoverColor; } } return kTransparentColor; } UIColor ResolveIconColor( BorderlessWindowChromeHitTarget target, const BorderlessWindowChromeState& state, const BorderlessWindowChromePalette& palette, bool topmostPinned) { const BorderlessWindowChromeButtonBehavior behavior = ResolveBorderlessWindowChromeButtonBehavior(target); if (behavior == BorderlessWindowChromeButtonBehavior::DestructiveAction && (state.hoveredTarget == target || state.pressedTarget == target)) { return palette.closeIconHoverColor; } if (behavior == BorderlessWindowChromeButtonBehavior::Toggle) { return topmostPinned ? palette.toggleIconOnColor : palette.toggleIconOffColor; } return palette.iconColor; } } // namespace void AppendBorderlessWindowChrome( UIDrawList& drawList, const BorderlessWindowChromeLayout& layout, const BorderlessWindowChromeState& state, bool maximized, bool topmostPinned, const BorderlessWindowChromePalette& palette, const BorderlessWindowChromeMetrics& metrics) { const struct ButtonEntry { BorderlessWindowChromeHitTarget target; UIRect rect; } buttons[] = { { BorderlessWindowChromeHitTarget::PinButton, layout.pinButtonRect }, { BorderlessWindowChromeHitTarget::MinimizeButton, layout.minimizeButtonRect }, { BorderlessWindowChromeHitTarget::MaximizeRestoreButton, layout.maximizeRestoreButtonRect }, { BorderlessWindowChromeHitTarget::CloseButton, layout.closeButtonRect } }; for (const ButtonEntry& button : buttons) { if (button.rect.width <= 0.0f || button.rect.height <= 0.0f) { continue; } 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, topmostPinned); switch (button.target) { case BorderlessWindowChromeHitTarget::PinButton: AppendPinGlyph(drawList, button.rect, iconColor, metrics.iconThickness); break; 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; } } } } // namespace XCEngine::UI::Editor::Host namespace XCEngine::UI::Editor::Host { namespace { using DwmSetWindowAttributeFn = HRESULT(WINAPI*)(HWND, DWORD, LPCVOID, DWORD); using DwmExtendFrameIntoClientAreaFn = HRESULT(WINAPI*)(HWND, const MARGINS*); HMODULE GetDwmApiModule() { static HMODULE dwmapi = []() -> HMODULE { HMODULE module = GetModuleHandleW(L"dwmapi.dll"); if (module == nullptr) { module = LoadLibraryW(L"dwmapi.dll"); } return module; }(); return dwmapi; } template ProcedureType GetDwmApiProcedure(const char* name) { const HMODULE dwmapi = GetDwmApiModule(); if (dwmapi == nullptr) { return nullptr; } return reinterpret_cast(GetProcAddress(dwmapi, name)); } 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; } template void ApplyDwmWindowAttribute(HWND hwnd, DWORD attribute, const ValueType& value) { if (hwnd == nullptr) { return; } static const DwmSetWindowAttributeFn setWindowAttribute = GetDwmApiProcedure("DwmSetWindowAttribute"); if (setWindowAttribute != nullptr) { setWindowAttribute(hwnd, attribute, &value, sizeof(value)); } } } // namespace void EnableBorderlessWindowShadow(HWND hwnd) { if (hwnd == nullptr) { return; } static const DwmExtendFrameIntoClientAreaFn extendFrameIntoClientArea = GetDwmApiProcedure("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; } const int ncRenderingPolicy = DWMNCRP_DISABLED; const BOOL allowNcPaint = FALSE; const BOOL disableTransitions = TRUE; const BOOL useImmersiveDarkMode = TRUE; const COLORREF captionColor = RGB(26, 26, 26); const COLORREF textColor = RGB(235, 235, 235); const COLORREF borderColor = RGB(26, 26, 26); ApplyDwmWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, ncRenderingPolicy); ApplyDwmWindowAttribute(hwnd, DWMWA_ALLOW_NCPAINT, allowNcPaint); ApplyDwmWindowAttribute(hwnd, DWMWA_TRANSITIONS_FORCEDISABLED, disableTransitions); ApplyDwmWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, useImmersiveDarkMode); ApplyDwmWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, captionColor); ApplyDwmWindowAttribute(hwnd, DWMWA_TEXT_COLOR, textColor); ApplyDwmWindowAttribute(hwnd, DWMWA_BORDER_COLOR, borderColor); EnableBorderlessWindowShadow(hwnd); } bool HandleBorderlessWindowGetMinMaxInfo( HWND hwnd, LPARAM lParam, int minimumOuterWidth, int minimumOuterHeight) { if (hwnd == nullptr || lParam == 0) { return false; } auto* minMaxInfo = reinterpret_cast(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; minMaxInfo->ptMinTrackSize.x = (std::max)(minimumOuterWidth, 1); minMaxInfo->ptMinTrackSize.y = (std::max)(minimumOuterHeight, 1); 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