#include #include #include #include #include #include namespace XCEngine::UI::Editor { namespace { UIEditorWorkspaceValidationResult MakeValidationError( UIEditorWorkspaceValidationCode code, std::string message) { UIEditorWorkspaceValidationResult result = {}; result.code = code; result.message = std::move(message); return result; } bool IsValidSplitRatio(float value) { return std::isfinite(value) && value > 0.0f && value < 1.0f; } std::string BuildSingleTabPanelNodeId(std::string_view stackNodeId) { if (stackNodeId.empty()) { return "single-tab-panel"; } return std::string(stackNodeId) + "__panel"; } UIEditorWorkspaceNode WrapStandalonePanelAsTabStack(UIEditorWorkspaceNode panelNode) { UIEditorWorkspaceNode panelChild = std::move(panelNode); const std::string stackNodeId = panelChild.nodeId; panelChild.nodeId = BuildSingleTabPanelNodeId(stackNodeId); UIEditorWorkspaceNode tabStack = {}; tabStack.kind = UIEditorWorkspaceNodeKind::TabStack; tabStack.nodeId = stackNodeId; tabStack.selectedTabIndex = 0u; tabStack.children.push_back(std::move(panelChild)); return tabStack; } void CollapseSplitNodeToOnlyChild(UIEditorWorkspaceNode& node) { if (node.kind != UIEditorWorkspaceNodeKind::Split || node.children.size() != 1u) { return; } // Move the remaining child through a temporary object first. Assigning // directly from node.children.front() aliases a subobject of node and can // trigger use-after-move when the vector storage is torn down. UIEditorWorkspaceNode remainingChild = std::move(node.children.front()); node = std::move(remainingChild); } void CanonicalizeNodeRecursive( UIEditorWorkspaceNode& node, bool allowStandalonePanelLeaf) { if (node.kind == UIEditorWorkspaceNodeKind::Panel) { if (!allowStandalonePanelLeaf) { node = WrapStandalonePanelAsTabStack(std::move(node)); } return; } for (UIEditorWorkspaceNode& child : node.children) { CanonicalizeNodeRecursive( child, node.kind == UIEditorWorkspaceNodeKind::TabStack); } if (node.kind == UIEditorWorkspaceNodeKind::TabStack && !node.children.empty() && node.selectedTabIndex >= node.children.size()) { node.selectedTabIndex = node.children.size() - 1u; } if (node.kind == UIEditorWorkspaceNodeKind::Split && node.children.size() == 1u) { CollapseSplitNodeToOnlyChild(node); } } const UIEditorPanelDescriptor& RequirePanelDescriptor( const UIEditorPanelRegistry& registry, std::string_view panelId) { if (const UIEditorPanelDescriptor* descriptor = FindUIEditorPanelDescriptor(registry, panelId); descriptor != nullptr) { return *descriptor; } static const UIEditorPanelDescriptor fallbackDescriptor = {}; return fallbackDescriptor; } const UIEditorWorkspacePanelState* FindPanelRecursive( const UIEditorWorkspaceNode& node, std::string_view panelId) { if (node.kind == UIEditorWorkspaceNodeKind::Panel) { return node.panel.panelId == panelId ? &node.panel : nullptr; } for (const UIEditorWorkspaceNode& child : node.children) { if (const UIEditorWorkspacePanelState* found = FindPanelRecursive(child, panelId)) { return found; } } return nullptr; } const UIEditorWorkspaceNode* FindNodeRecursive( const UIEditorWorkspaceNode& node, std::string_view nodeId) { if (node.nodeId == nodeId) { return &node; } for (const UIEditorWorkspaceNode& child : node.children) { if (const UIEditorWorkspaceNode* found = FindNodeRecursive(child, nodeId)) { return found; } } return nullptr; } UIEditorWorkspaceNode* FindMutableNodeRecursive( UIEditorWorkspaceNode& node, std::string_view nodeId) { if (node.nodeId == nodeId) { return &node; } for (UIEditorWorkspaceNode& child : node.children) { if (UIEditorWorkspaceNode* found = FindMutableNodeRecursive(child, nodeId)) { return found; } } return nullptr; } bool FindNodePathRecursive( const UIEditorWorkspaceNode& node, std::string_view nodeId, std::vector& path) { if (node.nodeId == nodeId) { return true; } for (std::size_t index = 0; index < node.children.size(); ++index) { path.push_back(index); if (FindNodePathRecursive(node.children[index], nodeId, path)) { return true; } path.pop_back(); } return false; } UIEditorWorkspaceNode* ResolveMutableNodeByPath( UIEditorWorkspaceNode& node, const std::vector& path) { UIEditorWorkspaceNode* current = &node; for (const std::size_t childIndex : path) { if (childIndex >= current->children.size()) { return nullptr; } current = ¤t->children[childIndex]; } return current; } bool IsPanelOpenAndVisibleInSession( const UIEditorWorkspaceSession& session, std::string_view panelId) { const UIEditorPanelSessionState* state = FindUIEditorPanelSessionState(session, panelId); return state != nullptr && state->open && state->visible; } std::size_t CountVisibleChildren( 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 && IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) { ++visibleCount; } } return visibleCount; } std::size_t ResolveActualInsertionIndexForVisibleInsertion( const UIEditorWorkspaceNode& node, const UIEditorWorkspaceSession& session, std::size_t targetVisibleInsertionIndex) { std::vector visibleIndices = {}; visibleIndices.reserve(node.children.size()); for (std::size_t index = 0; index < node.children.size(); ++index) { const UIEditorWorkspaceNode& child = node.children[index]; if (child.kind == UIEditorWorkspaceNodeKind::Panel && IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) { visibleIndices.push_back(index); } } if (targetVisibleInsertionIndex == 0u) { return visibleIndices.empty() ? 0u : visibleIndices.front(); } if (visibleIndices.empty()) { return 0u; } if (targetVisibleInsertionIndex >= visibleIndices.size()) { return visibleIndices.back() + 1u; } return visibleIndices[targetVisibleInsertionIndex]; } void FixTabStackSelectedIndex( UIEditorWorkspaceNode& node, std::string_view preferredPanelId) { if (node.kind != UIEditorWorkspaceNodeKind::TabStack || node.children.empty()) { return; } for (std::size_t index = 0; index < node.children.size(); ++index) { if (node.children[index].kind == UIEditorWorkspaceNodeKind::Panel && node.children[index].panel.panelId == preferredPanelId) { node.selectedTabIndex = index; return; } } if (node.selectedTabIndex >= node.children.size()) { node.selectedTabIndex = node.children.size() - 1u; } } bool RemoveNodeByIdRecursive( UIEditorWorkspaceNode& node, std::string_view nodeId) { if (node.kind != UIEditorWorkspaceNodeKind::Split) { return false; } for (std::size_t index = 0; index < node.children.size(); ++index) { if (node.children[index].nodeId == nodeId) { node.children.erase(node.children.begin() + static_cast(index)); if (node.children.size() == 1u) { CollapseSplitNodeToOnlyChild(node); } return true; } } for (UIEditorWorkspaceNode& child : node.children) { if (RemoveNodeByIdRecursive(child, nodeId)) { if (node.kind == UIEditorWorkspaceNodeKind::Split && node.children.size() == 1u) { CollapseSplitNodeToOnlyChild(node); } return true; } } return false; } float ClampDockSplitRatio(float value) { constexpr float kMinRatio = 0.1f; constexpr float kMaxRatio = 0.9f; return (std::min)(kMaxRatio, (std::max)(kMinRatio, value)); } bool IsLeadingDockPlacement(UIEditorWorkspaceDockPlacement placement) { return placement == UIEditorWorkspaceDockPlacement::Left || placement == UIEditorWorkspaceDockPlacement::Top; } UIEditorWorkspaceSplitAxis ResolveDockSplitAxis(UIEditorWorkspaceDockPlacement placement) { return placement == UIEditorWorkspaceDockPlacement::Left || placement == UIEditorWorkspaceDockPlacement::Right ? UIEditorWorkspaceSplitAxis::Horizontal : UIEditorWorkspaceSplitAxis::Vertical; } std::string MakeUniqueNodeId( const UIEditorWorkspaceModel& workspace, std::string base) { if (base.empty()) { base = "workspace-node"; } if (FindUIEditorWorkspaceNode(workspace, base) == nullptr) { return base; } for (std::size_t suffix = 1u; suffix < 1024u; ++suffix) { const std::string candidate = base + "-" + std::to_string(suffix); if (FindUIEditorWorkspaceNode(workspace, candidate) == nullptr) { return candidate; } } return base + "-overflow"; } bool TryExtractVisiblePanelFromTabStack( UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceSession& session, std::string_view sourceNodeId, std::string_view panelId, UIEditorWorkspaceNode& extractedPanel) { std::vector sourcePath = {}; if (!FindNodePathRecursive(workspace.root, sourceNodeId, sourcePath)) { return false; } UIEditorWorkspaceNode* sourceStack = ResolveMutableNodeByPath(workspace.root, sourcePath); if (sourceStack == nullptr || sourceStack->kind != UIEditorWorkspaceNodeKind::TabStack) { return false; } std::size_t panelIndex = sourceStack->children.size(); for (std::size_t index = 0; index < sourceStack->children.size(); ++index) { const UIEditorWorkspaceNode& child = sourceStack->children[index]; if (child.kind != UIEditorWorkspaceNodeKind::Panel) { return false; } if (child.panel.panelId == panelId) { if (!IsPanelOpenAndVisibleInSession(session, panelId)) { return false; } panelIndex = index; break; } } if (panelIndex >= sourceStack->children.size()) { return false; } if (sourcePath.empty() && sourceStack->children.size() == 1u) { return false; } std::string fallbackSelectedPanelId = {}; if (sourceStack->selectedTabIndex < sourceStack->children.size()) { fallbackSelectedPanelId = sourceStack->children[sourceStack->selectedTabIndex].panel.panelId; } extractedPanel = std::move(sourceStack->children[panelIndex]); sourceStack->children.erase( sourceStack->children.begin() + static_cast(panelIndex)); if (sourceStack->children.empty()) { if (sourcePath.empty()) { return false; } if (!RemoveNodeByIdRecursive(workspace.root, sourceNodeId)) { return false; } } else { if (fallbackSelectedPanelId == panelId) { const std::size_t nextIndex = (std::min)(panelIndex, sourceStack->children.size() - 1u); fallbackSelectedPanelId = sourceStack->children[nextIndex].panel.panelId; } FixTabStackSelectedIndex(*sourceStack, fallbackSelectedPanelId); } workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); return true; } bool TryActivateRecursive( UIEditorWorkspaceNode& node, std::string_view panelId) { switch (node.kind) { case UIEditorWorkspaceNodeKind::Panel: return node.panel.panelId == panelId; case UIEditorWorkspaceNodeKind::TabStack: for (std::size_t index = 0; index < node.children.size(); ++index) { UIEditorWorkspaceNode& child = node.children[index]; if (child.kind == UIEditorWorkspaceNodeKind::Panel && child.panel.panelId == panelId) { node.selectedTabIndex = index; return true; } } return false; case UIEditorWorkspaceNodeKind::Split: for (UIEditorWorkspaceNode& child : node.children) { if (TryActivateRecursive(child, panelId)) { return true; } } return false; } return false; } void CollectVisiblePanelsRecursive( const UIEditorWorkspaceNode& node, std::string_view activePanelId, std::vector& outPanels) { switch (node.kind) { case UIEditorWorkspaceNodeKind::Panel: { UIEditorWorkspaceVisiblePanel panel = {}; panel.panelId = node.panel.panelId; panel.title = node.panel.title; panel.active = node.panel.panelId == activePanelId; panel.placeholder = node.panel.placeholder; outPanels.push_back(std::move(panel)); return; } case UIEditorWorkspaceNodeKind::TabStack: if (node.selectedTabIndex < node.children.size()) { CollectVisiblePanelsRecursive( node.children[node.selectedTabIndex], activePanelId, outPanels); } return; case UIEditorWorkspaceNodeKind::Split: for (const UIEditorWorkspaceNode& child : node.children) { CollectVisiblePanelsRecursive(child, activePanelId, outPanels); } return; } } UIEditorWorkspaceValidationResult ValidateNodeRecursive( const UIEditorWorkspaceNode& node, std::unordered_set& panelIds) { if (node.nodeId.empty()) { return MakeValidationError( UIEditorWorkspaceValidationCode::EmptyNodeId, "Workspace node id must not be empty."); } switch (node.kind) { case UIEditorWorkspaceNodeKind::Panel: if (!node.children.empty()) { return MakeValidationError( UIEditorWorkspaceValidationCode::NonPanelTabChild, "Panel node '" + node.nodeId + "' must not contain child nodes."); } if (node.panel.panelId.empty()) { return MakeValidationError( UIEditorWorkspaceValidationCode::EmptyPanelId, "Panel node '" + node.nodeId + "' must define a panelId."); } if (node.panel.title.empty()) { return MakeValidationError( UIEditorWorkspaceValidationCode::EmptyPanelTitle, "Panel node '" + node.nodeId + "' must define a title."); } if (!panelIds.insert(node.panel.panelId).second) { return MakeValidationError( UIEditorWorkspaceValidationCode::DuplicatePanelId, "Panel id '" + node.panel.panelId + "' is duplicated in the workspace tree."); } return {}; case UIEditorWorkspaceNodeKind::TabStack: if (node.children.empty()) { return MakeValidationError( UIEditorWorkspaceValidationCode::EmptyTabStack, "Tab stack '" + node.nodeId + "' must contain at least one panel."); } if (node.selectedTabIndex >= node.children.size()) { return MakeValidationError( UIEditorWorkspaceValidationCode::InvalidSelectedTabIndex, "Tab stack '" + node.nodeId + "' selectedTabIndex is out of range."); } for (const UIEditorWorkspaceNode& child : node.children) { if (child.kind != UIEditorWorkspaceNodeKind::Panel) { return MakeValidationError( UIEditorWorkspaceValidationCode::NonPanelTabChild, "Tab stack '" + node.nodeId + "' may only contain panel leaf nodes."); } if (UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(child, panelIds); !result.IsValid()) { return result; } } return {}; case UIEditorWorkspaceNodeKind::Split: if (node.children.size() != 2u) { return MakeValidationError( UIEditorWorkspaceValidationCode::InvalidSplitChildCount, "Split node '" + node.nodeId + "' must contain exactly two child nodes."); } if (!IsValidSplitRatio(node.splitRatio)) { return MakeValidationError( UIEditorWorkspaceValidationCode::InvalidSplitRatio, "Split node '" + node.nodeId + "' must define a ratio in the open interval (0, 1)."); } for (const UIEditorWorkspaceNode& child : node.children) { if (UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(child, panelIds); !result.IsValid()) { return result; } } return {}; } return {}; } } // namespace bool AreUIEditorWorkspaceNodesEquivalent( const UIEditorWorkspaceNode& lhs, const UIEditorWorkspaceNode& rhs) { if (lhs.kind != rhs.kind || lhs.nodeId != rhs.nodeId || lhs.splitAxis != rhs.splitAxis || lhs.splitRatio != rhs.splitRatio || lhs.selectedTabIndex != rhs.selectedTabIndex || lhs.panel.panelId != rhs.panel.panelId || lhs.panel.title != rhs.panel.title || lhs.panel.placeholder != rhs.panel.placeholder || lhs.children.size() != rhs.children.size()) { return false; } for (std::size_t index = 0; index < lhs.children.size(); ++index) { if (!AreUIEditorWorkspaceNodesEquivalent(lhs.children[index], rhs.children[index])) { return false; } } return true; } bool AreUIEditorWorkspaceModelsEquivalent( const UIEditorWorkspaceModel& lhs, const UIEditorWorkspaceModel& rhs) { return lhs.activePanelId == rhs.activePanelId && AreUIEditorWorkspaceNodesEquivalent(lhs.root, rhs.root); } UIEditorWorkspaceModel BuildDefaultEditorShellWorkspaceModel() { const UIEditorPanelRegistry registry = BuildDefaultEditorShellPanelRegistry(); const UIEditorPanelDescriptor& rootPanel = RequirePanelDescriptor(registry, "editor-foundation-root"); UIEditorWorkspaceModel workspace = {}; workspace.root = BuildUIEditorWorkspaceSingleTabStack( "editor-foundation-root-node", rootPanel.panelId, rootPanel.defaultTitle, rootPanel.placeholder); workspace.activePanelId = rootPanel.panelId; return workspace; } UIEditorWorkspaceNode BuildUIEditorWorkspacePanel( std::string nodeId, std::string panelId, std::string title, bool placeholder) { UIEditorWorkspaceNode node = {}; node.kind = UIEditorWorkspaceNodeKind::Panel; node.nodeId = std::move(nodeId); node.panel.panelId = std::move(panelId); node.panel.title = std::move(title); node.panel.placeholder = placeholder; return node; } UIEditorWorkspaceNode BuildUIEditorWorkspaceSingleTabStack( std::string nodeId, std::string panelId, std::string title, bool placeholder) { UIEditorWorkspaceNode panel = BuildUIEditorWorkspacePanel( BuildSingleTabPanelNodeId(nodeId), std::move(panelId), std::move(title), placeholder); return BuildUIEditorWorkspaceTabStack( std::move(nodeId), { std::move(panel) }, 0u); } UIEditorWorkspaceNode BuildUIEditorWorkspaceTabStack( std::string nodeId, std::vector panels, std::size_t selectedTabIndex) { UIEditorWorkspaceNode node = {}; node.kind = UIEditorWorkspaceNodeKind::TabStack; node.nodeId = std::move(nodeId); node.selectedTabIndex = selectedTabIndex; node.children = std::move(panels); return node; } UIEditorWorkspaceNode BuildUIEditorWorkspaceSplit( std::string nodeId, UIEditorWorkspaceSplitAxis axis, float splitRatio, UIEditorWorkspaceNode primary, UIEditorWorkspaceNode secondary) { UIEditorWorkspaceNode node = {}; node.kind = UIEditorWorkspaceNodeKind::Split; node.nodeId = std::move(nodeId); node.splitAxis = axis; node.splitRatio = splitRatio; node.children.push_back(std::move(primary)); node.children.push_back(std::move(secondary)); return node; } UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace( const UIEditorWorkspaceModel& workspace) { std::unordered_set panelIds = {}; UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(workspace.root, panelIds); if (!result.IsValid()) { return result; } if (!workspace.activePanelId.empty()) { const UIEditorWorkspacePanelState* activePanel = FindUIEditorWorkspaceActivePanel(workspace); if (activePanel == nullptr) { return MakeValidationError( UIEditorWorkspaceValidationCode::InvalidActivePanelId, "Active panel id '" + workspace.activePanelId + "' is missing or hidden by the current tab selection."); } } return {}; } UIEditorWorkspaceModel CanonicalizeUIEditorWorkspaceModel( UIEditorWorkspaceModel workspace) { CanonicalizeNodeRecursive(workspace.root, false); return workspace; } std::vector CollectUIEditorWorkspaceVisiblePanels( const UIEditorWorkspaceModel& workspace) { std::vector visiblePanels = {}; CollectVisiblePanelsRecursive(workspace.root, workspace.activePanelId, visiblePanels); return visiblePanels; } bool ContainsUIEditorWorkspacePanel( const UIEditorWorkspaceModel& workspace, std::string_view panelId) { return FindPanelRecursive(workspace.root, panelId) != nullptr; } const UIEditorWorkspaceNode* FindUIEditorWorkspaceNode( const UIEditorWorkspaceModel& workspace, std::string_view nodeId) { return FindNodeRecursive(workspace.root, nodeId); } const UIEditorWorkspacePanelState* FindUIEditorWorkspaceActivePanel( const UIEditorWorkspaceModel& workspace) { if (workspace.activePanelId.empty()) { return nullptr; } std::vector visiblePanels = CollectUIEditorWorkspaceVisiblePanels(workspace); for (const UIEditorWorkspaceVisiblePanel& panel : visiblePanels) { if (panel.panelId == workspace.activePanelId) { return FindPanelRecursive(workspace.root, workspace.activePanelId); } } return nullptr; } bool TryActivateUIEditorWorkspacePanel( UIEditorWorkspaceModel& workspace, std::string_view panelId) { if (!TryActivateRecursive(workspace.root, panelId)) { return false; } workspace.activePanelId = std::string(panelId); return true; } bool TrySetUIEditorWorkspaceSplitRatio( UIEditorWorkspaceModel& workspace, std::string_view nodeId, float splitRatio) { if (!IsValidSplitRatio(splitRatio)) { return false; } UIEditorWorkspaceNode* node = FindMutableNodeRecursive(workspace.root, nodeId); if (node == nullptr || node->kind != UIEditorWorkspaceNodeKind::Split) { return false; } if (std::fabs(node->splitRatio - splitRatio) <= 0.0001f) { return false; } node->splitRatio = splitRatio; return true; } bool TryReorderUIEditorWorkspaceTab( UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceSession& session, std::string_view nodeId, std::string_view panelId, std::size_t targetVisibleInsertionIndex) { UIEditorWorkspaceNode* node = FindMutableNodeRecursive(workspace.root, nodeId); if (node == nullptr || node->kind != UIEditorWorkspaceNodeKind::TabStack) { return false; } std::vector visibleChildIndices = {}; std::vector reorderedVisibleChildren = {}; visibleChildIndices.reserve(node->children.size()); reorderedVisibleChildren.reserve(node->children.size()); std::size_t sourceVisibleIndex = node->children.size(); for (std::size_t index = 0; index < node->children.size(); ++index) { const UIEditorWorkspaceNode& child = node->children[index]; if (child.kind != UIEditorWorkspaceNodeKind::Panel) { return false; } if (!IsPanelOpenAndVisibleInSession(session, child.panel.panelId)) { continue; } if (child.panel.panelId == panelId) { sourceVisibleIndex = visibleChildIndices.size(); } visibleChildIndices.push_back(index); reorderedVisibleChildren.push_back(child); } if (sourceVisibleIndex >= reorderedVisibleChildren.size() || targetVisibleInsertionIndex > reorderedVisibleChildren.size()) { return false; } if (targetVisibleInsertionIndex == sourceVisibleIndex || targetVisibleInsertionIndex == sourceVisibleIndex + 1u) { return false; } UIEditorWorkspaceNode movedChild = std::move(reorderedVisibleChildren[sourceVisibleIndex]); reorderedVisibleChildren.erase( reorderedVisibleChildren.begin() + static_cast(sourceVisibleIndex)); std::size_t adjustedInsertionIndex = targetVisibleInsertionIndex; if (adjustedInsertionIndex > sourceVisibleIndex) { --adjustedInsertionIndex; } if (adjustedInsertionIndex > reorderedVisibleChildren.size()) { adjustedInsertionIndex = reorderedVisibleChildren.size(); } reorderedVisibleChildren.insert( reorderedVisibleChildren.begin() + static_cast(adjustedInsertionIndex), std::move(movedChild)); std::string selectedPanelId = {}; if (node->selectedTabIndex < node->children.size()) { selectedPanelId = node->children[node->selectedTabIndex].panel.panelId; } const std::vector originalChildren = node->children; std::size_t nextVisibleIndex = 0u; for (std::size_t index = 0; index < originalChildren.size(); ++index) { const UIEditorWorkspaceNode& originalChild = originalChildren[index]; if (!IsPanelOpenAndVisibleInSession(session, originalChild.panel.panelId)) { node->children[index] = originalChild; continue; } node->children[index] = reorderedVisibleChildren[nextVisibleIndex]; ++nextVisibleIndex; } for (std::size_t index = 0; index < node->children.size(); ++index) { if (node->children[index].panel.panelId == selectedPanelId) { node->selectedTabIndex = index; break; } } return true; } bool TryMoveUIEditorWorkspaceTabToStack( UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceSession& session, std::string_view sourceNodeId, std::string_view panelId, std::string_view targetNodeId, std::size_t targetVisibleInsertionIndex) { if (sourceNodeId.empty() || panelId.empty() || targetNodeId.empty()) { return false; } if (sourceNodeId == targetNodeId) { return TryReorderUIEditorWorkspaceTab( workspace, session, sourceNodeId, panelId, targetVisibleInsertionIndex); } const UIEditorWorkspaceNode* targetNode = FindUIEditorWorkspaceNode(workspace, targetNodeId); if (targetNode == nullptr || targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { return false; } if (targetVisibleInsertionIndex > CountVisibleChildren(*targetNode, session)) { return false; } UIEditorWorkspaceNode extractedPanel = {}; if (!TryExtractVisiblePanelFromTabStack( workspace, session, sourceNodeId, panelId, extractedPanel)) { return false; } UIEditorWorkspaceNode* targetStack = FindMutableNodeRecursive(workspace.root, targetNodeId); if (targetStack == nullptr || targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) { return false; } const std::size_t actualInsertionIndex = ResolveActualInsertionIndexForVisibleInsertion( *targetStack, session, targetVisibleInsertionIndex); if (actualInsertionIndex > targetStack->children.size()) { return false; } targetStack->children.insert( targetStack->children.begin() + static_cast(actualInsertionIndex), std::move(extractedPanel)); targetStack->selectedTabIndex = actualInsertionIndex; workspace.activePanelId = std::string(panelId); workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); return true; } bool TryDockUIEditorWorkspaceTabRelative( UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceSession& session, std::string_view sourceNodeId, std::string_view panelId, std::string_view targetNodeId, UIEditorWorkspaceDockPlacement placement, float splitRatio) { if (placement == UIEditorWorkspaceDockPlacement::Center) { const UIEditorWorkspaceNode* targetNode = FindUIEditorWorkspaceNode(workspace, targetNodeId); if (targetNode == nullptr || targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { return false; } return TryMoveUIEditorWorkspaceTabToStack( workspace, session, sourceNodeId, panelId, targetNodeId, CountVisibleChildren(*targetNode, session)); } if (sourceNodeId.empty() || panelId.empty() || targetNodeId.empty()) { return false; } const UIEditorWorkspaceNode* sourceNode = FindUIEditorWorkspaceNode(workspace, sourceNodeId); const UIEditorWorkspaceNode* targetNode = FindUIEditorWorkspaceNode(workspace, targetNodeId); if (sourceNode == nullptr || targetNode == nullptr || sourceNode->kind != UIEditorWorkspaceNodeKind::TabStack || targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { return false; } if (sourceNodeId == targetNodeId && sourceNode->children.size() <= 1u) { return false; } UIEditorWorkspaceNode extractedPanel = {}; if (!TryExtractVisiblePanelFromTabStack( workspace, session, sourceNodeId, panelId, extractedPanel)) { return false; } UIEditorWorkspaceNode* targetStack = FindMutableNodeRecursive(workspace.root, targetNodeId); if (targetStack == nullptr || targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) { return false; } const std::string movedStackNodeId = MakeUniqueNodeId( workspace, std::string(targetNodeId) + "__dock_" + std::string(panelId) + "_stack"); UIEditorWorkspaceNode movedStack = {}; movedStack.kind = UIEditorWorkspaceNodeKind::TabStack; movedStack.nodeId = movedStackNodeId; movedStack.selectedTabIndex = 0u; movedStack.children.push_back(std::move(extractedPanel)); UIEditorWorkspaceNode existingTarget = std::move(*targetStack); UIEditorWorkspaceNode primary = {}; UIEditorWorkspaceNode secondary = {}; if (IsLeadingDockPlacement(placement)) { primary = std::move(movedStack); secondary = std::move(existingTarget); } else { primary = std::move(existingTarget); secondary = std::move(movedStack); } const float requestedRatio = ClampDockSplitRatio(splitRatio); const float resolvedSplitRatio = IsLeadingDockPlacement(placement) ? requestedRatio : (1.0f - requestedRatio); *targetStack = BuildUIEditorWorkspaceSplit( MakeUniqueNodeId( workspace, std::string(targetNodeId) + "__dock_split"), ResolveDockSplitAxis(placement), resolvedSplitRatio, std::move(primary), std::move(secondary)); workspace.activePanelId = std::string(panelId); workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); return true; } } // namespace XCEngine::UI::Editor