From feffa36692a0c5a6008c7a52f97d192d146e33fc Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Wed, 15 Apr 2026 11:08:18 +0800 Subject: [PATCH] refactor(new_editor/workspace): split workspace model responsibilities --- new_editor/CMakeLists.txt | 3 + .../src/Workspace/UIEditorWorkspaceModel.cpp | 505 ++---------------- .../UIEditorWorkspaceModelInternal.h | 94 ++++ .../UIEditorWorkspaceModelMutation.cpp | 395 ++++++++++++++ .../UIEditorWorkspaceModelQueries.cpp | 44 ++ .../UIEditorWorkspaceModelValidation.cpp | 28 + 6 files changed, 604 insertions(+), 465 deletions(-) create mode 100644 new_editor/src/Workspace/UIEditorWorkspaceModelInternal.h create mode 100644 new_editor/src/Workspace/UIEditorWorkspaceModelMutation.cpp create mode 100644 new_editor/src/Workspace/UIEditorWorkspaceModelQueries.cpp create mode 100644 new_editor/src/Workspace/UIEditorWorkspaceModelValidation.cpp diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 9c284a82..bcbc28f6 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -103,6 +103,9 @@ set(XCUI_EDITOR_WORKSPACE_SOURCES src/Workspace/UIEditorWorkspaceInteraction.cpp src/Workspace/UIEditorWorkspaceLayoutPersistence.cpp src/Workspace/UIEditorWorkspaceModel.cpp + src/Workspace/UIEditorWorkspaceModelMutation.cpp + src/Workspace/UIEditorWorkspaceModelQueries.cpp + src/Workspace/UIEditorWorkspaceModelValidation.cpp src/Workspace/UIEditorWorkspaceSession.cpp src/Workspace/UIEditorWorkspaceTransfer.cpp src/Workspace/UIEditorWindowWorkspaceController.cpp diff --git a/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp b/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp index 6b3d9fe8..76a07539 100644 --- a/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp +++ b/new_editor/src/Workspace/UIEditorWorkspaceModel.cpp @@ -1,14 +1,10 @@ -#include -#include -#include +#include "Workspace/UIEditorWorkspaceModelInternal.h" +#include #include -#include #include -namespace XCEngine::UI::Editor { - -namespace { +namespace XCEngine::UI::Editor::Detail { UIEditorWorkspaceValidationResult MakeValidationError( UIEditorWorkspaceValidationCode code, @@ -50,9 +46,6 @@ void CollapseSplitNodeToOnlyChild(UIEditorWorkspaceNode& node) { 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); } @@ -88,7 +81,8 @@ void CanonicalizeNodeRecursive( const UIEditorPanelDescriptor& RequirePanelDescriptor( const UIEditorPanelRegistry& registry, std::string_view panelId) { - if (const UIEditorPanelDescriptor* descriptor = FindUIEditorPanelDescriptor(registry, panelId); + if (const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(registry, panelId); descriptor != nullptr) { return *descriptor; } @@ -105,7 +99,8 @@ const UIEditorWorkspacePanelState* FindPanelRecursive( } for (const UIEditorWorkspaceNode& child : node.children) { - if (const UIEditorWorkspacePanelState* found = FindPanelRecursive(child, panelId)) { + if (const UIEditorWorkspacePanelState* found = + FindPanelRecursive(child, panelId)) { return found; } } @@ -121,7 +116,8 @@ const UIEditorWorkspaceNode* FindNodeRecursive( } for (const UIEditorWorkspaceNode& child : node.children) { - if (const UIEditorWorkspaceNode* found = FindNodeRecursive(child, nodeId)) { + if (const UIEditorWorkspaceNode* found = + FindNodeRecursive(child, nodeId)) { return found; } } @@ -137,7 +133,8 @@ UIEditorWorkspaceNode* FindMutableNodeRecursive( } for (UIEditorWorkspaceNode& child : node.children) { - if (UIEditorWorkspaceNode* found = FindMutableNodeRecursive(child, nodeId)) { + if (UIEditorWorkspaceNode* found = + FindMutableNodeRecursive(child, nodeId)) { return found; } } @@ -181,7 +178,8 @@ UIEditorWorkspaceNode* ResolveMutableNodeByPath( bool IsPanelOpenAndVisibleInSession( const UIEditorWorkspaceSession& session, std::string_view panelId) { - const UIEditorPanelSessionState* state = FindUIEditorPanelSessionState(session, panelId); + const UIEditorPanelSessionState* state = + FindUIEditorPanelSessionState(session, panelId); return state != nullptr && state->open && state->visible; } @@ -261,7 +259,8 @@ bool RemoveNodeByIdRecursive( 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)); + node.children.erase( + node.children.begin() + static_cast(index)); if (node.children.size() == 1u) { CollapseSplitNodeToOnlyChild(node); } @@ -293,7 +292,8 @@ bool IsLeadingDockPlacement(UIEditorWorkspaceDockPlacement placement) { placement == UIEditorWorkspaceDockPlacement::Top; } -UIEditorWorkspaceSplitAxis ResolveDockSplitAxis(UIEditorWorkspaceDockPlacement placement) { +UIEditorWorkspaceSplitAxis ResolveDockSplitAxis( + UIEditorWorkspaceDockPlacement placement) { return placement == UIEditorWorkspaceDockPlacement::Left || placement == UIEditorWorkspaceDockPlacement::Right ? UIEditorWorkspaceSplitAxis::Horizontal @@ -307,13 +307,13 @@ std::string MakeUniqueNodeId( base = "workspace-node"; } - if (FindUIEditorWorkspaceNode(workspace, base) == nullptr) { + if (FindNodeRecursive(workspace.root, 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) { + if (FindNodeRecursive(workspace.root, candidate) == nullptr) { return candidate; } } @@ -489,7 +489,8 @@ UIEditorWorkspaceValidationResult ValidateNodeRecursive( if (!panelIds.insert(node.panel.panelId).second) { return MakeValidationError( UIEditorWorkspaceValidationCode::DuplicatePanelId, - "Panel id '" + node.panel.panelId + "' is duplicated in the workspace tree."); + "Panel id '" + node.panel.panelId + + "' is duplicated in the workspace tree."); } return {}; @@ -511,10 +512,12 @@ UIEditorWorkspaceValidationResult ValidateNodeRecursive( if (child.kind != UIEditorWorkspaceNodeKind::Panel) { return MakeValidationError( UIEditorWorkspaceValidationCode::NonPanelTabChild, - "Tab stack '" + node.nodeId + "' may only contain panel leaf nodes."); + "Tab stack '" + node.nodeId + + "' may only contain panel leaf nodes."); } - if (UIEditorWorkspaceValidationResult result = ValidateNodeRecursive(child, panelIds); + if (UIEditorWorkspaceValidationResult result = + ValidateNodeRecursive(child, panelIds); !result.IsValid()) { return result; } @@ -526,17 +529,20 @@ UIEditorWorkspaceValidationResult ValidateNodeRecursive( if (node.children.size() != 2u) { return MakeValidationError( UIEditorWorkspaceValidationCode::InvalidSplitChildCount, - "Split node '" + node.nodeId + "' must contain exactly two child nodes."); + "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)."); + "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); + if (UIEditorWorkspaceValidationResult result = + ValidateNodeRecursive(child, panelIds); !result.IsValid()) { return result; } @@ -548,7 +554,9 @@ UIEditorWorkspaceValidationResult ValidateNodeRecursive( return {}; } -} // namespace +} // namespace XCEngine::UI::Editor::Detail + +namespace XCEngine::UI::Editor { bool AreUIEditorWorkspaceNodesEquivalent( const UIEditorWorkspaceNode& lhs, @@ -566,7 +574,9 @@ bool AreUIEditorWorkspaceNodesEquivalent( } for (std::size_t index = 0; index < lhs.children.size(); ++index) { - if (!AreUIEditorWorkspaceNodesEquivalent(lhs.children[index], rhs.children[index])) { + if (!AreUIEditorWorkspaceNodesEquivalent( + lhs.children[index], + rhs.children[index])) { return false; } } @@ -584,7 +594,7 @@ bool AreUIEditorWorkspaceModelsEquivalent( UIEditorWorkspaceModel BuildDefaultEditorShellWorkspaceModel() { const UIEditorPanelRegistry registry = BuildDefaultEditorShellPanelRegistry(); const UIEditorPanelDescriptor& rootPanel = - RequirePanelDescriptor(registry, "editor-foundation-root"); + Detail::RequirePanelDescriptor(registry, "editor-foundation-root"); UIEditorWorkspaceModel workspace = {}; workspace.root = BuildUIEditorWorkspaceSingleTabStack( @@ -616,7 +626,7 @@ UIEditorWorkspaceNode BuildUIEditorWorkspaceSingleTabStack( std::string title, bool placeholder) { UIEditorWorkspaceNode panel = BuildUIEditorWorkspacePanel( - BuildSingleTabPanelNodeId(nodeId), + Detail::BuildSingleTabPanelNodeId(nodeId), std::move(panelId), std::move(title), placeholder); @@ -654,445 +664,10 @@ UIEditorWorkspaceNode BuildUIEditorWorkspaceSplit( 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); + Detail::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 TryExtractUIEditorWorkspaceVisiblePanelNode( - UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, - std::string_view sourceNodeId, - std::string_view panelId, - UIEditorWorkspaceNode& extractedPanel) { - return TryExtractVisiblePanelFromTabStack( - workspace, - session, - sourceNodeId, - panelId, - extractedPanel); -} - -bool TryInsertUIEditorWorkspacePanelNodeToStack( - UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, - UIEditorWorkspaceNode panelNode, - std::string_view targetNodeId, - std::size_t targetVisibleInsertionIndex) { - if (targetNodeId.empty() || - panelNode.kind != UIEditorWorkspaceNodeKind::Panel || - panelNode.panel.panelId.empty()) { - return false; - } - - const UIEditorWorkspaceNode* targetNode = - FindUIEditorWorkspaceNode(workspace, targetNodeId); - if (targetNode == nullptr || - targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - if (targetVisibleInsertionIndex > CountVisibleChildren(*targetNode, session)) { - return false; - } - - UIEditorWorkspaceNode* targetStack = - FindMutableNodeRecursive(workspace.root, targetNodeId); - if (targetStack == nullptr || - targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - const std::string movedPanelId = panelNode.panel.panelId; - 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(panelNode)); - targetStack->selectedTabIndex = actualInsertionIndex; - workspace.activePanelId = movedPanelId; - workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); - return true; -} - -bool TryDockUIEditorWorkspacePanelNodeRelative( - UIEditorWorkspaceModel& workspace, - const UIEditorWorkspaceSession& session, - UIEditorWorkspaceNode panelNode, - std::string_view targetNodeId, - UIEditorWorkspaceDockPlacement placement, - float splitRatio) { - if (targetNodeId.empty() || - panelNode.kind != UIEditorWorkspaceNodeKind::Panel || - panelNode.panel.panelId.empty()) { - return false; - } - - const UIEditorWorkspaceNode* targetNode = - FindUIEditorWorkspaceNode(workspace, targetNodeId); - if (targetNode == nullptr || - targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - if (placement == UIEditorWorkspaceDockPlacement::Center) { - return TryInsertUIEditorWorkspacePanelNodeToStack( - workspace, - session, - std::move(panelNode), - targetNodeId, - CountVisibleChildren(*targetNode, session)); - } - - UIEditorWorkspaceNode* targetStack = - FindMutableNodeRecursive(workspace.root, targetNodeId); - if (targetStack == nullptr || - targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) { - return false; - } - - const std::string movedPanelId = panelNode.panel.panelId; - const std::string movedStackNodeId = MakeUniqueNodeId( - workspace, - std::string(targetNodeId) + "__dock_" + movedPanelId + "_stack"); - UIEditorWorkspaceNode movedStack = {}; - movedStack.kind = UIEditorWorkspaceNodeKind::TabStack; - movedStack.nodeId = movedStackNodeId; - movedStack.selectedTabIndex = 0u; - movedStack.children.push_back(std::move(panelNode)); - - 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 = movedPanelId; - workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); - 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; - } - - return TryInsertUIEditorWorkspacePanelNodeToStack( - workspace, - session, - std::move(extractedPanel), - targetNodeId, - targetVisibleInsertionIndex); -} - -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; - } - - return TryDockUIEditorWorkspacePanelNodeRelative( - workspace, - session, - std::move(extractedPanel), - targetNodeId, - placement, - splitRatio); -} - } // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/UIEditorWorkspaceModelInternal.h b/new_editor/src/Workspace/UIEditorWorkspaceModelInternal.h new file mode 100644 index 00000000..8f8a3b4d --- /dev/null +++ b/new_editor/src/Workspace/UIEditorWorkspaceModelInternal.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Detail { + +UIEditorWorkspaceValidationResult MakeValidationError( + UIEditorWorkspaceValidationCode code, + std::string message); + +bool IsValidSplitRatio(float value); +std::string BuildSingleTabPanelNodeId(std::string_view stackNodeId); +UIEditorWorkspaceNode WrapStandalonePanelAsTabStack(UIEditorWorkspaceNode panelNode); +void CollapseSplitNodeToOnlyChild(UIEditorWorkspaceNode& node); +void CanonicalizeNodeRecursive( + UIEditorWorkspaceNode& node, + bool allowStandalonePanelLeaf); + +const UIEditorPanelDescriptor& RequirePanelDescriptor( + const UIEditorPanelRegistry& registry, + std::string_view panelId); + +const UIEditorWorkspacePanelState* FindPanelRecursive( + const UIEditorWorkspaceNode& node, + std::string_view panelId); +const UIEditorWorkspaceNode* FindNodeRecursive( + const UIEditorWorkspaceNode& node, + std::string_view nodeId); +UIEditorWorkspaceNode* FindMutableNodeRecursive( + UIEditorWorkspaceNode& node, + std::string_view nodeId); + +bool FindNodePathRecursive( + const UIEditorWorkspaceNode& node, + std::string_view nodeId, + std::vector& path); +UIEditorWorkspaceNode* ResolveMutableNodeByPath( + UIEditorWorkspaceNode& node, + const std::vector& path); + +bool IsPanelOpenAndVisibleInSession( + const UIEditorWorkspaceSession& session, + std::string_view panelId); +std::size_t CountVisibleChildren( + const UIEditorWorkspaceNode& node, + const UIEditorWorkspaceSession& session); +std::size_t ResolveActualInsertionIndexForVisibleInsertion( + const UIEditorWorkspaceNode& node, + const UIEditorWorkspaceSession& session, + std::size_t targetVisibleInsertionIndex); + +void FixTabStackSelectedIndex( + UIEditorWorkspaceNode& node, + std::string_view preferredPanelId); +bool RemoveNodeByIdRecursive( + UIEditorWorkspaceNode& node, + std::string_view nodeId); + +float ClampDockSplitRatio(float value); +bool IsLeadingDockPlacement(UIEditorWorkspaceDockPlacement placement); +UIEditorWorkspaceSplitAxis ResolveDockSplitAxis( + UIEditorWorkspaceDockPlacement placement); +std::string MakeUniqueNodeId( + const UIEditorWorkspaceModel& workspace, + std::string base); + +bool TryExtractVisiblePanelFromTabStack( + UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + std::string_view sourceNodeId, + std::string_view panelId, + UIEditorWorkspaceNode& extractedPanel); +bool TryActivateRecursive( + UIEditorWorkspaceNode& node, + std::string_view panelId); + +void CollectVisiblePanelsRecursive( + const UIEditorWorkspaceNode& node, + std::string_view activePanelId, + std::vector& outPanels); + +UIEditorWorkspaceValidationResult ValidateNodeRecursive( + const UIEditorWorkspaceNode& node, + std::unordered_set& panelIds); + +} // namespace XCEngine::UI::Editor::Detail diff --git a/new_editor/src/Workspace/UIEditorWorkspaceModelMutation.cpp b/new_editor/src/Workspace/UIEditorWorkspaceModelMutation.cpp new file mode 100644 index 00000000..0e0e8f4e --- /dev/null +++ b/new_editor/src/Workspace/UIEditorWorkspaceModelMutation.cpp @@ -0,0 +1,395 @@ +#include "Workspace/UIEditorWorkspaceModelInternal.h" + +#include +#include +#include + +namespace XCEngine::UI::Editor { + +bool TryActivateUIEditorWorkspacePanel( + UIEditorWorkspaceModel& workspace, + std::string_view panelId) { + if (!Detail::TryActivateRecursive(workspace.root, panelId)) { + return false; + } + + workspace.activePanelId = std::string(panelId); + return true; +} + +bool TrySetUIEditorWorkspaceSplitRatio( + UIEditorWorkspaceModel& workspace, + std::string_view nodeId, + float splitRatio) { + if (!Detail::IsValidSplitRatio(splitRatio)) { + return false; + } + + UIEditorWorkspaceNode* node = + Detail::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 = + Detail::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 (!Detail::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 (!Detail::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 TryExtractUIEditorWorkspaceVisiblePanelNode( + UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + std::string_view sourceNodeId, + std::string_view panelId, + UIEditorWorkspaceNode& extractedPanel) { + return Detail::TryExtractVisiblePanelFromTabStack( + workspace, + session, + sourceNodeId, + panelId, + extractedPanel); +} + +bool TryInsertUIEditorWorkspacePanelNodeToStack( + UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + UIEditorWorkspaceNode panelNode, + std::string_view targetNodeId, + std::size_t targetVisibleInsertionIndex) { + if (targetNodeId.empty() || + panelNode.kind != UIEditorWorkspaceNodeKind::Panel || + panelNode.panel.panelId.empty()) { + return false; + } + + const UIEditorWorkspaceNode* targetNode = + FindUIEditorWorkspaceNode(workspace, targetNodeId); + if (targetNode == nullptr || + targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + if (targetVisibleInsertionIndex > + Detail::CountVisibleChildren(*targetNode, session)) { + return false; + } + + UIEditorWorkspaceNode* targetStack = + Detail::FindMutableNodeRecursive(workspace.root, targetNodeId); + if (targetStack == nullptr || + targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + const std::string movedPanelId = panelNode.panel.panelId; + const std::size_t actualInsertionIndex = + Detail::ResolveActualInsertionIndexForVisibleInsertion( + *targetStack, + session, + targetVisibleInsertionIndex); + if (actualInsertionIndex > targetStack->children.size()) { + return false; + } + + targetStack->children.insert( + targetStack->children.begin() + + static_cast(actualInsertionIndex), + std::move(panelNode)); + targetStack->selectedTabIndex = actualInsertionIndex; + workspace.activePanelId = movedPanelId; + workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); + return true; +} + +bool TryDockUIEditorWorkspacePanelNodeRelative( + UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + UIEditorWorkspaceNode panelNode, + std::string_view targetNodeId, + UIEditorWorkspaceDockPlacement placement, + float splitRatio) { + if (targetNodeId.empty() || + panelNode.kind != UIEditorWorkspaceNodeKind::Panel || + panelNode.panel.panelId.empty()) { + return false; + } + + const UIEditorWorkspaceNode* targetNode = + FindUIEditorWorkspaceNode(workspace, targetNodeId); + if (targetNode == nullptr || + targetNode->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + if (placement == UIEditorWorkspaceDockPlacement::Center) { + return TryInsertUIEditorWorkspacePanelNodeToStack( + workspace, + session, + std::move(panelNode), + targetNodeId, + Detail::CountVisibleChildren(*targetNode, session)); + } + + UIEditorWorkspaceNode* targetStack = + Detail::FindMutableNodeRecursive(workspace.root, targetNodeId); + if (targetStack == nullptr || + targetStack->kind != UIEditorWorkspaceNodeKind::TabStack) { + return false; + } + + const std::string movedPanelId = panelNode.panel.panelId; + const std::string movedStackNodeId = Detail::MakeUniqueNodeId( + workspace, + std::string(targetNodeId) + "__dock_" + movedPanelId + "_stack"); + UIEditorWorkspaceNode movedStack = {}; + movedStack.kind = UIEditorWorkspaceNodeKind::TabStack; + movedStack.nodeId = movedStackNodeId; + movedStack.selectedTabIndex = 0u; + movedStack.children.push_back(std::move(panelNode)); + + UIEditorWorkspaceNode existingTarget = std::move(*targetStack); + UIEditorWorkspaceNode primary = {}; + UIEditorWorkspaceNode secondary = {}; + if (Detail::IsLeadingDockPlacement(placement)) { + primary = std::move(movedStack); + secondary = std::move(existingTarget); + } else { + primary = std::move(existingTarget); + secondary = std::move(movedStack); + } + + const float requestedRatio = Detail::ClampDockSplitRatio(splitRatio); + const float resolvedSplitRatio = + Detail::IsLeadingDockPlacement(placement) + ? requestedRatio + : (1.0f - requestedRatio); + *targetStack = BuildUIEditorWorkspaceSplit( + Detail::MakeUniqueNodeId( + workspace, + std::string(targetNodeId) + "__dock_split"), + Detail::ResolveDockSplitAxis(placement), + resolvedSplitRatio, + std::move(primary), + std::move(secondary)); + workspace.activePanelId = movedPanelId; + workspace = CanonicalizeUIEditorWorkspaceModel(std::move(workspace)); + 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 > + Detail::CountVisibleChildren(*targetNode, session)) { + return false; + } + + UIEditorWorkspaceNode extractedPanel = {}; + if (!Detail::TryExtractVisiblePanelFromTabStack( + workspace, + session, + sourceNodeId, + panelId, + extractedPanel)) { + return false; + } + + return TryInsertUIEditorWorkspacePanelNodeToStack( + workspace, + session, + std::move(extractedPanel), + targetNodeId, + targetVisibleInsertionIndex); +} + +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, + Detail::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 (!Detail::TryExtractVisiblePanelFromTabStack( + workspace, + session, + sourceNodeId, + panelId, + extractedPanel)) { + return false; + } + + return TryDockUIEditorWorkspacePanelNodeRelative( + workspace, + session, + std::move(extractedPanel), + targetNodeId, + placement, + splitRatio); +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/UIEditorWorkspaceModelQueries.cpp b/new_editor/src/Workspace/UIEditorWorkspaceModelQueries.cpp new file mode 100644 index 00000000..6435f03a --- /dev/null +++ b/new_editor/src/Workspace/UIEditorWorkspaceModelQueries.cpp @@ -0,0 +1,44 @@ +#include "Workspace/UIEditorWorkspaceModelInternal.h" + +namespace XCEngine::UI::Editor { + +std::vector CollectUIEditorWorkspaceVisiblePanels( + const UIEditorWorkspaceModel& workspace) { + std::vector visiblePanels = {}; + Detail::CollectVisiblePanelsRecursive( + workspace.root, + workspace.activePanelId, + visiblePanels); + return visiblePanels; +} + +bool ContainsUIEditorWorkspacePanel( + const UIEditorWorkspaceModel& workspace, + std::string_view panelId) { + return Detail::FindPanelRecursive(workspace.root, panelId) != nullptr; +} + +const UIEditorWorkspaceNode* FindUIEditorWorkspaceNode( + const UIEditorWorkspaceModel& workspace, + std::string_view nodeId) { + return Detail::FindNodeRecursive(workspace.root, nodeId); +} + +const UIEditorWorkspacePanelState* FindUIEditorWorkspaceActivePanel( + const UIEditorWorkspaceModel& workspace) { + if (workspace.activePanelId.empty()) { + return nullptr; + } + + const std::vector visiblePanels = + CollectUIEditorWorkspaceVisiblePanels(workspace); + for (const UIEditorWorkspaceVisiblePanel& panel : visiblePanels) { + if (panel.panelId == workspace.activePanelId) { + return Detail::FindPanelRecursive(workspace.root, workspace.activePanelId); + } + } + + return nullptr; +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Workspace/UIEditorWorkspaceModelValidation.cpp b/new_editor/src/Workspace/UIEditorWorkspaceModelValidation.cpp new file mode 100644 index 00000000..6547ed72 --- /dev/null +++ b/new_editor/src/Workspace/UIEditorWorkspaceModelValidation.cpp @@ -0,0 +1,28 @@ +#include "Workspace/UIEditorWorkspaceModelInternal.h" + +namespace XCEngine::UI::Editor { + +UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace( + const UIEditorWorkspaceModel& workspace) { + std::unordered_set panelIds = {}; + UIEditorWorkspaceValidationResult result = + Detail::ValidateNodeRecursive(workspace.root, panelIds); + if (!result.IsValid()) { + return result; + } + + if (!workspace.activePanelId.empty()) { + const UIEditorWorkspacePanelState* activePanel = + FindUIEditorWorkspaceActivePanel(workspace); + if (activePanel == nullptr) { + return Detail::MakeValidationError( + UIEditorWorkspaceValidationCode::InvalidActivePanelId, + "Active panel id '" + workspace.activePanelId + + "' is missing or hidden by the current tab selection."); + } + } + + return {}; +} + +} // namespace XCEngine::UI::Editor