#include "EditorWindowManager.h" #include "State/EditorContext.h" #include "EditorWindow.h" #include namespace XCEngine::UI::Editor::App { namespace { using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; struct CrossWindowDockDropTarget { bool valid = false; std::string nodeId = {}; UIEditorWorkspaceDockPlacement placement = UIEditorWorkspaceDockPlacement::Center; std::size_t insertionIndex = Widgets::UIEditorTabStripInvalidIndex; }; 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; } std::size_t ResolveCrossWindowDropInsertionIndex( 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 ResolveCrossWindowDockPlacement( 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 = ResolveCrossWindowDockPlacement(tabStack, point); if (outTarget.placement == UIEditorWorkspaceDockPlacement::Center) { outTarget.insertionIndex = ResolveCrossWindowDropInsertionIndex(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 = ResolveCrossWindowDockPlacement(tabStack, point); if (outTarget.placement == UIEditorWorkspaceDockPlacement::Center) { outTarget.insertionIndex = tabStack.items.size(); } return true; } } return false; } } // namespace EditorWindow* EditorWindowManager::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 = FindWindow(rootWindow); window != nullptr && window->GetWindowId() != excludedWindowId) { return window; } } for (auto it = m_windows.rbegin(); it != m_windows.rend(); ++it) { EditorWindow* const window = it->get(); if (window == nullptr || window->GetHwnd() == nullptr || 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* EditorWindowManager::FindTopmostWindowAtScreenPoint( const POINT& screenPoint, std::string_view excludedWindowId) const { return const_cast(this)->FindTopmostWindowAtScreenPoint( screenPoint, excludedWindowId); } bool EditorWindowManager::HandleGlobalTabDragPointerButtonUp(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 = 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 (targetWindow == nullptr || targetWindow->GetHwnd() == nullptr) { 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 = BuildLiveWindowWorkspaceController(targetWindow->GetWindowId()); 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 (!SynchronizeWindowsFromController( windowWorkspaceController, {}, screenPoint)) { LogRuntimeTrace("drag", "failed to synchronize windows after cross-window drop"); return true; } if (targetWindow->GetHwnd() != nullptr) { SetForegroundWindow(targetWindow->GetHwnd()); } LogRuntimeTrace( "drag", "committed cross-window drop panel '" + panelId + "' into window '" + std::string(targetWindow->GetWindowId()) + "'"); return true; } } // namespace XCEngine::UI::Editor::App