#include "Platform/Win32/WindowManager/Internal.h" #include "State/EditorContext.h" #include "Platform/Win32/EditorWindow.h" #include #include #include #include #include #include namespace XCEngine::UI::Editor::App::Internal { namespace { using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; constexpr LONG kFallbackDragHotspotX = 40; constexpr LONG kFallbackDragHotspotY = 12; struct CrossWindowDockDropTarget { bool valid = false; std::string nodeId = {}; UIEditorWorkspaceDockPlacement placement = UIEditorWorkspaceDockPlacement::Center; std::size_t insertionIndex = Widgets::UIEditorTabStripInvalidIndex; }; POINT BuildFallbackGlobalTabDragHotspot() { POINT hotspot = {}; hotspot.x = kFallbackDragHotspotX; hotspot.y = kFallbackDragHotspotY; return hotspot; } float ResolveWindowDpiScale(HWND hwnd) { if (hwnd == nullptr) { return 1.0f; } const UINT dpi = GetDpiForWindow(hwnd); return dpi == 0u ? 1.0f : static_cast(dpi) / 96.0f; } bool IsPointInsideRect(const UIRect& rect, const UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height; } bool CanStartGlobalTabDragFromWindow( const EditorWindow& sourceWindow, std::string_view sourceNodeId, std::string_view panelId) { UIEditorWorkspaceModel workspace = sourceWindow.GetWorkspaceController().GetWorkspace(); UIEditorWorkspaceSession session = sourceWindow.GetWorkspaceController().GetSession(); UIEditorWorkspaceExtractedPanel extractedPanel = {}; return TryExtractUIEditorWorkspaceVisiblePanel( workspace, session, sourceNodeId, panelId, extractedPanel); } bool IsLiveInteractiveWindow(const EditorWindow* window) { return window != nullptr && window->GetHwnd() != nullptr && !window->IsClosing(); } std::size_t ResolveDropInsertionIndex( const Widgets::UIEditorDockHostTabStackLayout& tabStack, const UIPoint& point) { if (!IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) { return Widgets::UIEditorTabStripInvalidIndex; } std::size_t insertionIndex = 0u; for (const UIRect& rect : tabStack.tabStripLayout.tabHeaderRects) { const float midpoint = rect.x + rect.width * 0.5f; if (point.x > midpoint) { ++insertionIndex; } } return insertionIndex; } UIEditorWorkspaceDockPlacement ResolveDropPlacement( const Widgets::UIEditorDockHostTabStackLayout& tabStack, const UIPoint& point) { if (IsPointInsideRect(tabStack.tabStripLayout.headerRect, point)) { return UIEditorWorkspaceDockPlacement::Center; } const float leftDistance = point.x - tabStack.bounds.x; const float rightDistance = tabStack.bounds.x + tabStack.bounds.width - point.x; const float topDistance = point.y - tabStack.bounds.y; const float bottomDistance = tabStack.bounds.y + tabStack.bounds.height - point.y; const float minHorizontalThreshold = tabStack.bounds.width * 0.25f; const float minVerticalThreshold = tabStack.bounds.height * 0.25f; const float nearestEdge = (std::min)( (std::min)(leftDistance, rightDistance), (std::min)(topDistance, bottomDistance)); if (nearestEdge == leftDistance && leftDistance <= minHorizontalThreshold) { return UIEditorWorkspaceDockPlacement::Left; } if (nearestEdge == rightDistance && rightDistance <= minHorizontalThreshold) { return UIEditorWorkspaceDockPlacement::Right; } if (nearestEdge == topDistance && topDistance <= minVerticalThreshold) { return UIEditorWorkspaceDockPlacement::Top; } if (nearestEdge == bottomDistance && bottomDistance <= minVerticalThreshold) { return UIEditorWorkspaceDockPlacement::Bottom; } return UIEditorWorkspaceDockPlacement::Center; } bool TryResolveCrossWindowDockDropTarget( const Widgets::UIEditorDockHostLayout& layout, const UIPoint& point, CrossWindowDockDropTarget& outTarget) { outTarget = {}; if (!IsPointInsideRect(layout.bounds, point)) { return false; } const Widgets::UIEditorDockHostHitTarget hitTarget = Widgets::HitTestUIEditorDockHost(layout, point); for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { if ((!hitTarget.nodeId.empty() && tabStack.nodeId != hitTarget.nodeId) || !IsPointInsideRect(tabStack.bounds, point)) { continue; } outTarget.valid = true; outTarget.nodeId = tabStack.nodeId; outTarget.placement = ResolveDropPlacement(tabStack, point); if (outTarget.placement == UIEditorWorkspaceDockPlacement::Center) { outTarget.insertionIndex = ResolveDropInsertionIndex(tabStack, point); if (outTarget.insertionIndex == Widgets::UIEditorTabStripInvalidIndex) { outTarget.insertionIndex = tabStack.items.size(); } } return true; } for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { if (IsPointInsideRect(tabStack.bounds, point)) { outTarget.valid = true; outTarget.nodeId = tabStack.nodeId; outTarget.placement = ResolveDropPlacement(tabStack, point); if (outTarget.placement == UIEditorWorkspaceDockPlacement::Center) { outTarget.insertionIndex = tabStack.items.size(); } return true; } } return false; } } // namespace bool EditorWindowWorkspaceCoordinator::IsGlobalTabDragActive() const { return m_globalTabDragSession.active; } bool EditorWindowWorkspaceCoordinator::OwnsActiveGlobalTabDrag(std::string_view windowId) const { return m_globalTabDragSession.active && m_globalTabDragSession.panelWindowId == windowId; } void EditorWindowWorkspaceCoordinator::BeginGlobalTabDragSession( std::string_view panelWindowId, std::string_view sourceNodeId, std::string_view panelId, const POINT& screenPoint, const POINT& dragHotspot) { 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.dragHotspot = dragHotspot; } bool EditorWindowWorkspaceCoordinator::TryResolveGlobalTabDragHotspot( const EditorWindow& sourceWindow, std::string_view nodeId, std::string_view panelId, const POINT& screenPoint, POINT& outDragHotspot) const { const HWND hwnd = sourceWindow.GetHwnd(); if (hwnd == nullptr) { return false; } const auto& layout = sourceWindow.GetShellFrame().workspaceInteractionFrame.dockHostFrame.layout; for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { if (tabStack.nodeId != nodeId) { continue; } const std::size_t tabCount = (std::min)(tabStack.items.size(), tabStack.tabStripLayout.tabHeaderRects.size()); for (std::size_t index = 0u; index < tabCount; ++index) { if (tabStack.items[index].panelId != panelId) { continue; } const ::XCEngine::UI::UIPoint clientPointDips = sourceWindow.ConvertScreenPixelsToClientDips(screenPoint); const ::XCEngine::UI::UIRect& tabRect = tabStack.tabStripLayout.tabHeaderRects[index]; const float dpiScale = ResolveWindowDpiScale(hwnd); const float localOffsetXDips = (std::clamp)( clientPointDips.x - tabRect.x, 0.0f, (std::max)(tabRect.width, 0.0f)); const float localOffsetYDips = (std::clamp)( clientPointDips.y - tabRect.y, 0.0f, (std::max)(tabRect.height, 0.0f)); outDragHotspot.x = static_cast(std::lround(localOffsetXDips * dpiScale)); outDragHotspot.y = static_cast(std::lround(localOffsetYDips * dpiScale)); return true; } } return false; } void EditorWindowWorkspaceCoordinator::UpdateGlobalTabDragOwnerWindowPosition() { if (!m_globalTabDragSession.active) { return; } EditorWindow* ownerWindow = m_hostRuntime.FindWindow(m_globalTabDragSession.panelWindowId); if (!IsLiveInteractiveWindow(ownerWindow)) { return; } RECT windowRect = {}; if (!GetWindowRect(ownerWindow->GetHwnd(), &windowRect)) { return; } const LONG targetLeft = m_globalTabDragSession.screenPoint.x - m_globalTabDragSession.dragHotspot.x; const LONG targetTop = m_globalTabDragSession.screenPoint.y - m_globalTabDragSession.dragHotspot.y; if (windowRect.left == targetLeft && windowRect.top == targetTop) { return; } SetWindowPos( ownerWindow->GetHwnd(), nullptr, targetLeft, targetTop, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } void EditorWindowWorkspaceCoordinator::ClearGlobalTabDragDropPreview() { if (m_globalTabDragSession.previewWindowId.empty()) { return; } if (EditorWindow* previewWindow = m_hostRuntime.FindWindow(m_globalTabDragSession.previewWindowId); IsLiveInteractiveWindow(previewWindow)) { previewWindow->ClearExternalDockHostDropPreview(); previewWindow->InvalidateHostWindow(); } m_globalTabDragSession.previewWindowId.clear(); } void EditorWindowWorkspaceCoordinator::UpdateGlobalTabDragDropPreview() { if (!m_globalTabDragSession.active) { return; } EditorWindow* targetWindow = FindTopmostWindowAtScreenPoint( m_globalTabDragSession.screenPoint, m_globalTabDragSession.panelWindowId); if (!IsLiveInteractiveWindow(targetWindow)) { ClearGlobalTabDragDropPreview(); return; } const ::XCEngine::UI::UIPoint targetPoint = targetWindow->ConvertScreenPixelsToClientDips(m_globalTabDragSession.screenPoint); const Widgets::UIEditorDockHostLayout& targetLayout = targetWindow->GetShellFrame().workspaceInteractionFrame.dockHostFrame.layout; CrossWindowDockDropTarget dropTarget = {}; if (!TryResolveCrossWindowDockDropTarget(targetLayout, targetPoint, dropTarget) || !dropTarget.valid) { ClearGlobalTabDragDropPreview(); return; } if (!m_globalTabDragSession.previewWindowId.empty() && m_globalTabDragSession.previewWindowId != targetWindow->GetWindowId()) { ClearGlobalTabDragDropPreview(); } Widgets::UIEditorDockHostDropPreviewState preview = {}; preview.visible = true; preview.sourceNodeId = m_globalTabDragSession.sourceNodeId; preview.sourcePanelId = m_globalTabDragSession.panelId; preview.targetNodeId = dropTarget.nodeId; preview.placement = dropTarget.placement; preview.insertionIndex = dropTarget.insertionIndex; targetWindow->SetExternalDockHostDropPreview(preview); targetWindow->InvalidateHostWindow(); m_globalTabDragSession.previewWindowId = std::string(targetWindow->GetWindowId()); } void EditorWindowWorkspaceCoordinator::EndGlobalTabDragSession() { if (!m_globalTabDragSession.active) { return; } ClearGlobalTabDragDropPreview(); if (EditorWindow* ownerWindow = m_hostRuntime.FindWindow(m_globalTabDragSession.panelWindowId); ownerWindow != nullptr) { ownerWindow->ReleasePointerCapture(EditorWindowPointerCaptureOwner::GlobalTabDrag); if (!ownerWindow->IsClosing()) { ownerWindow->ResetInteractionState(); } } m_globalTabDragSession = {}; } bool EditorWindowWorkspaceCoordinator::HandleGlobalTabDragPointerMove(HWND hwnd) { if (!m_globalTabDragSession.active) { return false; } const EditorWindow* ownerWindow = m_hostRuntime.FindWindow(m_globalTabDragSession.panelWindowId); if (!IsLiveInteractiveWindow(ownerWindow)) { EndGlobalTabDragSession(); return false; } if (ownerWindow->GetHwnd() != hwnd) { return false; } POINT screenPoint = {}; if (GetCursorPos(&screenPoint)) { m_globalTabDragSession.screenPoint = screenPoint; UpdateGlobalTabDragOwnerWindowPosition(); UpdateGlobalTabDragDropPreview(); } return true; } bool EditorWindowWorkspaceCoordinator::HandleGlobalTabDragPointerButtonUp(HWND hwnd) { if (!m_globalTabDragSession.active) { return false; } const EditorWindow* ownerWindow = m_hostRuntime.FindWindow(m_globalTabDragSession.panelWindowId); if (!IsLiveInteractiveWindow(ownerWindow)) { EndGlobalTabDragSession(); return false; } if (ownerWindow->GetHwnd() != hwnd) { return false; } POINT screenPoint = m_globalTabDragSession.screenPoint; GetCursorPos(&screenPoint); const std::string panelWindowId = m_globalTabDragSession.panelWindowId; const std::string sourceNodeId = m_globalTabDragSession.sourceNodeId; const std::string panelId = m_globalTabDragSession.panelId; EndGlobalTabDragSession(); EditorWindow* targetWindow = FindTopmostWindowAtScreenPoint(screenPoint, panelWindowId); if (!IsLiveInteractiveWindow(targetWindow)) { return true; } const UIPoint targetPoint = targetWindow->ConvertScreenPixelsToClientDips(screenPoint); const Widgets::UIEditorDockHostLayout& targetLayout = targetWindow->GetShellFrame().workspaceInteractionFrame.dockHostFrame.layout; CrossWindowDockDropTarget dropTarget = {}; if (!TryResolveCrossWindowDockDropTarget(targetLayout, targetPoint, dropTarget)) { return true; } UIEditorWindowWorkspaceController windowWorkspaceController = BuildWorkspaceMutationController(); const UIEditorWindowWorkspaceOperationResult result = dropTarget.placement == UIEditorWorkspaceDockPlacement::Center ? windowWorkspaceController.MovePanelToStack( panelWindowId, sourceNodeId, panelId, targetWindow->GetWindowId(), dropTarget.nodeId, dropTarget.insertionIndex) : windowWorkspaceController.DockPanelRelative( panelWindowId, sourceNodeId, panelId, targetWindow->GetWindowId(), dropTarget.nodeId, dropTarget.placement); if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { LogRuntimeTrace("drag", "cross-window drop rejected: " + result.message); return true; } if (!CommitWindowWorkspaceMutation( windowWorkspaceController, {}, screenPoint)) { LogRuntimeTrace("drag", "failed to synchronize windows after cross-window drop"); return true; } if (EditorWindow* updatedTargetWindow = m_hostRuntime.FindWindow(targetWindow->GetWindowId()); updatedTargetWindow != nullptr && updatedTargetWindow->GetHwnd() != nullptr && !updatedTargetWindow->IsClosing()) { SetForegroundWindow(updatedTargetWindow->GetHwnd()); } LogRuntimeTrace( "drag", "committed cross-window drop panel '" + panelId + "' into window '" + std::string(targetWindow->GetWindowId()) + "'"); return true; } bool EditorWindowWorkspaceCoordinator::TryStartGlobalTabDrag( EditorWindow& sourceWindow, const EditorWindowPanelTransferRequest& request) { if (sourceWindow.IsClosing()) { LogRuntimeTrace("drag", "failed to start global tab drag: source window is closing"); return false; } POINT dragHotspot = BuildFallbackGlobalTabDragHotspot(); TryResolveGlobalTabDragHotspot( sourceWindow, request.nodeId, request.panelId, request.screenPoint, dragHotspot); const auto tryStartDetachedPanelGlobalDrag = [this, &request, &dragHotspot]( UIEditorWindowWorkspaceController& windowWorkspaceController, const UIEditorWindowWorkspaceOperationResult& result) { if (!CommitWindowWorkspaceMutation( windowWorkspaceController, result.targetWindowId, request.screenPoint)) { LogRuntimeTrace("drag", "failed to synchronize detached drag window state"); return false; } EditorWindow* detachedWindow = m_hostRuntime.FindWindow(result.targetWindowId); if (!IsLiveInteractiveWindow(detachedWindow)) { LogRuntimeTrace("drag", "detached drag window was not created."); return false; } BeginGlobalTabDragSession( detachedWindow->GetWindowId(), detachedWindow->GetWorkspaceController().GetWorkspace().root.nodeId, request.panelId, request.screenPoint, dragHotspot); UpdateGlobalTabDragOwnerWindowPosition(); detachedWindow->AcquirePointerCapture( EditorWindowPointerCaptureOwner::GlobalTabDrag); SetForegroundWindow(detachedWindow->GetHwnd()); LogRuntimeTrace( "drag", "started global tab drag by detaching panel '" + request.panelId + "' into window '" + std::string(detachedWindow->GetWindowId()) + "'"); return true; }; UIEditorWindowWorkspaceController windowWorkspaceController = BuildWorkspaceMutationController(); const UIEditorWindowWorkspaceOperationResult result = windowWorkspaceController.DetachPanelToNewWindow( sourceWindow.GetWindowId(), request.nodeId, request.panelId); if (result.status == UIEditorWindowWorkspaceOperationStatus::Changed) { return tryStartDetachedPanelGlobalDrag(windowWorkspaceController, result); } if (sourceWindow.IsPrimary()) { LogRuntimeTrace( "drag", "failed to start global tab drag from primary window: " + result.message); return false; } sourceWindow.ResetInteractionState(); if (result.status != UIEditorWindowWorkspaceOperationStatus::NoOp) { LogRuntimeTrace( "drag", "failed to start global tab drag from detached window: " + result.message); return false; } if (!CanStartGlobalTabDragFromWindow(sourceWindow, request.nodeId, request.panelId)) { LogRuntimeTrace( "drag", "failed to start global tab drag from detached window: invalid source panel request"); return false; } BeginGlobalTabDragSession( sourceWindow.GetWindowId(), request.nodeId, request.panelId, request.screenPoint, dragHotspot); UpdateGlobalTabDragOwnerWindowPosition(); sourceWindow.AcquirePointerCapture(EditorWindowPointerCaptureOwner::GlobalTabDrag); LogRuntimeTrace( "drag", "started global tab drag from detached window '" + std::string(sourceWindow.GetWindowId()) + "' panel '" + request.panelId + "'"); return true; } bool EditorWindowWorkspaceCoordinator::TryProcessDetachRequest( EditorWindow& sourceWindow, const EditorWindowPanelTransferRequest& request) { if (sourceWindow.IsClosing()) { LogRuntimeTrace("detach", "detach request rejected: source window is closing"); return false; } const std::string sourceWindowId(sourceWindow.GetWindowId()); UIEditorWindowWorkspaceController windowWorkspaceController = BuildWorkspaceMutationController(); const UIEditorWindowWorkspaceOperationResult result = windowWorkspaceController.DetachPanelToNewWindow( sourceWindowId, request.nodeId, request.panelId); if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { LogRuntimeTrace("detach", "detach request rejected: " + result.message); return false; } if (!CommitWindowWorkspaceMutation( windowWorkspaceController, result.targetWindowId, request.screenPoint)) { LogRuntimeTrace("detach", "failed to synchronize detached window state"); return false; } if (EditorWindow* detachedWindow = m_hostRuntime.FindWindow(result.targetWindowId); detachedWindow != nullptr && detachedWindow->GetHwnd() != nullptr && !detachedWindow->IsClosing()) { SetForegroundWindow(detachedWindow->GetHwnd()); } LogRuntimeTrace( "detach", "detached panel '" + request.panelId + "' from window '" + sourceWindowId + "' to window '" + result.targetWindowId + "'"); return true; } EditorWindow* EditorWindowWorkspaceCoordinator::FindTopmostWindowAtScreenPoint( const POINT& screenPoint, std::string_view excludedWindowId) { if (const HWND hitWindow = WindowFromPoint(screenPoint); hitWindow != nullptr) { const HWND rootWindow = GetAncestor(hitWindow, GA_ROOT); if (EditorWindow* window = m_hostRuntime.FindWindow(rootWindow); window != nullptr && !window->IsClosing() && window->GetWindowId() != excludedWindowId) { return window; } } for (auto it = m_hostRuntime.GetWindows().rbegin(); it != m_hostRuntime.GetWindows().rend(); ++it) { EditorWindow* const window = it->get(); if (window == nullptr || window->GetHwnd() == nullptr || window->IsClosing() || window->GetWindowId() == excludedWindowId) { continue; } RECT windowRect = {}; if (GetWindowRect(window->GetHwnd(), &windowRect) && screenPoint.x >= windowRect.left && screenPoint.x < windowRect.right && screenPoint.y >= windowRect.top && screenPoint.y < windowRect.bottom) { return window; } } return nullptr; } const EditorWindow* EditorWindowWorkspaceCoordinator::FindTopmostWindowAtScreenPoint( const POINT& screenPoint, std::string_view excludedWindowId) const { return const_cast(this)->FindTopmostWindowAtScreenPoint( screenPoint, excludedWindowId); } void EditorWindowWorkspaceCoordinator::LogRuntimeTrace( std::string_view channel, std::string_view message) const { m_hostRuntime.LogRuntimeTrace(channel, message); } } // namespace XCEngine::UI::Editor::App::Internal