Add borderless editor host chrome

This commit is contained in:
2026-04-14 01:14:45 +08:00
parent 5797a75619
commit 9064c2f5f2
6 changed files with 660 additions and 3 deletions

View File

@@ -38,6 +38,8 @@ constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost";
constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor";
constexpr UINT kDefaultDpi = 96u;
constexpr float kBaseDpiScale = 96.0f;
constexpr DWORD kBorderlessWindowStyle =
WS_POPUP | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
bool ResolveVerboseRuntimeTraceEnabled() {
wchar_t buffer[8] = {};
@@ -406,7 +408,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
WNDCLASSEXW windowClass = {};
windowClass.cbSize = sizeof(windowClass);
windowClass.style = CS_HREDRAW | CS_VREDRAW;
windowClass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;
windowClass.lpfnWndProc = &Application::WndProc;
windowClass.hInstance = hInstance;
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
@@ -418,10 +420,10 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
}
m_hwnd = CreateWindowExW(
0,
WS_EX_APPWINDOW,
kWindowClassName,
kWindowTitle,
WS_OVERLAPPEDWINDOW,
kBorderlessWindowStyle,
CW_USEDEFAULT,
CW_USEDEFAULT,
1540,
@@ -434,6 +436,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
LogRuntimeTrace("app", "window creation failed");
return false;
}
Host::EnableBorderlessWindowShadow(m_hwnd);
m_hostRuntime.Reset();
m_hostRuntime.SetWindowDpi(QueryWindowDpi(m_hwnd));
m_renderer.SetDpiScale(GetDpiScale());
@@ -607,6 +610,8 @@ void Application::RenderFrame() {
12.0f);
}
AppendBorderlessWindowChrome(drawList, width);
const Host::D3D12WindowRenderLoopPresentResult presentResult =
m_windowRenderLoop.Present(drawData);
if (!presentResult.warning.empty()) {
@@ -631,6 +636,18 @@ void Application::OnPaintMessage() {
EndPaint(m_hwnd, &paintStruct);
}
bool Application::IsBorderlessWindowEnabled() const {
return true;
}
LRESULT Application::HandleBorderlessWindowNcCalcSize(WPARAM wParam, LPARAM lParam) const {
return Host::HandleBorderlessWindowNcCalcSize(
m_hwnd,
wParam,
lParam,
m_hostRuntime.GetWindowDpi());
}
float Application::GetDpiScale() const {
return m_hostRuntime.GetDpiScale(kBaseDpiScale);
}
@@ -690,6 +707,175 @@ bool Application::ApplyCurrentCursor() const {
return true;
}
Host::BorderlessWindowChromeLayout Application::ResolveBorderlessWindowChromeLayout(
float clientWidthDips) const {
const auto& menuBarMetrics = ResolveUIEditorMenuBarMetrics();
::XCEngine::UI::UIRect titleBarRect(0.0f, 0.0f, clientWidthDips, menuBarMetrics.barHeight);
float leadingOccupiedRight = titleBarRect.x;
const UIEditorShellInteractionFrame& shellFrame = m_editorWorkspace.GetShellFrame();
if (shellFrame.shellFrame.layout.menuBarRect.width > 0.0f &&
shellFrame.shellFrame.layout.menuBarRect.height > 0.0f) {
titleBarRect = shellFrame.shellFrame.layout.menuBarRect;
}
const auto& buttonRects = shellFrame.shellFrame.layout.menuBarLayout.buttonRects;
if (!buttonRects.empty()) {
const auto& lastRect = buttonRects.back();
leadingOccupiedRight = lastRect.x + lastRect.width;
}
return Host::BuildBorderlessWindowChromeLayout(titleBarRect, leadingOccupiedRight);
}
Host::BorderlessWindowChromeHitTarget Application::HitTestBorderlessWindowChrome(LPARAM lParam) const {
if (!IsBorderlessWindowEnabled() || m_hwnd == nullptr) {
return Host::BorderlessWindowChromeHitTarget::None;
}
RECT clientRect = {};
if (!GetClientRect(m_hwnd, &clientRect)) {
return Host::BorderlessWindowChromeHitTarget::None;
}
const float clientWidthDips = PixelsToDips(
static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L)));
const Host::BorderlessWindowChromeLayout layout =
ResolveBorderlessWindowChromeLayout(clientWidthDips);
return Host::HitTestBorderlessWindowChrome(
layout,
ConvertClientPixelsToDips(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)));
}
bool Application::UpdateBorderlessWindowChromeHover(LPARAM lParam) {
const Host::BorderlessWindowChromeHitTarget hitTarget = HitTestBorderlessWindowChrome(lParam);
const Host::BorderlessWindowChromeHitTarget buttonTarget =
hitTarget == Host::BorderlessWindowChromeHitTarget::MinimizeButton ||
hitTarget == Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton ||
hitTarget == Host::BorderlessWindowChromeHitTarget::CloseButton
? hitTarget
: Host::BorderlessWindowChromeHitTarget::None;
if (m_borderlessWindowChromeState.hoveredTarget == buttonTarget) {
return false;
}
m_borderlessWindowChromeState.hoveredTarget = buttonTarget;
return true;
}
bool Application::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) {
const Host::BorderlessWindowChromeHitTarget hitTarget = HitTestBorderlessWindowChrome(lParam);
switch (hitTarget) {
case Host::BorderlessWindowChromeHitTarget::MinimizeButton:
case Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton:
case Host::BorderlessWindowChromeHitTarget::CloseButton:
m_borderlessWindowChromeState.pressedTarget = hitTarget;
if (m_hwnd != nullptr) {
SetCapture(m_hwnd);
}
InvalidateHostWindow();
return true;
case Host::BorderlessWindowChromeHitTarget::DragRegion:
if (m_hwnd != nullptr) {
ReleaseCapture();
SendMessageW(m_hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0);
}
return true;
case Host::BorderlessWindowChromeHitTarget::None:
default:
return false;
}
}
bool Application::HandleBorderlessWindowChromeButtonUp(LPARAM lParam) {
const Host::BorderlessWindowChromeHitTarget pressedTarget =
m_borderlessWindowChromeState.pressedTarget;
if (pressedTarget != Host::BorderlessWindowChromeHitTarget::MinimizeButton &&
pressedTarget != Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton &&
pressedTarget != Host::BorderlessWindowChromeHitTarget::CloseButton) {
return false;
}
const Host::BorderlessWindowChromeHitTarget releasedTarget =
HitTestBorderlessWindowChrome(lParam);
m_borderlessWindowChromeState.pressedTarget = Host::BorderlessWindowChromeHitTarget::None;
if (GetCapture() == m_hwnd) {
ReleaseCapture();
}
InvalidateHostWindow();
if (pressedTarget == releasedTarget) {
ExecuteBorderlessWindowChromeAction(pressedTarget);
}
return true;
}
bool Application::HandleBorderlessWindowChromeDoubleClick(LPARAM lParam) {
if (HitTestBorderlessWindowChrome(lParam) != Host::BorderlessWindowChromeHitTarget::DragRegion) {
return false;
}
ExecuteBorderlessWindowChromeAction(Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton);
return true;
}
void Application::ClearBorderlessWindowChromeState() {
if (m_borderlessWindowChromeState.hoveredTarget == Host::BorderlessWindowChromeHitTarget::None &&
m_borderlessWindowChromeState.pressedTarget == Host::BorderlessWindowChromeHitTarget::None) {
return;
}
m_borderlessWindowChromeState = {};
InvalidateHostWindow();
}
void Application::AppendBorderlessWindowChrome(
::XCEngine::UI::UIDrawList& drawList,
float clientWidthDips) const {
if (!IsBorderlessWindowEnabled()) {
return;
}
const Host::BorderlessWindowChromeLayout layout =
ResolveBorderlessWindowChromeLayout(clientWidthDips);
Host::AppendBorderlessWindowChrome(
drawList,
layout,
m_borderlessWindowChromeState,
m_hwnd != nullptr && IsZoomed(m_hwnd));
}
void Application::ExecuteBorderlessWindowChromeAction(
Host::BorderlessWindowChromeHitTarget target) {
if (m_hwnd == nullptr) {
return;
}
switch (target) {
case Host::BorderlessWindowChromeHitTarget::MinimizeButton:
ShowWindow(m_hwnd, SW_MINIMIZE);
break;
case Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton:
ShowWindow(m_hwnd, IsZoomed(m_hwnd) ? SW_RESTORE : SW_MAXIMIZE);
break;
case Host::BorderlessWindowChromeHitTarget::CloseButton:
PostMessageW(m_hwnd, WM_CLOSE, 0, 0);
break;
case Host::BorderlessWindowChromeHitTarget::DragRegion:
case Host::BorderlessWindowChromeHitTarget::None:
default:
break;
}
InvalidateHostWindow();
}
void Application::InvalidateHostWindow() const {
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
InvalidateRect(m_hwnd, nullptr, FALSE);
}
}
UIPoint Application::ConvertClientPixelsToDips(LONG x, LONG y) const {
return UIPoint(
PixelsToDips(static_cast<float>(x)),
@@ -822,6 +1008,7 @@ void Application::OnDpiChanged(UINT dpi, const RECT& suggestedRect) {
if (QueryCurrentClientPixelSize(clientWidth, clientHeight)) {
ApplyWindowResize(clientWidth, clientHeight);
}
Host::EnableBorderlessWindowShadow(m_hwnd);
}
std::ostringstream trace = {};
@@ -969,6 +1156,9 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
switch (message) {
case WM_MOUSEMOVE:
if (application != nullptr) {
if (application->UpdateBorderlessWindowChromeHover(lParam)) {
application->InvalidateHostWindow();
}
if (!application->m_trackingMouseLeave) {
TRACKMOUSEEVENT trackMouseEvent = {};
trackMouseEvent.cbSize = sizeof(trackMouseEvent);
@@ -978,6 +1168,13 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
application->m_trackingMouseLeave = true;
}
}
const Host::BorderlessWindowChromeHitTarget chromeHitTarget =
application->HitTestBorderlessWindowChrome(lParam);
if (chromeHitTarget == Host::BorderlessWindowChromeHitTarget::MinimizeButton ||
chromeHitTarget == Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton ||
chromeHitTarget == Host::BorderlessWindowChromeHitTarget::CloseButton) {
return 0;
}
application->QueuePointerEvent(
UIInputEventType::PointerMove,
UIPointerButton::None,
@@ -989,12 +1186,16 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
case WM_MOUSELEAVE:
if (application != nullptr) {
application->m_trackingMouseLeave = false;
application->ClearBorderlessWindowChromeState();
application->QueuePointerLeaveEvent();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (application != nullptr) {
if (application->HandleBorderlessWindowChromeButtonDown(lParam)) {
return 0;
}
SetFocus(hwnd);
application->QueuePointerEvent(
UIInputEventType::PointerButtonDown,
@@ -1006,6 +1207,9 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
break;
case WM_LBUTTONUP:
if (application != nullptr) {
if (application->HandleBorderlessWindowChromeButtonUp(lParam)) {
return 0;
}
application->QueuePointerEvent(
UIInputEventType::PointerButtonUp,
UIPointerButton::Left,
@@ -1014,6 +1218,12 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
return 0;
}
break;
case WM_LBUTTONDBLCLK:
if (application != nullptr &&
application->HandleBorderlessWindowChromeDoubleClick(lParam)) {
return 0;
}
break;
case WM_MOUSEWHEEL:
if (application != nullptr) {
application->QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam);
@@ -1039,8 +1249,13 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
reinterpret_cast<HWND>(lParam) != hwnd &&
application->HasInteractiveCaptureState()) {
application->QueueWindowFocusEvent(UIInputEventType::FocusLost);
application->ClearBorderlessWindowChromeState();
return 0;
}
if (application != nullptr &&
reinterpret_cast<HWND>(lParam) != hwnd) {
application->ClearBorderlessWindowChromeState();
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN: