diff --git a/engine/include/XCEngine/UI/Layout/UISplitterLayout.h b/engine/include/XCEngine/UI/Layout/UISplitterLayout.h index 117df9cf..4d371e5e 100644 --- a/engine/include/XCEngine/UI/Layout/UISplitterLayout.h +++ b/engine/include/XCEngine/UI/Layout/UISplitterLayout.h @@ -49,6 +49,25 @@ inline float ClampFiniteExtent(float value, float minValue, float maxValue) { return (std::clamp)(value, minValue, maxValue); } +inline float ResolveOverflowPrimaryExtentFromMinimums( + float usableExtent, + float preferredPrimaryMinimum, + float preferredSecondaryMinimum) { + const float clampedUsableExtent = ClampSplitterExtent(usableExtent); + const float primaryMinimum = ClampSplitterExtent(preferredPrimaryMinimum); + const float secondaryMinimum = ClampSplitterExtent(preferredSecondaryMinimum); + const float totalMinimum = primaryMinimum + secondaryMinimum; + if (clampedUsableExtent <= 0.0f) { + return 0.0f; + } + + if (totalMinimum <= 0.0f) { + return clampedUsableExtent * 0.5f; + } + + return clampedUsableExtent * (primaryMinimum / totalMinimum); +} + inline float GetMainExtent(const UISize& size, UILayoutAxis axis) { return axis == UILayoutAxis::Horizontal ? size.width : size.height; } @@ -108,8 +127,12 @@ inline float ClampSplitterRatio( float minimumPrimaryExtent = minPrimaryExtent; float maximumPrimaryExtent = usableExtent - minSecondaryExtent; if (minimumPrimaryExtent > maximumPrimaryExtent) { - minimumPrimaryExtent = 0.0f; - maximumPrimaryExtent = usableExtent; + const float overflowPrimaryExtent = + SplitterDetail::ResolveOverflowPrimaryExtentFromMinimums( + usableExtent, + options.minPrimaryExtent, + options.minSecondaryExtent); + return usableExtent <= 0.0f ? 0.5f : overflowPrimaryExtent / usableExtent; } const float clampedPrimaryExtent = (std::clamp)( @@ -161,7 +184,20 @@ inline float ClampSplitterRatio( minimumPrimaryExtent = (std::max)(minimumPrimaryExtent, minimumFromSecondary); maximumPrimaryExtent = (std::min)(maximumPrimaryExtent, maximumFromSecondary); + const bool minimumsOverflow = + SplitterDetail::ClampSplitterExtent(constraints.primaryMin) + + SplitterDetail::ClampSplitterExtent(constraints.secondaryMin) > + usableExtent; if (minimumPrimaryExtent > maximumPrimaryExtent) { + if (minimumsOverflow) { + const float overflowPrimaryExtent = + SplitterDetail::ResolveOverflowPrimaryExtentFromMinimums( + usableExtent, + constraints.primaryMin, + constraints.secondaryMin); + return usableExtent <= 0.0f ? 0.5f : overflowPrimaryExtent / usableExtent; + } + minimumPrimaryExtent = 0.0f; maximumPrimaryExtent = usableExtent; } diff --git a/new_editor/app/Application.cpp b/new_editor/app/Application.cpp index b7a2cf2a..0b4d833d 100644 --- a/new_editor/app/Application.cpp +++ b/new_editor/app/Application.cpp @@ -36,8 +36,11 @@ using ::XCEngine::UI::UIRect; constexpr const wchar_t* kWindowClassName = L"XCEditorShellHost"; constexpr const wchar_t* kWindowTitle = L"Main Scene * - Main.xx - XCEngine Editor"; +constexpr const char* kWindowTitleText = "Main Scene * - Main.xx - XCEngine Editor"; constexpr UINT kDefaultDpi = 96u; constexpr float kBaseDpiScale = 96.0f; +constexpr float kBorderlessTitleBarHeightDips = 28.0f; +constexpr float kBorderlessTitleBarFontSize = 12.0f; constexpr DWORD kBorderlessWindowStyle = WS_POPUP | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX; @@ -436,7 +439,7 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) { LogRuntimeTrace("app", "window creation failed"); return false; } - Host::EnableBorderlessWindowShadow(m_hwnd); + Host::RefreshBorderlessWindowDwmDecorations(m_hwnd); m_hostRuntime.Reset(); m_hostRuntime.SetWindowDpi(QueryWindowDpi(m_hwnd)); m_renderer.SetDpiScale(GetDpiScale()); @@ -534,6 +537,7 @@ void Application::RenderFrame() { } const float width = PixelsToDips(static_cast(pixelWidth)); const float height = PixelsToDips(static_cast(pixelHeight)); + const UIRect workspaceBounds = ResolveWorkspaceBounds(width, height); UIDrawData drawData = {}; UIDrawList& drawList = drawData.EmplaceDrawList("XCEditorShell"); @@ -560,7 +564,7 @@ void Application::RenderFrame() { m_editorWorkspace.Update( m_editorContext, - UIRect(0.0f, 0.0f, width, height), + workspaceBounds, frameEvents, BuildCaptureStatusText()); const UIEditorShellInteractionFrame& shellFrame = @@ -639,6 +643,34 @@ bool Application::IsBorderlessWindowEnabled() const { return true; } +bool Application::IsBorderlessWindowMaximized() const { + return m_hostRuntime.IsBorderlessWindowMaximized(); +} + +bool Application::HandleBorderlessWindowSystemCommand(WPARAM wParam) { + if (!IsBorderlessWindowEnabled()) { + return false; + } + + switch (wParam & 0xFFF0u) { + case SC_MAXIMIZE: + ToggleBorderlessWindowMaximizeRestore(); + return true; + case SC_RESTORE: + if (!IsIconic(m_hwnd)) { + ToggleBorderlessWindowMaximizeRestore(); + return true; + } + return false; + default: + return false; + } +} + +bool Application::HandleBorderlessWindowGetMinMaxInfo(LPARAM lParam) const { + return Host::HandleBorderlessWindowGetMinMaxInfo(m_hwnd, lParam); +} + LRESULT Application::HandleBorderlessWindowNcCalcSize(WPARAM wParam, LPARAM lParam) const { return Host::HandleBorderlessWindowNcCalcSize( m_hwnd, @@ -715,7 +747,7 @@ bool Application::ApplyCurrentCursor() const { } Host::BorderlessWindowResizeEdge Application::HitTestBorderlessWindowResizeEdge(LPARAM lParam) const { - if (!IsBorderlessWindowEnabled() || m_hwnd == nullptr || IsZoomed(m_hwnd)) { + if (!IsBorderlessWindowEnabled() || m_hwnd == nullptr || IsBorderlessWindowMaximized()) { return Host::BorderlessWindowResizeEdge::None; } @@ -857,23 +889,9 @@ void Application::ApplyBorderlessWindowResizeCursorHoverPriority() { 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); + return Host::BuildBorderlessWindowChromeLayout( + ::XCEngine::UI::UIRect(0.0f, 0.0f, clientWidthDips, kBorderlessTitleBarHeightDips), + 0.0f); } Host::BorderlessWindowChromeHitTarget Application::HitTestBorderlessWindowChrome(LPARAM lParam) const { @@ -938,6 +956,15 @@ bool Application::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) { return true; case Host::BorderlessWindowChromeHitTarget::DragRegion: if (m_hwnd != nullptr) { + if (IsBorderlessWindowMaximized()) { + POINT screenPoint = {}; + if (GetCursorPos(&screenPoint)) { + m_hostRuntime.BeginBorderlessWindowDragRestore(screenPoint); + SetCapture(m_hwnd); + return true; + } + } + ReleaseCapture(); SendMessageW(m_hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); } @@ -949,6 +976,11 @@ bool Application::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) { } bool Application::HandleBorderlessWindowChromeButtonUp(LPARAM lParam) { + if (m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) { + ClearBorderlessWindowChromeDragRestoreState(); + return true; + } + const Host::BorderlessWindowChromeHitTarget pressedTarget = m_borderlessWindowChromeState.pressedTarget; if (pressedTarget != Host::BorderlessWindowChromeHitTarget::MinimizeButton && @@ -972,6 +1004,10 @@ bool Application::HandleBorderlessWindowChromeButtonUp(LPARAM lParam) { } bool Application::HandleBorderlessWindowChromeDoubleClick(LPARAM lParam) { + if (m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) { + ClearBorderlessWindowChromeDragRestoreState(); + } + if (HitTestBorderlessWindowChrome(lParam) != Host::BorderlessWindowChromeHitTarget::DragRegion) { return false; } @@ -980,6 +1016,85 @@ bool Application::HandleBorderlessWindowChromeDoubleClick(LPARAM lParam) { return true; } +bool Application::HandleBorderlessWindowChromeDragRestorePointerMove() { + if (!m_hostRuntime.IsBorderlessWindowDragRestoreArmed() || m_hwnd == nullptr) { + return false; + } + + POINT currentScreenPoint = {}; + if (!GetCursorPos(¤tScreenPoint)) { + return true; + } + + const POINT initialScreenPoint = m_hostRuntime.GetBorderlessWindowDragRestoreInitialScreenPoint(); + const int dragThresholdX = (std::max)(GetSystemMetrics(SM_CXDRAG), 1); + const int dragThresholdY = (std::max)(GetSystemMetrics(SM_CYDRAG), 1); + const LONG deltaX = currentScreenPoint.x - initialScreenPoint.x; + const LONG deltaY = currentScreenPoint.y - initialScreenPoint.y; + if (std::abs(deltaX) < dragThresholdX && std::abs(deltaY) < dragThresholdY) { + return true; + } + + RECT restoreRect = {}; + RECT currentRect = {}; + RECT workAreaRect = {}; + if (!m_hostRuntime.TryGetBorderlessWindowRestoreRect(restoreRect) || + !QueryCurrentWindowRect(currentRect) || + !QueryBorderlessWindowWorkAreaRect(workAreaRect)) { + ClearBorderlessWindowChromeDragRestoreState(); + return true; + } + + const int restoreWidth = restoreRect.right - restoreRect.left; + const int restoreHeight = restoreRect.bottom - restoreRect.top; + const int currentWidth = currentRect.right - currentRect.left; + if (restoreWidth <= 0 || restoreHeight <= 0 || currentWidth <= 0) { + ClearBorderlessWindowChromeDragRestoreState(); + return true; + } + + const float pointerRatio = + static_cast(currentScreenPoint.x - currentRect.left) / + static_cast(currentWidth); + const float clampedPointerRatio = (std::clamp)(pointerRatio, 0.0f, 1.0f); + const int newLeft = + (std::clamp)( + currentScreenPoint.x - static_cast(clampedPointerRatio * static_cast(restoreWidth)), + workAreaRect.left, + workAreaRect.right - restoreWidth); + const int titleBarHeightPixels = + static_cast(kBorderlessTitleBarHeightDips * GetDpiScale()); + const int newTop = + (std::clamp)( + currentScreenPoint.y - (std::max)(titleBarHeightPixels / 2, 1), + workAreaRect.top, + workAreaRect.bottom - restoreHeight); + const RECT targetRect = { + newLeft, + newTop, + newLeft + restoreWidth, + newTop + restoreHeight + }; + + m_hostRuntime.SetBorderlessWindowMaximized(false); + ApplyPredictedWindowRectTransition(targetRect); + ClearBorderlessWindowChromeDragRestoreState(); + ReleaseCapture(); + SendMessageW(m_hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); + return true; +} + +void Application::ClearBorderlessWindowChromeDragRestoreState() { + if (!m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) { + return; + } + + m_hostRuntime.EndBorderlessWindowDragRestore(); + if (GetCapture() == m_hwnd) { + ReleaseCapture(); + } +} + void Application::ClearBorderlessWindowChromeState() { if (m_borderlessWindowChromeState.hoveredTarget == Host::BorderlessWindowChromeHitTarget::None && m_borderlessWindowChromeState.pressedTarget == Host::BorderlessWindowChromeHitTarget::None) { @@ -999,11 +1114,29 @@ void Application::AppendBorderlessWindowChrome( const Host::BorderlessWindowChromeLayout layout = ResolveBorderlessWindowChromeLayout(clientWidthDips); + drawList.AddFilledRect( + layout.titleBarRect, + UIColor(0.97f, 0.97f, 0.97f, 1.0f)); + drawList.AddLine( + UIPoint(layout.titleBarRect.x, layout.titleBarRect.y + layout.titleBarRect.height), + UIPoint( + layout.titleBarRect.x + layout.titleBarRect.width, + layout.titleBarRect.y + layout.titleBarRect.height), + UIColor(0.78f, 0.78f, 0.78f, 1.0f), + 1.0f); + drawList.AddText( + UIPoint( + 12.0f, + layout.titleBarRect.y + + (std::max)(0.0f, (layout.titleBarRect.height - kBorderlessTitleBarFontSize) * 0.5f - 1.0f)), + kWindowTitleText, + UIColor(0.12f, 0.12f, 0.12f, 1.0f), + kBorderlessTitleBarFontSize); Host::AppendBorderlessWindowChrome( drawList, layout, m_borderlessWindowChromeState, - m_hwnd != nullptr && IsZoomed(m_hwnd)); + IsBorderlessWindowMaximized()); } void Application::ExecuteBorderlessWindowChromeAction( @@ -1017,7 +1150,7 @@ void Application::ExecuteBorderlessWindowChromeAction( ShowWindow(m_hwnd, SW_MINIMIZE); break; case Host::BorderlessWindowChromeHitTarget::MaximizeRestoreButton: - ShowWindow(m_hwnd, IsZoomed(m_hwnd) ? SW_RESTORE : SW_MAXIMIZE); + ToggleBorderlessWindowMaximizeRestore(); break; case Host::BorderlessWindowChromeHitTarget::CloseButton: PostMessageW(m_hwnd, WM_CLOSE, 0, 0); @@ -1031,6 +1164,89 @@ void Application::ExecuteBorderlessWindowChromeAction( InvalidateHostWindow(); } +bool Application::QueryCurrentWindowRect(RECT& outRect) const { + outRect = {}; + return m_hwnd != nullptr && GetWindowRect(m_hwnd, &outRect) != FALSE; +} + +bool Application::QueryBorderlessWindowWorkAreaRect(RECT& outRect) const { + outRect = {}; + if (m_hwnd == nullptr) { + return false; + } + + const HMONITOR monitor = MonitorFromWindow(m_hwnd, MONITOR_DEFAULTTONEAREST); + if (monitor == nullptr) { + return false; + } + + MONITORINFO monitorInfo = {}; + monitorInfo.cbSize = sizeof(monitorInfo); + if (!GetMonitorInfoW(monitor, &monitorInfo)) { + return false; + } + + outRect = monitorInfo.rcWork; + return true; +} + +bool Application::ApplyPredictedWindowRectTransition(const RECT& targetRect) { + if (m_hwnd == nullptr) { + return false; + } + + const int width = targetRect.right - targetRect.left; + const int height = targetRect.bottom - targetRect.top; + if (width <= 0 || height <= 0) { + return false; + } + + m_hostRuntime.SetPredictedClientPixelSize( + static_cast(width), + static_cast(height)); + ApplyWindowResize( + static_cast(width), + static_cast(height)); + RenderFrame(); + SetWindowPos( + m_hwnd, + nullptr, + targetRect.left, + targetRect.top, + width, + height, + SWP_NOZORDER | SWP_NOACTIVATE); + InvalidateHostWindow(); + return true; +} + +void Application::ToggleBorderlessWindowMaximizeRestore() { + if (m_hwnd == nullptr) { + return; + } + + if (!IsBorderlessWindowMaximized()) { + RECT currentRect = {}; + RECT workAreaRect = {}; + if (!QueryCurrentWindowRect(currentRect) || !QueryBorderlessWindowWorkAreaRect(workAreaRect)) { + return; + } + + m_hostRuntime.SetBorderlessWindowRestoreRect(currentRect); + m_hostRuntime.SetBorderlessWindowMaximized(true); + ApplyPredictedWindowRectTransition(workAreaRect); + return; + } + + RECT restoreRect = {}; + if (!m_hostRuntime.TryGetBorderlessWindowRestoreRect(restoreRect)) { + return; + } + + m_hostRuntime.SetBorderlessWindowMaximized(false); + ApplyPredictedWindowRectTransition(restoreRect); +} + void Application::InvalidateHostWindow() const { if (m_hwnd != nullptr && IsWindow(m_hwnd)) { InvalidateRect(m_hwnd, nullptr, FALSE); @@ -1091,6 +1307,9 @@ std::string Application::DescribeInputEvents( void Application::OnResize(UINT width, UINT height) { m_hostRuntime.ClearPredictedClientPixelSize(); + if (IsBorderlessWindowEnabled() && m_hwnd != nullptr) { + Host::RefreshBorderlessWindowDwmDecorations(m_hwnd); + } ApplyWindowResize(width, height); } @@ -1160,6 +1379,19 @@ bool Application::ResolveRenderClientPixelSize(UINT& outWidth, UINT& outHeight) return QueryCurrentClientPixelSize(outWidth, outHeight); } +UIRect Application::ResolveWorkspaceBounds(float clientWidthDips, float clientHeightDips) const { + if (!IsBorderlessWindowEnabled()) { + return UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips); + } + + const float titleBarHeight = (std::min)(kBorderlessTitleBarHeightDips, clientHeightDips); + return UIRect( + 0.0f, + titleBarHeight, + clientWidthDips, + (std::max)(0.0f, clientHeightDips - titleBarHeight)); +} + void Application::OnDpiChanged(UINT dpi, const RECT& suggestedRect) { m_hostRuntime.SetWindowDpi(dpi == 0u ? kDefaultDpi : dpi); m_renderer.SetDpiScale(GetDpiScale()); @@ -1179,7 +1411,7 @@ void Application::OnDpiChanged(UINT dpi, const RECT& suggestedRect) { if (QueryCurrentClientPixelSize(clientWidth, clientHeight)) { ApplyWindowResize(clientWidth, clientHeight); } - Host::EnableBorderlessWindowShadow(m_hwnd); + Host::RefreshBorderlessWindowDwmDecorations(m_hwnd); } std::ostringstream trace = {}; @@ -1209,7 +1441,8 @@ void Application::ApplyHostedContentCaptureRequests() { } bool Application::HasInteractiveCaptureState() const { - return m_editorWorkspace.HasInteractiveCapture(); + return m_editorWorkspace.HasInteractiveCapture() || + m_hostRuntime.IsBorderlessWindowDragRestoreArmed(); } void Application::QueuePointerEvent( @@ -1330,6 +1563,9 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP if (application->HandleBorderlessWindowResizePointerMove()) { return 0; } + if (application->HandleBorderlessWindowChromeDragRestorePointerMove()) { + return 0; + } const bool resizeHoverChanged = application->UpdateBorderlessWindowResizeHover(lParam); if (application->UpdateBorderlessWindowChromeHover(lParam)) { @@ -1370,6 +1606,7 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP if (application != nullptr) { application->m_trackingMouseLeave = false; application->ClearBorderlessWindowResizeState(); + application->ClearBorderlessWindowChromeDragRestoreState(); application->ClearBorderlessWindowChromeState(); application->QueuePointerLeaveEvent(); return 0; @@ -1440,12 +1677,14 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP application->HasInteractiveCaptureState()) { application->QueueWindowFocusEvent(UIInputEventType::FocusLost); application->ForceClearBorderlessWindowResizeState(); + application->ClearBorderlessWindowChromeDragRestoreState(); application->ClearBorderlessWindowChromeState(); return 0; } if (application != nullptr && reinterpret_cast(lParam) != hwnd) { application->ForceClearBorderlessWindowResizeState(); + application->ClearBorderlessWindowChromeDragRestoreState(); application->ClearBorderlessWindowChromeState(); } break; diff --git a/new_editor/app/Application.h b/new_editor/app/Application.h index 3a23a2d2..e0ae6451 100644 --- a/new_editor/app/Application.h +++ b/new_editor/app/Application.h @@ -55,6 +55,9 @@ private: bool ApplyWindowResize(UINT width, UINT height); bool QueryCurrentClientPixelSize(UINT& outWidth, UINT& outHeight) const; bool ResolveRenderClientPixelSize(UINT& outWidth, UINT& outHeight) const; + ::XCEngine::UI::UIRect ResolveWorkspaceBounds( + float clientWidthDips, + float clientHeightDips) const; bool IsPointerInsideClientArea() const; bool ApplyCurrentCursor() const; LPCWSTR ResolveCurrentCursorResource() const; @@ -79,12 +82,17 @@ private: void QueueCharacterEvent(WPARAM wParam, LPARAM lParam); void QueueWindowFocusEvent(::XCEngine::UI::UIInputEventType type); bool IsBorderlessWindowEnabled() const; + bool IsBorderlessWindowMaximized() const; + bool HandleBorderlessWindowSystemCommand(WPARAM wParam); + bool HandleBorderlessWindowGetMinMaxInfo(LPARAM lParam) const; LRESULT HandleBorderlessWindowNcCalcSize(WPARAM wParam, LPARAM lParam) const; Host::BorderlessWindowChromeHitTarget HitTestBorderlessWindowChrome(LPARAM lParam) const; bool UpdateBorderlessWindowChromeHover(LPARAM lParam); bool HandleBorderlessWindowChromeButtonDown(LPARAM lParam); bool HandleBorderlessWindowChromeButtonUp(LPARAM lParam); bool HandleBorderlessWindowChromeDoubleClick(LPARAM lParam); + bool HandleBorderlessWindowChromeDragRestorePointerMove(); + void ClearBorderlessWindowChromeDragRestoreState(); void ClearBorderlessWindowChromeState(); Host::BorderlessWindowResizeEdge HitTestBorderlessWindowResizeEdge(LPARAM lParam) const; bool UpdateBorderlessWindowResizeHover(LPARAM lParam); @@ -94,6 +102,10 @@ private: void ClearBorderlessWindowResizeState(); void ForceClearBorderlessWindowResizeState(); void ApplyBorderlessWindowResizeCursorHoverPriority(); + bool QueryCurrentWindowRect(RECT& outRect) const; + bool QueryBorderlessWindowWorkAreaRect(RECT& outRect) const; + bool ApplyPredictedWindowRectTransition(const RECT& targetRect); + void ToggleBorderlessWindowMaximizeRestore(); void AppendBorderlessWindowChrome( ::XCEngine::UI::UIDrawList& drawList, float clientWidthDips) const; diff --git a/new_editor/app/Host/BorderlessWindowChrome.cpp b/new_editor/app/Host/BorderlessWindowChrome.cpp index 90955b79..2aa317f7 100644 --- a/new_editor/app/Host/BorderlessWindowChrome.cpp +++ b/new_editor/app/Host/BorderlessWindowChrome.cpp @@ -25,13 +25,14 @@ void AppendMinimizeGlyph( UIDrawList& drawList, const UIRect& rect, const UIColor& color, - float thickness, - float insetX, - float insetY) { - const float y = rect.y + rect.height - insetY; + 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(rect.x + insetX, y), - UIPoint(rect.x + rect.width - insetX, y), + UIPoint(centerX - halfWidth, y), + UIPoint(centerX + halfWidth, y), color, thickness); } @@ -40,15 +41,16 @@ void AppendMaximizeGlyph( UIDrawList& drawList, const UIRect& rect, const UIColor& color, - float thickness, - float insetX, - float insetY) { + 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( - rect.x + insetX, - rect.y + insetY, - (std::max)(0.0f, rect.width - insetX * 2.0f), - (std::max)(0.0f, rect.height - insetY * 2.0f)), + centerX - halfExtent, + centerY - halfExtent, + halfExtent * 2.0f, + halfExtent * 2.0f), color, thickness); } @@ -57,26 +59,25 @@ void AppendRestoreGlyph( UIDrawList& drawList, const UIRect& rect, const UIColor& color, - float thickness, - float insetX, - float insetY) { - const float width = (std::max)(0.0f, rect.width - insetX * 2.0f); - const float height = (std::max)(0.0f, rect.height - insetY * 2.0f); - const float offset = 3.0f; + 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( - rect.x + insetX + offset, - rect.y + insetY, - (std::max)(0.0f, width - offset), - (std::max)(0.0f, height - offset)), + centerX - halfExtent + offset, + centerY - halfExtent - offset, + halfExtent * 2.0f, + halfExtent * 2.0f), color, thickness); drawList.AddRectOutline( UIRect( - rect.x + insetX, - rect.y + insetY + offset, - (std::max)(0.0f, width - offset), - (std::max)(0.0f, height - offset)), + centerX - halfExtent - offset, + centerY - halfExtent + offset, + halfExtent * 2.0f, + halfExtent * 2.0f), color, thickness); } @@ -85,17 +86,19 @@ void AppendCloseGlyph( UIDrawList& drawList, const UIRect& rect, const UIColor& color, - float thickness, - float insetX, - float insetY) { + 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(rect.x + insetX, rect.y + insetY), - UIPoint(rect.x + rect.width - insetX, rect.y + rect.height - insetY), + UIPoint(centerX - halfWidth, centerY - halfHeight), + UIPoint(centerX + halfWidth, centerY + halfHeight), color, thickness); drawList.AddLine( - UIPoint(rect.x + rect.width - insetX, rect.y + insetY), - UIPoint(rect.x + insetX, rect.y + rect.height - insetY), + UIPoint(centerX + halfWidth, centerY - halfHeight), + UIPoint(centerX - halfWidth, centerY + halfHeight), color, thickness); } @@ -152,6 +155,58 @@ int QuerySystemMetricForDpi(int index, UINT 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( + GetProcAddress(dwmapi, "DwmSetWindowAttribute")); + }(); + + if (setWindowAttribute != nullptr) { + setWindowAttribute(hwnd, attribute, &value, sizeof(value)); + } +} + } // namespace BorderlessWindowChromeLayout BuildBorderlessWindowChromeLayout( @@ -232,9 +287,7 @@ void AppendBorderlessWindowChrome( drawList, button.rect, iconColor, - metrics.iconThickness, - metrics.iconInsetX, - metrics.iconInsetY); + metrics.iconThickness); break; case BorderlessWindowChromeHitTarget::MaximizeRestoreButton: if (maximized) { @@ -242,17 +295,13 @@ void AppendBorderlessWindowChrome( drawList, button.rect, iconColor, - metrics.iconThickness, - metrics.iconInsetX, - metrics.iconInsetY); + metrics.iconThickness); } else { AppendMaximizeGlyph( drawList, button.rect, iconColor, - metrics.iconThickness, - metrics.iconInsetX, - metrics.iconInsetY); + metrics.iconThickness); } break; case BorderlessWindowChromeHitTarget::CloseButton: @@ -260,9 +309,7 @@ void AppendBorderlessWindowChrome( drawList, button.rect, iconColor, - metrics.iconThickness, - metrics.iconInsetX, - metrics.iconInsetY); + metrics.iconThickness); break; case BorderlessWindowChromeHitTarget::DragRegion: case BorderlessWindowChromeHitTarget::None: @@ -290,38 +337,66 @@ void EnableBorderlessWindowShadow(HWND hwnd) { GetProcAddress(dwmapi, "DwmExtendFrameIntoClientArea")); }(); if (extendFrameIntoClientArea != nullptr) { - const MARGINS margins = { 1, 1, 1, 1 }; + 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(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) { - if (wParam == FALSE || lParam == 0) { - return 0; - } - - auto* params = reinterpret_cast(lParam); - if (params == nullptr) { - return 0; - } - - if (IsZoomed(hwnd)) { - const int frameX = - QuerySystemMetricForDpi(SM_CXFRAME, dpi) + - QuerySystemMetricForDpi(SM_CXPADDEDBORDER, dpi); - const int frameY = - QuerySystemMetricForDpi(SM_CYFRAME, dpi) + - QuerySystemMetricForDpi(SM_CXPADDEDBORDER, dpi); - params->rgrc[0].left += frameX; - params->rgrc[0].right -= frameX; - params->rgrc[0].top += frameY; - params->rgrc[0].bottom -= frameY; - } - + (void)hwnd; + (void)wParam; + (void)lParam; + (void)dpi; return 0; } diff --git a/new_editor/app/Host/BorderlessWindowChrome.h b/new_editor/app/Host/BorderlessWindowChrome.h index eff5c2cf..ef3d5959 100644 --- a/new_editor/app/Host/BorderlessWindowChrome.h +++ b/new_editor/app/Host/BorderlessWindowChrome.h @@ -19,13 +19,11 @@ enum class BorderlessWindowChromeHitTarget : std::uint8_t { }; struct BorderlessWindowChromeMetrics { - float buttonWidth = 45.0f; + float buttonWidth = 46.0f; float buttonInsetX = 0.0f; - float dragPaddingLeft = 8.0f; - float dragPaddingRight = 8.0f; - float iconInsetX = 16.0f; - float iconInsetY = 8.0f; - float iconThickness = 1.2f; + float dragPaddingLeft = 6.0f; + float dragPaddingRight = 6.0f; + float iconThickness = 1.0f; }; struct BorderlessWindowChromePalette { @@ -73,8 +71,11 @@ void AppendBorderlessWindowChrome( const BorderlessWindowChromePalette& palette = {}, const BorderlessWindowChromeMetrics& metrics = {}); +void RefreshBorderlessWindowDwmDecorations(HWND hwnd); void EnableBorderlessWindowShadow(HWND hwnd); +bool HandleBorderlessWindowGetMinMaxInfo(HWND hwnd, LPARAM lParam); + LRESULT HandleBorderlessWindowNcCalcSize( HWND hwnd, WPARAM wParam, diff --git a/new_editor/app/Host/HostRuntimeState.h b/new_editor/app/Host/HostRuntimeState.h index 200d3b5d..96017a6a 100644 --- a/new_editor/app/Host/HostRuntimeState.h +++ b/new_editor/app/Host/HostRuntimeState.h @@ -20,6 +20,17 @@ struct PredictedClientPixelSize { UINT height = 0u; }; +struct BorderlessWindowPlacementState { + bool maximized = false; + bool hasRestoreRect = false; + RECT restoreRect = {}; +}; + +struct BorderlessWindowDragRestoreState { + bool armed = false; + POINT initialScreenPoint = {}; +}; + class HostRuntimeState { public: void Reset() { @@ -27,6 +38,8 @@ public: m_inInteractiveResize = false; m_borderlessResizeState = {}; m_predictedClientPixelSize = {}; + m_borderlessWindowPlacementState = {}; + m_borderlessWindowDragRestoreState = {}; } void SetWindowDpi(UINT dpi) { @@ -127,11 +140,53 @@ public: return true; } + void SetBorderlessWindowMaximized(bool maximized) { + m_borderlessWindowPlacementState.maximized = maximized; + } + + bool IsBorderlessWindowMaximized() const { + return m_borderlessWindowPlacementState.maximized; + } + + void SetBorderlessWindowRestoreRect(const RECT& rect) { + m_borderlessWindowPlacementState.restoreRect = rect; + m_borderlessWindowPlacementState.hasRestoreRect = true; + } + + bool TryGetBorderlessWindowRestoreRect(RECT& outRect) const { + outRect = {}; + if (!m_borderlessWindowPlacementState.hasRestoreRect) { + return false; + } + + outRect = m_borderlessWindowPlacementState.restoreRect; + return true; + } + + void BeginBorderlessWindowDragRestore(const POINT& initialScreenPoint) { + m_borderlessWindowDragRestoreState.armed = true; + m_borderlessWindowDragRestoreState.initialScreenPoint = initialScreenPoint; + } + + void EndBorderlessWindowDragRestore() { + m_borderlessWindowDragRestoreState = {}; + } + + bool IsBorderlessWindowDragRestoreArmed() const { + return m_borderlessWindowDragRestoreState.armed; + } + + const POINT& GetBorderlessWindowDragRestoreInitialScreenPoint() const { + return m_borderlessWindowDragRestoreState.initialScreenPoint; + } + private: UINT m_windowDpi = 96u; bool m_inInteractiveResize = false; BorderlessWindowResizeState m_borderlessResizeState = {}; PredictedClientPixelSize m_predictedClientPixelSize = {}; + BorderlessWindowPlacementState m_borderlessWindowPlacementState = {}; + BorderlessWindowDragRestoreState m_borderlessWindowDragRestoreState = {}; }; } // namespace XCEngine::UI::Editor::Host diff --git a/new_editor/app/Host/WindowMessageDispatcher.cpp b/new_editor/app/Host/WindowMessageDispatcher.cpp index db906353..8379edfb 100644 --- a/new_editor/app/Host/WindowMessageDispatcher.cpp +++ b/new_editor/app/Host/WindowMessageDispatcher.cpp @@ -63,6 +63,13 @@ bool WindowMessageDispatcher::TryDispatch( }; switch (message) { + case WM_GETMINMAXINFO: + if (application.IsBorderlessWindowEnabled() && + application.HandleBorderlessWindowGetMinMaxInfo(lParam)) { + outResult = 0; + return true; + } + return false; case WM_NCCALCSIZE: if (application.IsBorderlessWindowEnabled()) { outResult = application.HandleBorderlessWindowNcCalcSize(wParam, lParam); @@ -75,6 +82,12 @@ bool WindowMessageDispatcher::TryDispatch( return true; } return false; + case WM_SYSCOMMAND: + if (application.HandleBorderlessWindowSystemCommand(wParam)) { + outResult = 0; + return true; + } + return false; case WM_SETCURSOR: if (LOWORD(lParam) == HTCLIENT && application.ApplyCurrentCursor()) { outResult = TRUE; diff --git a/new_editor/app/Shell/ProductShellAsset.cpp b/new_editor/app/Shell/ProductShellAsset.cpp index 018ab0c5..4a8298b4 100644 --- a/new_editor/app/Shell/ProductShellAsset.cpp +++ b/new_editor/app/Shell/ProductShellAsset.cpp @@ -39,16 +39,16 @@ UIEditorWorkspaceModel BuildWorkspace() { BuildUIEditorWorkspaceSplit( "workspace-top", UIEditorWorkspaceSplitAxis::Horizontal, - 0.15f, - BuildUIEditorWorkspaceSingleTabStack( - "hierarchy-panel", - "hierarchy", - "Hierarchy", - true), + 0.7875f, BuildUIEditorWorkspaceSplit( "workspace-main", UIEditorWorkspaceSplitAxis::Horizontal, - 0.75f, + 0.19047619f, + BuildUIEditorWorkspaceSingleTabStack( + "hierarchy-panel", + "hierarchy", + "Hierarchy", + true), BuildUIEditorWorkspaceTabStack( "center-tabs", { @@ -63,12 +63,12 @@ UIEditorWorkspaceModel BuildWorkspace() { "Game", false) }, - 0u), - BuildUIEditorWorkspaceSingleTabStack( - "inspector-panel", - "inspector", - "Inspector", - true))), + 0u)), + BuildUIEditorWorkspaceSingleTabStack( + "inspector-panel", + "inspector", + "Inspector", + true)), BuildUIEditorWorkspaceTabStack( "bottom-tabs", { diff --git a/new_editor/app/Workspace/ProductEditorWorkspace.cpp b/new_editor/app/Workspace/ProductEditorWorkspace.cpp index e770e67d..55f3a1a7 100644 --- a/new_editor/app/Workspace/ProductEditorWorkspace.cpp +++ b/new_editor/app/Workspace/ProductEditorWorkspace.cpp @@ -15,6 +15,7 @@ using ::XCEngine::UI::UIColor; using ::XCEngine::UI::UIDrawList; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; +using Widgets::UIEditorDockHostHitTargetKind; bool IsProductViewportPanel(std::string_view panelId) { return panelId == "scene" || panelId == "game"; @@ -154,6 +155,66 @@ std::vector FilterShellInputEventsForHostedContentCapture( return filteredEvents; } +bool IsPointerInputEventType(UIInputEventType type) { + switch (type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerLeave: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + case UIInputEventType::PointerWheel: + return true; + default: + return false; + } +} + +bool ShouldHostedContentYieldPointerStream( + const UIEditorShellInteractionFrame& shellFrame, + bool shellInteractiveCaptureActive) { + if (shellInteractiveCaptureActive || + shellFrame.result.requestPointerCapture || + shellFrame.result.releasePointerCapture) { + return true; + } + + return shellFrame.result.workspaceResult.dockHostResult.hitTarget.kind == + UIEditorDockHostHitTargetKind::SplitterHandle; +} + +std::vector FilterHostedContentInputEventsForShellOwnership( + const std::vector& inputEvents, + bool shellOwnsPointerStream) { + if (!shellOwnsPointerStream) { + return inputEvents; + } + + std::vector filteredEvents = {}; + filteredEvents.reserve(inputEvents.size() + 1u); + + bool strippedPointerInput = false; + UIInputEvent lastPointerEvent = {}; + for (const UIInputEvent& event : inputEvents) { + if (IsPointerInputEventType(event.type)) { + strippedPointerInput = true; + lastPointerEvent = event; + continue; + } + + filteredEvents.push_back(event); + } + + if (strippedPointerInput) { + UIInputEvent leaveEvent = {}; + leaveEvent.type = UIInputEventType::PointerLeave; + leaveEvent.position = lastPointerEvent.position; + leaveEvent.modifiers = lastPointerEvent.modifiers; + filteredEvents.push_back(leaveEvent); + } + + return filteredEvents; +} + } // namespace void ProductEditorWorkspace::Initialize( @@ -198,7 +259,6 @@ void ProductEditorWorkspace::Update( context.BuildShellDefinition(captureText); m_viewportHostService.BeginFrame(); definition.workspacePresentations = BuildWorkspacePresentations(definition); - const std::vector hostedContentEvents = inputEvents; const std::vector shellEvents = HasHostedContentCapture() ? FilterShellInputEventsForHostedContentCapture(inputEvents) @@ -212,6 +272,12 @@ void ProductEditorWorkspace::Update( shellEvents, context.GetShellServices(), metrics); + const bool shellOwnsHostedContentPointerStream = + ShouldHostedContentYieldPointerStream(m_shellFrame, HasShellInteractiveCapture()); + const std::vector hostedContentEvents = + FilterHostedContentInputEventsForShellOwnership( + inputEvents, + shellOwnsHostedContentPointerStream); ApplyViewportFramesToShellFrame(m_shellFrame, m_viewportHostService); context.SyncSessionFromWorkspace(); context.UpdateStatusFromShellResult(m_shellFrame.result); diff --git a/tests/UI/Core/unit/test_ui_splitter_layout.cpp b/tests/UI/Core/unit/test_ui_splitter_layout.cpp index 343dcce3..5f33acb4 100644 --- a/tests/UI/Core/unit/test_ui_splitter_layout.cpp +++ b/tests/UI/Core/unit/test_ui_splitter_layout.cpp @@ -74,3 +74,30 @@ TEST(UISplitterLayoutTest, HorizontalArrangementSplitsAvailableExtentAroundHandl ExpectRect(result.handleRect, 206.0f, 20.0f, 8.0f, 120.0f); ExpectRect(result.secondaryRect, 214.0f, 20.0f, 196.0f, 120.0f); } + +TEST(UISplitterLayoutTest, HorizontalArrangementCompressesMinimumsContinuouslyWhenSpaceUnderflows) { + UISplitterConstraints constraints = {}; + constraints.primaryMin = 200.0f; + constraints.secondaryMin = 120.0f; + + const UISplitterMetrics metrics{ 10.0f, 18.0f }; + const auto thresholdResult = ArrangeUISplitter( + UIRect(0.0f, 0.0f, 330.0f, 180.0f), + UILayoutAxis::Horizontal, + 0.1f, + constraints, + metrics); + const auto underflowResult = ArrangeUISplitter( + UIRect(0.0f, 0.0f, 329.0f, 180.0f), + UILayoutAxis::Horizontal, + 0.1f, + constraints, + metrics); + + EXPECT_FLOAT_EQ(thresholdResult.primaryExtent, 200.0f); + EXPECT_FLOAT_EQ(thresholdResult.secondaryExtent, 120.0f); + EXPECT_NEAR(underflowResult.primaryExtent, 319.0f * (200.0f / 320.0f), 0.0001f); + EXPECT_NEAR(underflowResult.secondaryExtent, 319.0f * (120.0f / 320.0f), 0.0001f); + EXPECT_LT(underflowResult.primaryExtent, thresholdResult.primaryExtent); + EXPECT_GT(underflowResult.primaryExtent, 190.0f); +}