#include "Workspace/WorkspaceControllerInternal.h" #include #include #include #include #include namespace XCEngine::UI::Editor::Internal { bool IsPanelOpenAndVisible( const UIEditorWorkspaceSession& session, std::string_view panelId) { const UIEditorPanelSessionState* panelState = FindUIEditorPanelSessionState(session, panelId); return panelState != nullptr && panelState->open && panelState->visible; } std::vector CollectVisiblePanelIds( const UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceSession& session) { const std::vector panels = CollectUIEditorWorkspaceVisiblePanels(workspace, session); std::vector ids = {}; ids.reserve(panels.size()); for (const UIEditorWorkspaceVisiblePanel& panel : panels) { ids.push_back(panel.panelId); } return ids; } VisibleTabStackInfo ResolveVisibleTabStackInfo( const UIEditorWorkspaceNode& node, const UIEditorWorkspaceSession& session, std::string_view panelId) { VisibleTabStackInfo info = {}; for (const UIEditorWorkspaceNode& child : node.children) { if (child.kind != UIEditorWorkspaceNodeKind::Panel) { continue; } const bool visible = IsPanelOpenAndVisible(session, child.panel.panelId); if (child.panel.panelId == panelId) { info.panelExists = true; info.panelVisible = visible; if (visible) { info.currentVisibleIndex = info.visibleTabCount; } } if (visible) { ++info.visibleTabCount; } } return info; } std::size_t CountVisibleTabs( const UIEditorWorkspaceNode& node, const UIEditorWorkspaceSession& session) { if (node.kind != UIEditorWorkspaceNodeKind::TabStack) { return 0u; } std::size_t visibleCount = 0u; for (const UIEditorWorkspaceNode& child : node.children) { if (child.kind == UIEditorWorkspaceNodeKind::Panel && IsPanelOpenAndVisible(session, child.panel.panelId)) { ++visibleCount; } } return visibleCount; } } // namespace XCEngine::UI::Editor::Internal namespace XCEngine::UI::Editor { std::string_view GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind kind) { switch (kind) { case UIEditorWorkspaceCommandKind::OpenPanel: return "OpenPanel"; case UIEditorWorkspaceCommandKind::ClosePanel: return "ClosePanel"; case UIEditorWorkspaceCommandKind::ShowPanel: return "ShowPanel"; case UIEditorWorkspaceCommandKind::HidePanel: return "HidePanel"; case UIEditorWorkspaceCommandKind::ActivatePanel: return "ActivatePanel"; case UIEditorWorkspaceCommandKind::ResetWorkspace: return "ResetWorkspace"; } return "Unknown"; } std::string_view GetUIEditorWorkspaceCommandStatusName( UIEditorWorkspaceCommandStatus status) { switch (status) { case UIEditorWorkspaceCommandStatus::Changed: return "Changed"; case UIEditorWorkspaceCommandStatus::NoOp: return "NoOp"; case UIEditorWorkspaceCommandStatus::Rejected: return "Rejected"; } return "Unknown"; } std::string_view GetUIEditorWorkspaceLayoutOperationStatusName( UIEditorWorkspaceLayoutOperationStatus status) { switch (status) { case UIEditorWorkspaceLayoutOperationStatus::Changed: return "Changed"; case UIEditorWorkspaceLayoutOperationStatus::NoOp: return "NoOp"; case UIEditorWorkspaceLayoutOperationStatus::Rejected: return "Rejected"; } return "Unknown"; } UIEditorWorkspaceController::UIEditorWorkspaceController( UIEditorPanelRegistry panelRegistry, UIEditorWorkspaceModel workspace, UIEditorWorkspaceSession session) : m_panelRegistry(std::move(panelRegistry)) , m_baselineWorkspace(CanonicalizeUIEditorWorkspaceModel(workspace)) , m_baselineSession(session) , m_workspace(m_baselineWorkspace) , m_session(std::move(session)) { } UIEditorWorkspaceController::UIEditorWorkspaceController( const UIEditorWorkspaceController& other) : m_panelRegistry(other.m_panelRegistry) , m_baselineWorkspace(other.m_baselineWorkspace) , m_baselineSession(other.m_baselineSession) , m_workspace(other.m_workspace) , m_session(other.m_session) { } UIEditorWorkspaceController& UIEditorWorkspaceController::operator=( const UIEditorWorkspaceController& other) { if (this == &other) { return *this; } m_panelRegistry = other.m_panelRegistry; m_baselineWorkspace = other.m_baselineWorkspace; m_baselineSession = other.m_baselineSession; m_workspace = other.m_workspace; m_session = other.m_session; m_boundWorkspace = nullptr; m_boundSession = nullptr; return *this; } UIEditorWorkspaceController UIEditorWorkspaceController::BindToState( UIEditorPanelRegistry panelRegistry, UIEditorWorkspaceModel& workspace, UIEditorWorkspaceSession& session) { UIEditorWorkspaceController controller( std::move(panelRegistry), workspace, session); controller.m_boundWorkspace = &workspace; controller.m_boundSession = &session; return controller; } void UIEditorWorkspaceController::SyncBoundState() { if (m_boundWorkspace != nullptr) { *m_boundWorkspace = m_workspace; } if (m_boundSession != nullptr) { *m_boundSession = m_session; } } UIEditorWorkspaceControllerValidationResult UIEditorWorkspaceController::ValidateState() const { const UIEditorPanelRegistryValidationResult registryValidation = ValidateUIEditorPanelRegistry(m_panelRegistry); if (!registryValidation.IsValid()) { UIEditorWorkspaceControllerValidationResult result = {}; result.code = UIEditorWorkspaceControllerValidationCode::InvalidPanelRegistry; result.message = registryValidation.message; return result; } const UIEditorWorkspaceValidationResult workspaceValidation = ValidateUIEditorWorkspace(m_workspace); if (!workspaceValidation.IsValid()) { UIEditorWorkspaceControllerValidationResult result = {}; result.code = UIEditorWorkspaceControllerValidationCode::InvalidWorkspace; result.message = workspaceValidation.message; return result; } const UIEditorWorkspaceSessionValidationResult sessionValidation = ValidateUIEditorWorkspaceSession(m_panelRegistry, m_workspace, m_session); if (!sessionValidation.IsValid()) { UIEditorWorkspaceControllerValidationResult result = {}; result.code = UIEditorWorkspaceControllerValidationCode::InvalidWorkspaceSession; result.message = sessionValidation.message; return result; } return {}; } UIEditorWorkspaceLayoutSnapshot UIEditorWorkspaceController::CaptureLayoutSnapshot() const { return BuildUIEditorWorkspaceLayoutSnapshot(m_workspace, m_session); } UIEditorWorkspaceCommandResult UIEditorWorkspaceController::BuildResult( const UIEditorWorkspaceCommand& command, UIEditorWorkspaceCommandStatus status, std::string message) const { UIEditorWorkspaceCommandResult result = {}; result.kind = command.kind; result.status = status; result.panelId = command.panelId; result.message = std::move(message); result.activePanelId = m_workspace.activePanelId; result.visiblePanelIds = Internal::CollectVisiblePanelIds(m_workspace, m_session); return result; } UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus status, std::string message) const { UIEditorWorkspaceLayoutOperationResult result = {}; result.status = status; result.message = std::move(message); result.activePanelId = m_workspace.activePanelId; result.visiblePanelIds = Internal::CollectVisiblePanelIds(m_workspace, m_session); return result; } UIEditorWorkspaceCommandResult UIEditorWorkspaceController::FinalizeMutation( const UIEditorWorkspaceCommand& command, bool changed, std::string changedMessage, std::string unexpectedFailureMessage, const UIEditorWorkspaceModel& previousWorkspace, const UIEditorWorkspaceSession& previousSession) { if (!changed) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, std::move(unexpectedFailureMessage)); } const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); if (!validation.IsValid()) { m_workspace = previousWorkspace; m_session = previousSession; return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "Command produced invalid workspace state: " + validation.message); } SyncBoundState(); return BuildResult( command, UIEditorWorkspaceCommandStatus::Changed, std::move(changedMessage)); } const UIEditorPanelDescriptor* UIEditorWorkspaceController::FindPanelDescriptor( std::string_view panelId) const { return FindUIEditorPanelDescriptor(m_panelRegistry, panelId); } UIEditorWorkspaceController BuildDefaultUIEditorWorkspaceController( const UIEditorPanelRegistry& panelRegistry, const UIEditorWorkspaceModel& workspace) { const UIEditorWorkspaceModel canonicalWorkspace = CanonicalizeUIEditorWorkspaceModel(workspace); return UIEditorWorkspaceController( panelRegistry, canonicalWorkspace, BuildDefaultUIEditorWorkspaceSession(panelRegistry, canonicalWorkspace)); } UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch( const UIEditorWorkspaceCommand& command) { const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); if (command.kind != UIEditorWorkspaceCommandKind::ResetWorkspace && !validation.IsValid()) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "Controller state invalid: " + validation.message); } const UIEditorWorkspaceModel previousWorkspace = m_workspace; const UIEditorWorkspaceSession previousSession = m_session; const UIEditorPanelSessionState* panelState = command.kind == UIEditorWorkspaceCommandKind::ResetWorkspace ? nullptr : FindUIEditorPanelSessionState(m_session, command.panelId); const UIEditorPanelDescriptor* panelDescriptor = command.kind == UIEditorWorkspaceCommandKind::ResetWorkspace ? nullptr : FindPanelDescriptor(command.panelId); switch (command.kind) { case UIEditorWorkspaceCommandKind::OpenPanel: if (command.panelId.empty()) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "OpenPanel requires a panelId."); } if (panelDescriptor == nullptr || panelState == nullptr) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "OpenPanel target panel is missing."); } if (panelState->open && panelState->visible) { return BuildResult( command, UIEditorWorkspaceCommandStatus::NoOp, "Panel is already open and visible."); } return FinalizeMutation( command, TryOpenUIEditorWorkspacePanel( m_panelRegistry, m_workspace, m_session, command.panelId), "Panel opened and activated.", "OpenPanel failed unexpectedly.", previousWorkspace, previousSession); case UIEditorWorkspaceCommandKind::ClosePanel: if (command.panelId.empty()) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "ClosePanel requires a panelId."); } if (panelDescriptor == nullptr || panelState == nullptr) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "ClosePanel target panel is missing."); } if (!panelDescriptor->canClose) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "Panel cannot be closed."); } if (!panelState->open) { return BuildResult( command, UIEditorWorkspaceCommandStatus::NoOp, "Panel is already closed."); } return FinalizeMutation( command, TryCloseUIEditorWorkspacePanel( m_panelRegistry, m_workspace, m_session, command.panelId), "Panel closed.", "ClosePanel failed unexpectedly.", previousWorkspace, previousSession); case UIEditorWorkspaceCommandKind::ShowPanel: if (command.panelId.empty()) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "ShowPanel requires a panelId."); } if (panelDescriptor == nullptr || panelState == nullptr) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "ShowPanel target panel is missing."); } if (!panelState->open) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "Closed panel must be opened before it can be shown."); } if (panelState->visible) { return BuildResult( command, UIEditorWorkspaceCommandStatus::NoOp, "Panel is already visible."); } return FinalizeMutation( command, TryShowUIEditorWorkspacePanel( m_panelRegistry, m_workspace, m_session, command.panelId), "Panel shown and activated.", "ShowPanel failed unexpectedly.", previousWorkspace, previousSession); case UIEditorWorkspaceCommandKind::HidePanel: if (command.panelId.empty()) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "HidePanel requires a panelId."); } if (panelDescriptor == nullptr || panelState == nullptr) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "HidePanel target panel is missing."); } if (!panelDescriptor->canHide) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "Panel cannot be hidden."); } if (!panelState->open) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "Closed panel cannot be hidden."); } if (!panelState->visible) { return BuildResult( command, UIEditorWorkspaceCommandStatus::NoOp, "Panel is already hidden."); } return FinalizeMutation( command, TryHideUIEditorWorkspacePanel( m_panelRegistry, m_workspace, m_session, command.panelId), "Panel hidden and active panel re-resolved.", "HidePanel failed unexpectedly.", previousWorkspace, previousSession); case UIEditorWorkspaceCommandKind::ActivatePanel: if (command.panelId.empty()) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "ActivatePanel requires a panelId."); } if (panelDescriptor == nullptr || panelState == nullptr) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "ActivatePanel target panel is missing."); } if (!panelState->open || !panelState->visible) { return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "Only open and visible panels can be activated."); } if (m_workspace.activePanelId == command.panelId) { return BuildResult( command, UIEditorWorkspaceCommandStatus::NoOp, "Panel is already active."); } return FinalizeMutation( command, TryActivateUIEditorWorkspacePanel( m_panelRegistry, m_workspace, m_session, command.panelId), "Panel activated.", "ActivatePanel failed unexpectedly.", previousWorkspace, previousSession); case UIEditorWorkspaceCommandKind::ResetWorkspace: if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, m_baselineWorkspace) && AreUIEditorWorkspaceSessionsEquivalent(m_session, m_baselineSession)) { return BuildResult( command, UIEditorWorkspaceCommandStatus::NoOp, "Workspace already matches the baseline state."); } m_workspace = m_baselineWorkspace; m_session = m_baselineSession; return FinalizeMutation( command, true, "Workspace reset to baseline.", "ResetWorkspace failed unexpectedly.", previousWorkspace, previousSession); } return BuildResult( command, UIEditorWorkspaceCommandStatus::Rejected, "Unknown command kind."); } UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayoutSnapshot( const UIEditorWorkspaceLayoutSnapshot& snapshot) { UIEditorWorkspaceLayoutSnapshot canonicalSnapshot = snapshot; canonicalSnapshot.workspace = CanonicalizeUIEditorWorkspaceModel(std::move(canonicalSnapshot.workspace)); const UIEditorPanelRegistryValidationResult registryValidation = ValidateUIEditorPanelRegistry(m_panelRegistry); if (!registryValidation.IsValid()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Panel registry invalid: " + registryValidation.message); } const UIEditorWorkspaceValidationResult workspaceValidation = ValidateUIEditorWorkspace(canonicalSnapshot.workspace); if (!workspaceValidation.IsValid()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Layout workspace invalid: " + workspaceValidation.message); } const UIEditorWorkspaceSessionValidationResult sessionValidation = ValidateUIEditorWorkspaceSession( m_panelRegistry, canonicalSnapshot.workspace, canonicalSnapshot.session); if (!sessionValidation.IsValid()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Layout session invalid: " + sessionValidation.message); } if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, canonicalSnapshot.workspace) && AreUIEditorWorkspaceSessionsEquivalent(m_session, canonicalSnapshot.session)) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::NoOp, "Current state already matches the requested layout snapshot."); } const UIEditorWorkspaceModel previousWorkspace = m_workspace; const UIEditorWorkspaceSession previousSession = m_session; m_workspace = canonicalSnapshot.workspace; m_session = canonicalSnapshot.session; const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); if (!validation.IsValid()) { m_workspace = previousWorkspace; m_session = previousSession; return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Restored layout produced invalid controller state: " + validation.message); } SyncBoundState(); return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Changed, "Layout restored."); } UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreSerializedLayout( std::string_view serializedLayout) { const UIEditorWorkspaceLayoutLoadResult loadResult = DeserializeUIEditorWorkspaceLayoutSnapshot(m_panelRegistry, serializedLayout); if (!loadResult.IsValid()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Serialized layout rejected: " + loadResult.message); } return RestoreLayoutSnapshot(loadResult.snapshot); } UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::SetSplitRatio( std::string_view nodeId, float splitRatio) { const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); if (!validation.IsValid()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Controller state invalid: " + validation.message); } if (nodeId.empty()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "SetSplitRatio requires a split node id."); } const UIEditorWorkspaceNode* splitNode = FindUIEditorWorkspaceNode(m_workspace, nodeId); if (splitNode == nullptr || splitNode->kind != UIEditorWorkspaceNodeKind::Split) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "SetSplitRatio target split node is missing."); } if (std::fabs(splitNode->splitRatio - splitRatio) <= 0.0001f) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::NoOp, "Split ratio already matches the requested value."); } const UIEditorWorkspaceModel previousWorkspace = m_workspace; if (!TrySetUIEditorWorkspaceSplitRatio(m_workspace, nodeId, splitRatio)) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Split ratio update rejected."); } const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState(); if (!postValidation.IsValid()) { m_workspace = previousWorkspace; return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Split ratio update produced invalid controller state: " + postValidation.message); } SyncBoundState(); return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Changed, "Split ratio updated."); } UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::MoveTabToStack( std::string_view sourceNodeId, std::string_view panelId, std::string_view targetNodeId, std::size_t targetVisibleInsertionIndex) { { std::ostringstream trace = {}; trace << "MoveTabToStack begin sourceNode=" << sourceNodeId << " panel=" << panelId << " targetNode=" << targetNodeId << " insertion=" << targetVisibleInsertionIndex; AppendUIEditorRuntimeTrace("workspace", trace.str()); } const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); if (!validation.IsValid()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Controller state invalid: " + validation.message); } if (sourceNodeId.empty() || targetNodeId.empty()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "MoveTabToStack requires both source and target tab stack ids."); } if (panelId.empty()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "MoveTabToStack requires a panel id."); } if (sourceNodeId == targetNodeId) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "MoveTabToStack requires distinct source and target tab stack ids."); } const UIEditorWorkspaceNode* sourceTabStack = FindUIEditorWorkspaceNode(m_workspace, sourceNodeId); const UIEditorWorkspaceNode* targetTabStack = FindUIEditorWorkspaceNode(m_workspace, targetNodeId); if (sourceTabStack == nullptr || targetTabStack == nullptr || sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack || targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "MoveTabToStack source or target tab stack is missing."); } const Internal::VisibleTabStackInfo sourceInfo = Internal::ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId); if (!sourceInfo.panelExists) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "MoveTabToStack target panel is missing from the source tab stack."); } if (!sourceInfo.panelVisible) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "MoveTabToStack only supports open and visible tabs."); } const std::size_t visibleTargetCount = Internal::CountVisibleTabs(*targetTabStack, m_session); if (targetVisibleInsertionIndex > visibleTargetCount) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "MoveTabToStack target visible insertion index is out of range."); } const UIEditorWorkspaceModel previousWorkspace = m_workspace; if (!TryMoveUIEditorWorkspaceTabToStack( m_workspace, m_session, sourceNodeId, panelId, targetNodeId, targetVisibleInsertionIndex)) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "MoveTabToStack rejected."); } if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::NoOp, "Tab already matches the requested target stack insertion."); } const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState(); if (!postValidation.IsValid()) { m_workspace = previousWorkspace; return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "MoveTabToStack produced invalid controller state: " + postValidation.message); } SyncBoundState(); return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Changed, "Tab moved to target stack."); } UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::DockTabRelative( std::string_view sourceNodeId, std::string_view panelId, std::string_view targetNodeId, UIEditorWorkspaceDockPlacement placement, float splitRatio) { { std::ostringstream trace = {}; trace << "DockTabRelative begin sourceNode=" << sourceNodeId << " panel=" << panelId << " targetNode=" << targetNodeId << " placement=" << static_cast(placement) << " splitRatio=" << splitRatio; AppendUIEditorRuntimeTrace("workspace", trace.str()); } const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); if (!validation.IsValid()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Controller state invalid: " + validation.message); } if (sourceNodeId.empty() || targetNodeId.empty()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "DockTabRelative requires both source and target tab stack ids."); } if (panelId.empty()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "DockTabRelative requires a panel id."); } const UIEditorWorkspaceNode* sourceTabStack = FindUIEditorWorkspaceNode(m_workspace, sourceNodeId); const UIEditorWorkspaceNode* targetTabStack = FindUIEditorWorkspaceNode(m_workspace, targetNodeId); if (sourceTabStack == nullptr || targetTabStack == nullptr || sourceTabStack->kind != UIEditorWorkspaceNodeKind::TabStack || targetTabStack->kind != UIEditorWorkspaceNodeKind::TabStack) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "DockTabRelative source or target tab stack is missing."); } const Internal::VisibleTabStackInfo sourceInfo = Internal::ResolveVisibleTabStackInfo(*sourceTabStack, m_session, panelId); if (!sourceInfo.panelExists) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "DockTabRelative target panel is missing from the source tab stack."); } if (!sourceInfo.panelVisible) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "DockTabRelative only supports open and visible tabs."); } const UIEditorWorkspaceModel previousWorkspace = m_workspace; if (!TryDockUIEditorWorkspaceTabRelative( m_workspace, m_session, sourceNodeId, panelId, targetNodeId, placement, splitRatio)) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "DockTabRelative rejected."); } if (AreUIEditorWorkspaceModelsEquivalent(previousWorkspace, m_workspace)) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::NoOp, "Dock layout already matches the requested placement."); } const UIEditorWorkspaceControllerValidationResult postValidation = ValidateState(); if (!postValidation.IsValid()) { m_workspace = previousWorkspace; return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "DockTabRelative produced invalid controller state: " + postValidation.message); } SyncBoundState(); return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Changed, "Tab docked relative to target stack."); } } // namespace XCEngine::UI::Editor