diff --git a/new_editor/app/Application.cpp b/new_editor/app/Application.cpp index 94519a65..cabdcfee 100644 --- a/new_editor/app/Application.cpp +++ b/new_editor/app/Application.cpp @@ -44,6 +44,8 @@ constexpr UINT kDefaultDpi = 96u; constexpr float kBaseDpiScale = 96.0f; constexpr float kBorderlessTitleBarHeightDips = 28.0f; constexpr float kBorderlessTitleBarFontSize = 12.0f; +constexpr LONG kDetachedWindowDragOffsetXPixels = 40; +constexpr LONG kDetachedWindowDragOffsetYPixels = 12; const UIColor kShellSurfaceColor(0.10f, 0.10f, 0.10f, 1.0f); const UIColor kShellBorderColor(0.15f, 0.15f, 0.15f, 1.0f); const UIColor kShellTextColor(0.92f, 0.92f, 0.92f, 1.0f); @@ -451,6 +453,18 @@ bool IsPointInsideRect( point.y <= rect.y + rect.height; } +const Widgets::UIEditorDockHostTabStackLayout* FindDockHostTabStackLayoutByNodeId( + const Widgets::UIEditorDockHostLayout& layout, + std::string_view nodeId) { + for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { + if (tabStack.nodeId == nodeId) { + return &tabStack; + } + } + + return nullptr; +} + std::size_t ResolveCrossWindowDropInsertionIndex( const Widgets::UIEditorDockHostTabStackLayout& tabStack, const UIPoint& point) { @@ -1010,10 +1024,10 @@ std::wstring Application::BuildManagedWindowTitle( RECT Application::BuildDetachedWindowRect(const POINT& screenPoint) const { RECT rect = { - screenPoint.x - 420, - screenPoint.y - 24, - screenPoint.x - 420 + 960, - screenPoint.y - 24 + 720 + screenPoint.x - kDetachedWindowDragOffsetXPixels, + screenPoint.y - kDetachedWindowDragOffsetYPixels, + screenPoint.x - kDetachedWindowDragOffsetXPixels + 960, + screenPoint.y - kDetachedWindowDragOffsetYPixels + 720 }; const HMONITOR monitor = MonitorFromPoint(screenPoint, MONITOR_DEFAULTTONEAREST); @@ -1056,6 +1070,7 @@ void Application::ResetManagedWindowInteractionState(ManagedWindowState& windowS windowState.pendingGlobalTabDragNodeId.clear(); windowState.pendingGlobalTabDragPanelId.clear(); windowState.pendingGlobalTabDragScreenPoint = {}; + windowState.pendingGlobalTabDragWindowOffset = {}; } bool Application::SynchronizeManagedWindowsFromWindowSet( @@ -1122,12 +1137,14 @@ void Application::BeginGlobalTabDragSession( std::string_view panelWindowId, std::string_view sourceNodeId, std::string_view panelId, - const POINT& screenPoint) { + const POINT& screenPoint, + const POINT& windowDragOffset) { m_globalTabDragSession.active = true; m_globalTabDragSession.panelWindowId = std::string(panelWindowId); m_globalTabDragSession.sourceNodeId = std::string(sourceNodeId); m_globalTabDragSession.panelId = std::string(panelId); m_globalTabDragSession.screenPoint = screenPoint; + m_globalTabDragSession.windowDragOffset = windowDragOffset; } void Application::EndGlobalTabDragSession() { @@ -1187,6 +1204,112 @@ const Application::ManagedWindowState* Application::FindTopmostWindowStateAtScre excludedWindowId); } +POINT Application::ConvertClientDipsToScreenPixels( + const ManagedWindowState& windowState, + const UIPoint& point) const { + const float dpiScale = windowState.hostRuntime.GetDpiScale(kBaseDpiScale); + POINT clientPoint = { + static_cast(point.x * dpiScale), + static_cast(point.y * dpiScale) + }; + if (windowState.hwnd != nullptr) { + ClientToScreen(windowState.hwnd, &clientPoint); + } + return clientPoint; +} + +bool Application::TryResolveDraggedTabScreenRect( + const ManagedWindowState& windowState, + std::string_view nodeId, + std::string_view panelId, + RECT& outRect) const { + outRect = {}; + const Widgets::UIEditorDockHostLayout& layout = + windowState.editorWorkspace + .GetShellFrame() + .workspaceInteractionFrame + .dockHostFrame + .layout; + const Widgets::UIEditorDockHostTabStackLayout* tabStack = + FindDockHostTabStackLayoutByNodeId(layout, nodeId); + if (tabStack == nullptr) { + return false; + } + + std::size_t tabIndex = Widgets::UIEditorTabStripInvalidIndex; + for (std::size_t index = 0; index < tabStack->items.size(); ++index) { + if (tabStack->items[index].panelId == panelId) { + tabIndex = index; + break; + } + } + + if (tabIndex == Widgets::UIEditorTabStripInvalidIndex || + tabIndex >= tabStack->tabStripLayout.tabHeaderRects.size()) { + return false; + } + + const UIRect& tabRect = tabStack->tabStripLayout.tabHeaderRects[tabIndex]; + const POINT topLeft = ConvertClientDipsToScreenPixels( + windowState, + UIPoint(tabRect.x, tabRect.y)); + const POINT bottomRight = ConvertClientDipsToScreenPixels( + windowState, + UIPoint(tabRect.x + tabRect.width, tabRect.y + tabRect.height)); + outRect.left = topLeft.x; + outRect.top = topLeft.y; + outRect.right = bottomRight.x; + outRect.bottom = bottomRight.y; + return outRect.right > outRect.left && outRect.bottom > outRect.top; +} + +POINT Application::ResolveGlobalTabDragWindowOffset( + const ManagedWindowState& windowState, + std::string_view nodeId, + std::string_view panelId, + const POINT& screenPoint) const { + RECT tabScreenRect = {}; + if (TryResolveDraggedTabScreenRect(windowState, nodeId, panelId, tabScreenRect)) { + const LONG offsetX = + (std::clamp)(screenPoint.x - tabScreenRect.left, 0L, tabScreenRect.right - tabScreenRect.left); + const LONG offsetY = + (std::clamp)(screenPoint.y - tabScreenRect.top, 0L, tabScreenRect.bottom - tabScreenRect.top); + return POINT { offsetX, offsetY }; + } + + const float dpiScale = windowState.hostRuntime.GetDpiScale(kBaseDpiScale); + return POINT { + static_cast(static_cast(kDetachedWindowDragOffsetXPixels) * dpiScale), + static_cast(static_cast(kDetachedWindowDragOffsetYPixels) * dpiScale) + }; +} + +void Application::MoveGlobalTabDragWindow( + ManagedWindowState& windowState, + const POINT& screenPoint) const { + if (windowState.hwnd == nullptr) { + return; + } + + RECT currentRect = {}; + if (!GetWindowRect(windowState.hwnd, ¤tRect)) { + return; + } + + const LONG width = currentRect.right - currentRect.left; + const LONG height = currentRect.bottom - currentRect.top; + const LONG left = screenPoint.x - m_globalTabDragSession.windowDragOffset.x; + const LONG top = screenPoint.y - m_globalTabDragSession.windowDragOffset.y; + SetWindowPos( + windowState.hwnd, + nullptr, + left, + top, + width, + height, + SWP_NOZORDER | SWP_NOACTIVATE); +} + bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) { if (!sourceWindowState.pendingGlobalTabDragStart || sourceWindowState.pendingGlobalTabDragNodeId.empty() || @@ -1197,10 +1320,12 @@ bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) { const std::string sourceNodeId = sourceWindowState.pendingGlobalTabDragNodeId; const std::string panelId = sourceWindowState.pendingGlobalTabDragPanelId; const POINT screenPoint = sourceWindowState.pendingGlobalTabDragScreenPoint; + const POINT windowDragOffset = sourceWindowState.pendingGlobalTabDragWindowOffset; sourceWindowState.pendingGlobalTabDragStart = false; sourceWindowState.pendingGlobalTabDragNodeId.clear(); sourceWindowState.pendingGlobalTabDragPanelId.clear(); sourceWindowState.pendingGlobalTabDragScreenPoint = {}; + sourceWindowState.pendingGlobalTabDragWindowOffset = {}; if (sourceWindowState.primary) { UIEditorWindowWorkspaceController windowWorkspaceController( @@ -1236,7 +1361,9 @@ bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) { detachedWindowState->windowId, detachedWindowState->workspaceController.GetWorkspace().root.nodeId, panelId, - screenPoint); + screenPoint, + windowDragOffset); + MoveGlobalTabDragWindow(*detachedWindowState, screenPoint); SetCapture(detachedWindowState->hwnd); SetForegroundWindow(detachedWindowState->hwnd); LogRuntimeTrace( @@ -1251,7 +1378,9 @@ bool Application::TryStartGlobalTabDrag(ManagedWindowState& sourceWindowState) { sourceWindowState.windowId, sourceNodeId, panelId, - screenPoint); + screenPoint, + windowDragOffset); + MoveGlobalTabDragWindow(sourceWindowState, screenPoint); if (sourceWindowState.hwnd != nullptr) { SetCapture(sourceWindowState.hwnd); } @@ -1352,7 +1481,7 @@ bool Application::HandleGlobalTabDragPointerMove(HWND hwnd) { return false; } - const ManagedWindowState* ownerWindowState = FindWindowState(m_globalTabDragSession.panelWindowId); + ManagedWindowState* ownerWindowState = FindWindowState(m_globalTabDragSession.panelWindowId); if (ownerWindowState == nullptr || ownerWindowState->hwnd != hwnd) { return false; } @@ -1360,6 +1489,7 @@ bool Application::HandleGlobalTabDragPointerMove(HWND hwnd) { POINT screenPoint = {}; if (GetCursorPos(&screenPoint)) { m_globalTabDragSession.screenPoint = screenPoint; + MoveGlobalTabDragWindow(*ownerWindowState, screenPoint); } return true; } @@ -1443,6 +1573,59 @@ bool Application::HandleGlobalTabDragPointerButtonUp(HWND hwnd) { return true; } +void Application::AppendGlobalTabDragDropPreview(UIDrawList& drawList) const { + if (!m_globalTabDragSession.active || m_currentWindowState == nullptr) { + return; + } + + if (m_currentWindowState->windowId == m_globalTabDragSession.panelWindowId) { + return; + } + + const ManagedWindowState* targetWindowState = FindTopmostWindowStateAtScreenPoint( + m_globalTabDragSession.screenPoint, + m_globalTabDragSession.panelWindowId); + if (targetWindowState == nullptr || targetWindowState != m_currentWindowState) { + return; + } + + const UIPoint targetPoint = + ConvertScreenPixelsToClientDips(*targetWindowState, m_globalTabDragSession.screenPoint); + const Widgets::UIEditorDockHostLayout& targetLayout = + targetWindowState->editorWorkspace + .GetShellFrame() + .workspaceInteractionFrame + .dockHostFrame + .layout; + CrossWindowDockDropTarget dropTarget = {}; + if (!TryResolveCrossWindowDockDropTarget(targetLayout, targetPoint, dropTarget)) { + return; + } + + Widgets::UIEditorDockHostDropPreviewState previewState = {}; + previewState.visible = true; + previewState.sourceNodeId = m_globalTabDragSession.sourceNodeId; + previewState.sourcePanelId = m_globalTabDragSession.panelId; + previewState.targetNodeId = dropTarget.nodeId; + previewState.placement = dropTarget.placement; + previewState.insertionIndex = dropTarget.insertionIndex; + const Widgets::UIEditorDockHostDropPreviewLayout previewLayout = + Widgets::ResolveUIEditorDockHostDropPreviewLayout(targetLayout, previewState); + if (!previewLayout.visible) { + return; + } + + const Widgets::UIEditorDockHostPalette& dockPalette = + ResolveUIEditorShellInteractionPalette().shellPalette.dockHostPalette; + drawList.AddFilledRect( + previewLayout.previewRect, + dockPalette.dropPreviewFillColor); + drawList.AddRectOutline( + previewLayout.previewRect, + dockPalette.dropPreviewBorderColor, + 1.0f); +} + void Application::HandleDestroyedWindow(HWND hwnd) { if (ManagedWindowState* windowState = FindWindowState(hwnd); windowState != nullptr) { windowState->hwnd = nullptr; @@ -1570,6 +1753,12 @@ void Application::RenderFrame() { RequireCurrentWindowState().pendingGlobalTabDragPanelId = dockHostInteractionState.activeTabDragPanelId; RequireCurrentWindowState().pendingGlobalTabDragScreenPoint = screenPoint; + RequireCurrentWindowState().pendingGlobalTabDragWindowOffset = + ResolveGlobalTabDragWindowOffset( + RequireCurrentWindowState(), + dockHostInteractionState.activeTabDragNodeId, + dockHostInteractionState.activeTabDragPanelId, + screenPoint); } if (shellFrame.result.workspaceResult.dockHostResult.detachRequested) { POINT screenPoint = {}; @@ -1588,6 +1777,7 @@ void Application::RenderFrame() { ApplyHostedContentCaptureRequests(); ApplyCurrentCursor(); m_editorWorkspace.Append(drawList); + AppendGlobalTabDragDropPreview(drawList); if (frameContext.canRenderViewports) { m_editorWorkspace.RenderRequestedViewports(frameContext.renderContext); } @@ -1636,6 +1826,10 @@ bool Application::IsBorderlessWindowEnabled() const { return true; } +bool Application::HasBorderlessWindowChrome() const { + return m_currentWindowState != nullptr && m_currentWindowState->primary; +} + bool Application::IsBorderlessWindowMaximized() const { return m_hostRuntime.IsBorderlessWindowMaximized(); } @@ -1888,7 +2082,7 @@ Host::BorderlessWindowChromeLayout Application::ResolveBorderlessWindowChromeLay } Host::BorderlessWindowChromeHitTarget Application::HitTestBorderlessWindowChrome(LPARAM lParam) const { - if (!IsBorderlessWindowEnabled() || m_hwnd == nullptr) { + if (!HasBorderlessWindowChrome() || m_hwnd == nullptr) { return Host::BorderlessWindowChromeHitTarget::None; } @@ -1931,6 +2125,10 @@ bool Application::UpdateBorderlessWindowChromeHover(LPARAM lParam) { } bool Application::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) { + if (!HasBorderlessWindowChrome()) { + return false; + } + if (m_hostRuntime.GetHoveredBorderlessResizeEdge() != Host::BorderlessWindowResizeEdge::None || m_hostRuntime.IsBorderlessResizeActive()) { return false; @@ -1969,6 +2167,10 @@ bool Application::HandleBorderlessWindowChromeButtonDown(LPARAM lParam) { } bool Application::HandleBorderlessWindowChromeButtonUp(LPARAM lParam) { + if (!HasBorderlessWindowChrome()) { + return false; + } + if (m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) { ClearBorderlessWindowChromeDragRestoreState(); return true; @@ -1997,6 +2199,10 @@ bool Application::HandleBorderlessWindowChromeButtonUp(LPARAM lParam) { } bool Application::HandleBorderlessWindowChromeDoubleClick(LPARAM lParam) { + if (!HasBorderlessWindowChrome()) { + return false; + } + if (m_hostRuntime.IsBorderlessWindowDragRestoreArmed()) { ClearBorderlessWindowChromeDragRestoreState(); } @@ -2010,6 +2216,10 @@ bool Application::HandleBorderlessWindowChromeDoubleClick(LPARAM lParam) { } bool Application::HandleBorderlessWindowChromeDragRestorePointerMove() { + if (!HasBorderlessWindowChrome()) { + return false; + } + if (!m_hostRuntime.IsBorderlessWindowDragRestoreArmed() || m_hwnd == nullptr) { return false; } @@ -2101,7 +2311,7 @@ void Application::ClearBorderlessWindowChromeState() { void Application::AppendBorderlessWindowChrome( ::XCEngine::UI::UIDrawList& drawList, float clientWidthDips) const { - if (!IsBorderlessWindowEnabled()) { + if (!HasBorderlessWindowChrome()) { return; } @@ -2412,7 +2622,7 @@ bool Application::ResolveRenderClientPixelSize(UINT& outWidth, UINT& outHeight) } UIRect Application::ResolveWorkspaceBounds(float clientWidthDips, float clientHeightDips) const { - if (!IsBorderlessWindowEnabled()) { + if (!HasBorderlessWindowChrome()) { return UIRect(0.0f, 0.0f, clientWidthDips, clientHeightDips); } diff --git a/new_editor/app/Application.h b/new_editor/app/Application.h index 7aeac2f3..dfb7fa25 100644 --- a/new_editor/app/Application.h +++ b/new_editor/app/Application.h @@ -75,6 +75,7 @@ private: std::string pendingGlobalTabDragNodeId = {}; std::string pendingGlobalTabDragPanelId = {}; POINT pendingGlobalTabDragScreenPoint = {}; + POINT pendingGlobalTabDragWindowOffset = {}; }; struct GlobalTabDragSession { bool active = false; @@ -82,6 +83,7 @@ private: std::string sourceNodeId = {}; std::string panelId = {}; POINT screenPoint = {}; + POINT windowDragOffset = {}; }; struct ManagedWindowCreateParams { std::string windowId = {}; @@ -127,7 +129,8 @@ private: std::string_view panelWindowId, std::string_view sourceNodeId, std::string_view panelId, - const POINT& screenPoint); + const POINT& screenPoint, + const POINT& windowDragOffset); void EndGlobalTabDragSession(); bool HandleGlobalTabDragPointerMove(HWND hwnd); bool HandleGlobalTabDragPointerButtonUp(HWND hwnd); @@ -163,10 +166,26 @@ private: LPCWSTR ResolveCurrentCursorResource() const; float GetDpiScale() const; float PixelsToDips(float pixels) const; + bool HasBorderlessWindowChrome() const; ::XCEngine::UI::UIPoint ConvertClientPixelsToDips(LONG x, LONG y) const; ::XCEngine::UI::UIPoint ConvertScreenPixelsToClientDips( const ManagedWindowState& windowState, const POINT& screenPoint) const; + POINT ConvertClientDipsToScreenPixels( + const ManagedWindowState& windowState, + const ::XCEngine::UI::UIPoint& point) const; + bool TryResolveDraggedTabScreenRect( + const ManagedWindowState& windowState, + std::string_view nodeId, + std::string_view panelId, + RECT& outRect) const; + POINT ResolveGlobalTabDragWindowOffset( + const ManagedWindowState& windowState, + std::string_view nodeId, + std::string_view panelId, + const POINT& screenPoint) const; + void MoveGlobalTabDragWindow(ManagedWindowState& windowState, const POINT& screenPoint) const; + void AppendGlobalTabDragDropPreview(::XCEngine::UI::UIDrawList& drawList) const; std::string BuildCaptureStatusText() const; void LogRuntimeTrace(std::string_view channel, std::string_view message) const; void ApplyHostCaptureRequests(const UIEditorShellInteractionResult& result); diff --git a/new_editor/include/XCEditor/Shell/UIEditorDockHost.h b/new_editor/include/XCEditor/Shell/UIEditorDockHost.h index e3ce5cd6..7902e506 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorDockHost.h +++ b/new_editor/include/XCEditor/Shell/UIEditorDockHost.h @@ -75,21 +75,21 @@ struct UIEditorDockHostPalette { UIEditorTabStripPalette tabStripPalette = {}; UIEditorPanelFramePalette panelFramePalette = {}; ::XCEngine::UI::UIColor splitterColor = - ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f); + ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); ::XCEngine::UI::UIColor splitterHoveredColor = - ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f); + ::XCEngine::UI::UIColor(0.16f, 0.16f, 0.16f, 1.0f); ::XCEngine::UI::UIColor splitterActiveColor = - ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f); + ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f); ::XCEngine::UI::UIColor placeholderTitleColor = - ::XCEngine::UI::UIColor(0.93f, 0.94f, 0.96f, 1.0f); + ::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f); ::XCEngine::UI::UIColor placeholderTextColor = - ::XCEngine::UI::UIColor(0.70f, 0.72f, 0.74f, 1.0f); + ::XCEngine::UI::UIColor(0.72f, 0.72f, 0.72f, 1.0f); ::XCEngine::UI::UIColor placeholderMutedColor = - ::XCEngine::UI::UIColor(0.58f, 0.59f, 0.62f, 1.0f); + ::XCEngine::UI::UIColor(0.62f, 0.62f, 0.62f, 1.0f); ::XCEngine::UI::UIColor dropPreviewFillColor = - ::XCEngine::UI::UIColor(0.88f, 0.88f, 0.88f, 0.14f); + ::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 0.06f); ::XCEngine::UI::UIColor dropPreviewBorderColor = - ::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 0.78f); + ::XCEngine::UI::UIColor(0.95f, 0.95f, 0.95f, 0.55f); }; struct UIEditorDockHostTabItemLayout { @@ -165,6 +165,10 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout( const UIEditorDockHostState& state = {}, const UIEditorDockHostMetrics& metrics = {}); +UIEditorDockHostDropPreviewLayout ResolveUIEditorDockHostDropPreviewLayout( + const UIEditorDockHostLayout& layout, + const UIEditorDockHostDropPreviewState& state); + UIEditorDockHostHitTarget HitTestUIEditorDockHost( const UIEditorDockHostLayout& layout, const ::XCEngine::UI::UIPoint& point); diff --git a/new_editor/src/Shell/UIEditorDockHost.cpp b/new_editor/src/Shell/UIEditorDockHost.cpp index f20a3897..4e84458e 100644 --- a/new_editor/src/Shell/UIEditorDockHost.cpp +++ b/new_editor/src/Shell/UIEditorDockHost.cpp @@ -653,6 +653,14 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout( return layout; } +UIEditorDockHostDropPreviewLayout ResolveUIEditorDockHostDropPreviewLayout( + const UIEditorDockHostLayout& layout, + const UIEditorDockHostDropPreviewState& state) { + UIEditorDockHostState dockHostState = {}; + dockHostState.dropPreview = state; + return ResolveDropPreviewLayout(layout, dockHostState); +} + UIEditorDockHostHitTarget HitTestUIEditorDockHost( const UIEditorDockHostLayout& layout, const UIPoint& point) {