#include #include #include #include namespace XCEngine::UI::Editor { namespace { bool IsSinglePanelRootWindow( const UIEditorWindowWorkspaceState& state, std::string_view panelId) { const UIEditorWorkspaceNode& root = state.workspace.root; return root.kind == UIEditorWorkspaceNodeKind::TabStack && root.children.size() == 1u && root.children.front().kind == UIEditorWorkspaceNodeKind::Panel && root.children.front().panel.panelId == panelId && state.session.panelStates.size() == 1u && state.session.panelStates.front().panelId == panelId; } bool TryExtractPanelFromWindow( UIEditorWindowWorkspaceSet& windowSet, std::string_view sourceWindowId, std::string_view primaryWindowId, std::string_view sourceNodeId, std::string_view panelId, UIEditorWorkspaceExtractedPanel& extractedPanel) { UIEditorWindowWorkspaceState* sourceWindow = FindMutableUIEditorWindowWorkspaceState(windowSet, sourceWindowId); if (sourceWindow == nullptr) { return false; } if (sourceWindowId != primaryWindowId && IsSinglePanelRootWindow(*sourceWindow, panelId)) { extractedPanel.panelNode = std::move(sourceWindow->workspace.root.children.front()); extractedPanel.sessionState = sourceWindow->session.panelStates.front(); windowSet.windows.erase( std::remove_if( windowSet.windows.begin(), windowSet.windows.end(), [sourceWindowId](const UIEditorWindowWorkspaceState& state) { return state.windowId == sourceWindowId; }), windowSet.windows.end()); return true; } return TryExtractUIEditorWorkspaceVisiblePanel( sourceWindow->workspace, sourceWindow->session, sourceNodeId, panelId, extractedPanel); } } // namespace std::string_view GetUIEditorWindowWorkspaceOperationStatusName( UIEditorWindowWorkspaceOperationStatus status) { switch (status) { case UIEditorWindowWorkspaceOperationStatus::Changed: return "Changed"; case UIEditorWindowWorkspaceOperationStatus::NoOp: return "NoOp"; case UIEditorWindowWorkspaceOperationStatus::Rejected: return "Rejected"; } return "Unknown"; } UIEditorWindowWorkspaceController::UIEditorWindowWorkspaceController( UIEditorPanelRegistry panelRegistry, UIEditorWindowWorkspaceSet windowSet) : m_panelRegistry(std::move(panelRegistry)) , m_windowSet(std::move(windowSet)) { } UIEditorWindowWorkspaceValidationResult UIEditorWindowWorkspaceController::ValidateState() const { return ValidateUIEditorWindowWorkspaceSet(m_panelRegistry, m_windowSet); } UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::BuildOperationResult( UIEditorWindowWorkspaceOperationStatus status, std::string message, std::string_view sourceWindowId, std::string_view targetWindowId, std::string_view panelId) const { UIEditorWindowWorkspaceOperationResult result = {}; result.status = status; result.message = std::move(message); result.sourceWindowId = std::string(sourceWindowId); result.targetWindowId = std::string(targetWindowId); result.panelId = std::string(panelId); result.activeWindowId = m_windowSet.activeWindowId; result.windowIds.reserve(m_windowSet.windows.size()); for (const UIEditorWindowWorkspaceState& state : m_windowSet.windows) { result.windowIds.push_back(state.windowId); } return result; } std::string UIEditorWindowWorkspaceController::MakeUniqueWindowId(std::string_view base) const { std::string resolvedBase = base.empty() ? std::string("detached-window") : std::string(base); if (FindUIEditorWindowWorkspaceState(m_windowSet, resolvedBase) == nullptr) { return resolvedBase; } for (std::size_t suffix = 1u; suffix < 1024u; ++suffix) { const std::string candidate = resolvedBase + "-" + std::to_string(suffix); if (FindUIEditorWindowWorkspaceState(m_windowSet, candidate) == nullptr) { return candidate; } } return resolvedBase + "-overflow"; } UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::DetachPanelToNewWindow( std::string_view sourceWindowId, std::string_view sourceNodeId, std::string_view panelId, std::string_view preferredNewWindowId) { const UIEditorWindowWorkspaceValidationResult stateValidation = ValidateState(); if (!stateValidation.IsValid()) { return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Window workspace state invalid: " + stateValidation.message, sourceWindowId, {}, panelId); } const UIEditorWindowWorkspaceState* sourceWindow = FindUIEditorWindowWorkspaceState(m_windowSet, sourceWindowId); if (sourceWindow == nullptr) { return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Source window not found.", sourceWindowId, {}, panelId); } if (sourceWindowId != m_windowSet.primaryWindowId && IsSinglePanelRootWindow(*sourceWindow, panelId)) { return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::NoOp, "Panel already occupies its own detached window.", sourceWindowId, sourceWindowId, panelId); } const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet; UIEditorWorkspaceExtractedPanel extractedPanel = {}; if (!TryExtractPanelFromWindow( m_windowSet, sourceWindowId, m_windowSet.primaryWindowId, sourceNodeId, panelId, extractedPanel)) { m_windowSet = windowSetBefore; return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Failed to extract panel from source window.", sourceWindowId, {}, panelId); } const std::string newWindowId = MakeUniqueWindowId( preferredNewWindowId.empty() ? std::string(panelId) + "-window" : std::string(preferredNewWindowId)); UIEditorWindowWorkspaceState detachedWindow = {}; detachedWindow.windowId = newWindowId; detachedWindow.workspace = BuildUIEditorDetachedWorkspaceFromExtractedPanel( newWindowId + "-root", extractedPanel); detachedWindow.session = BuildUIEditorDetachedWorkspaceSessionFromExtractedPanel(extractedPanel); m_windowSet.windows.push_back(std::move(detachedWindow)); m_windowSet.activeWindowId = newWindowId; const UIEditorWindowWorkspaceValidationResult validation = ValidateState(); if (!validation.IsValid()) { m_windowSet = windowSetBefore; return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Detach produced invalid state: " + validation.message, sourceWindowId, newWindowId, panelId); } return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Changed, "Panel detached into a new window.", sourceWindowId, newWindowId, panelId); } UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::MovePanelToStack( std::string_view sourceWindowId, std::string_view sourceNodeId, std::string_view panelId, std::string_view targetWindowId, std::string_view targetNodeId, std::size_t targetVisibleInsertionIndex) { const UIEditorWindowWorkspaceValidationResult stateValidation = ValidateState(); if (!stateValidation.IsValid()) { return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Window workspace state invalid: " + stateValidation.message, sourceWindowId, targetWindowId, panelId); } if (sourceWindowId == targetWindowId) { UIEditorWindowWorkspaceState* window = FindMutableUIEditorWindowWorkspaceState(m_windowSet, sourceWindowId); if (window == nullptr) { return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Source window not found.", sourceWindowId, targetWindowId, panelId); } if (!TryMoveUIEditorWorkspaceTabToStack( window->workspace, window->session, sourceNodeId, panelId, targetNodeId, targetVisibleInsertionIndex)) { return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Move operation rejected by the workspace model.", sourceWindowId, targetWindowId, panelId); } m_windowSet.activeWindowId = std::string(targetWindowId); return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Changed, "Panel moved within the same window.", sourceWindowId, targetWindowId, panelId); } const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet; UIEditorWorkspaceExtractedPanel extractedPanel = {}; if (!TryExtractPanelFromWindow( m_windowSet, sourceWindowId, m_windowSet.primaryWindowId, sourceNodeId, panelId, extractedPanel)) { m_windowSet = windowSetBefore; return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Failed to extract panel from source window.", sourceWindowId, targetWindowId, panelId); } UIEditorWindowWorkspaceState* targetWindow = FindMutableUIEditorWindowWorkspaceState(m_windowSet, targetWindowId); if (targetWindow == nullptr || !TryInsertExtractedUIEditorWorkspacePanelToStack( targetWindow->workspace, targetWindow->session, std::move(extractedPanel), targetNodeId, targetVisibleInsertionIndex)) { m_windowSet = windowSetBefore; return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Failed to insert the extracted panel into the target stack.", sourceWindowId, targetWindowId, panelId); } m_windowSet.activeWindowId = std::string(targetWindowId); const UIEditorWindowWorkspaceValidationResult validation = ValidateState(); if (!validation.IsValid()) { m_windowSet = windowSetBefore; return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Move produced invalid state: " + validation.message, sourceWindowId, targetWindowId, panelId); } return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Changed, "Panel moved across windows.", sourceWindowId, targetWindowId, panelId); } UIEditorWindowWorkspaceOperationResult UIEditorWindowWorkspaceController::DockPanelRelative( std::string_view sourceWindowId, std::string_view sourceNodeId, std::string_view panelId, std::string_view targetWindowId, std::string_view targetNodeId, UIEditorWorkspaceDockPlacement placement, float splitRatio) { const UIEditorWindowWorkspaceValidationResult stateValidation = ValidateState(); if (!stateValidation.IsValid()) { return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Window workspace state invalid: " + stateValidation.message, sourceWindowId, targetWindowId, panelId); } if (sourceWindowId == targetWindowId) { UIEditorWindowWorkspaceState* window = FindMutableUIEditorWindowWorkspaceState(m_windowSet, sourceWindowId); if (window == nullptr) { return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Source window not found.", sourceWindowId, targetWindowId, panelId); } if (!TryDockUIEditorWorkspaceTabRelative( window->workspace, window->session, sourceNodeId, panelId, targetNodeId, placement, splitRatio)) { return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Dock operation rejected by the workspace model.", sourceWindowId, targetWindowId, panelId); } m_windowSet.activeWindowId = std::string(targetWindowId); return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Changed, "Panel docked within the same window.", sourceWindowId, targetWindowId, panelId); } const UIEditorWindowWorkspaceSet windowSetBefore = m_windowSet; UIEditorWorkspaceExtractedPanel extractedPanel = {}; if (!TryExtractPanelFromWindow( m_windowSet, sourceWindowId, m_windowSet.primaryWindowId, sourceNodeId, panelId, extractedPanel)) { m_windowSet = windowSetBefore; return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Failed to extract panel from source window.", sourceWindowId, targetWindowId, panelId); } UIEditorWindowWorkspaceState* targetWindow = FindMutableUIEditorWindowWorkspaceState(m_windowSet, targetWindowId); if (targetWindow == nullptr || !TryDockExtractedUIEditorWorkspacePanelRelative( targetWindow->workspace, targetWindow->session, std::move(extractedPanel), targetNodeId, placement, splitRatio)) { m_windowSet = windowSetBefore; return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Failed to dock the extracted panel into the target window.", sourceWindowId, targetWindowId, panelId); } m_windowSet.activeWindowId = std::string(targetWindowId); const UIEditorWindowWorkspaceValidationResult validation = ValidateState(); if (!validation.IsValid()) { m_windowSet = windowSetBefore; return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Rejected, "Dock produced invalid state: " + validation.message, sourceWindowId, targetWindowId, panelId); } return BuildOperationResult( UIEditorWindowWorkspaceOperationStatus::Changed, "Panel docked across windows.", sourceWindowId, targetWindowId, panelId); } UIEditorWindowWorkspaceController BuildDefaultUIEditorWindowWorkspaceController( const UIEditorPanelRegistry& panelRegistry, const UIEditorWorkspaceModel& workspace, std::string primaryWindowId) { return UIEditorWindowWorkspaceController( panelRegistry, BuildDefaultUIEditorWindowWorkspaceSet( panelRegistry, workspace, std::move(primaryWindowId))); } } // namespace XCEngine::UI::Editor