#include "Platform/Win32/EditorWindowManager.h" #include "Platform/Win32/WindowManager/CrossWindowDropInternal.h" #include "State/EditorContext.h" #include "Platform/Win32/EditorWindow.h" #include #include namespace XCEngine::UI::Editor::App { namespace { using Win32::Internal::CrossWindowDockDropTarget; using Win32::Internal::TryResolveCrossWindowDockDropTarget; constexpr LONG kFallbackDragHotspotX = 40; constexpr LONG kFallbackDragHotspotY = 12; 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; } } // namespace bool EditorWindowManager::IsGlobalTabDragActive() const { return m_globalTabDragSession.active; } bool EditorWindowManager::OwnsActiveGlobalTabDrag(std::string_view windowId) const { return m_globalTabDragSession.active && m_globalTabDragSession.panelWindowId == windowId; } void EditorWindowManager::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 EditorWindowManager::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 EditorWindowManager::UpdateGlobalTabDragOwnerWindowPosition() { if (!m_globalTabDragSession.active) { return; } EditorWindow* ownerWindow = FindWindow(m_globalTabDragSession.panelWindowId); if (ownerWindow == nullptr || ownerWindow->GetHwnd() == nullptr) { 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 EditorWindowManager::ClearGlobalTabDragDropPreview() { if (m_globalTabDragSession.previewWindowId.empty()) { return; } if (EditorWindow* previewWindow = FindWindow(m_globalTabDragSession.previewWindowId); previewWindow != nullptr) { previewWindow->ClearExternalDockHostDropPreview(); previewWindow->InvalidateHostWindow(); } m_globalTabDragSession.previewWindowId.clear(); } void EditorWindowManager::UpdateGlobalTabDragDropPreview() { if (!m_globalTabDragSession.active) { return; } EditorWindow* targetWindow = FindTopmostWindowAtScreenPoint( m_globalTabDragSession.screenPoint, m_globalTabDragSession.panelWindowId); if (targetWindow == nullptr || targetWindow->GetHwnd() == nullptr) { 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 EditorWindowManager::EndGlobalTabDragSession() { if (!m_globalTabDragSession.active) { return; } ClearGlobalTabDragDropPreview(); if (EditorWindow* ownerWindow = FindWindow(m_globalTabDragSession.panelWindowId); ownerWindow != nullptr) { if (GetCapture() == ownerWindow->GetHwnd()) { ReleaseCapture(); } ownerWindow->ResetInteractionState(); } m_globalTabDragSession = {}; } bool EditorWindowManager::HandleGlobalTabDragPointerMove(HWND hwnd) { if (!m_globalTabDragSession.active) { return false; } const EditorWindow* ownerWindow = FindWindow(m_globalTabDragSession.panelWindowId); if (ownerWindow == nullptr || ownerWindow->GetHwnd() != hwnd) { return false; } POINT screenPoint = {}; if (GetCursorPos(&screenPoint)) { m_globalTabDragSession.screenPoint = screenPoint; UpdateGlobalTabDragOwnerWindowPosition(); UpdateGlobalTabDragDropPreview(); } return true; } void EditorWindowManager::ProcessPendingGlobalTabDragStarts() { if (m_globalTabDragSession.active) { return; } for (const std::unique_ptr& window : m_windows) { if (window == nullptr || window->GetHwnd() == nullptr) { continue; } if (TryStartGlobalTabDrag(*window)) { return; } } } bool EditorWindowManager::TryStartGlobalTabDrag(EditorWindow& sourceWindow) { const std::optional pending = sourceWindow.ConsumePendingTabDragStart(); if (!pending.has_value()) { return false; } POINT dragHotspot = BuildFallbackGlobalTabDragHotspot(); TryResolveGlobalTabDragHotspot( sourceWindow, pending->nodeId, pending->panelId, pending->screenPoint, dragHotspot); if (sourceWindow.IsPrimary()) { UIEditorWindowWorkspaceController windowWorkspaceController = BuildLiveWindowWorkspaceController(sourceWindow.GetWindowId()); const UIEditorWindowWorkspaceOperationResult result = windowWorkspaceController.DetachPanelToNewWindow( sourceWindow.GetWindowId(), pending->nodeId, pending->panelId); if (result.status != UIEditorWindowWorkspaceOperationStatus::Changed) { LogRuntimeTrace( "drag", "failed to start global tab drag from primary window: " + result.message); return false; } if (!SynchronizeWindowsFromController( windowWorkspaceController, result.targetWindowId, pending->screenPoint)) { LogRuntimeTrace("drag", "failed to synchronize detached drag window state"); return false; } EditorWindow* detachedWindow = FindWindow(result.targetWindowId); if (detachedWindow == nullptr || detachedWindow->GetHwnd() == nullptr) { LogRuntimeTrace("drag", "detached drag window was not created."); return false; } BeginGlobalTabDragSession( detachedWindow->GetWindowId(), detachedWindow->GetWorkspaceController().GetWorkspace().root.nodeId, pending->panelId, pending->screenPoint, dragHotspot); UpdateGlobalTabDragOwnerWindowPosition(); SetCapture(detachedWindow->GetHwnd()); SetForegroundWindow(detachedWindow->GetHwnd()); LogRuntimeTrace( "drag", "started global tab drag by detaching panel '" + pending->panelId + "' into window '" + std::string(detachedWindow->GetWindowId()) + "'"); return true; } sourceWindow.ResetInteractionState(); BeginGlobalTabDragSession( sourceWindow.GetWindowId(), pending->nodeId, pending->panelId, pending->screenPoint, dragHotspot); UpdateGlobalTabDragOwnerWindowPosition(); if (sourceWindow.GetHwnd() != nullptr) { SetCapture(sourceWindow.GetHwnd()); } LogRuntimeTrace( "drag", "started global tab drag from detached window '" + std::string(sourceWindow.GetWindowId()) + "' panel '" + pending->panelId + "'"); return true; } } // namespace XCEngine::UI::Editor::App