diff --git a/new_editor/app/Shell/ProductShellAsset.cpp b/new_editor/app/Shell/ProductShellAsset.cpp index 08404ba4..201a3266 100644 --- a/new_editor/app/Shell/ProductShellAsset.cpp +++ b/new_editor/app/Shell/ProductShellAsset.cpp @@ -40,7 +40,7 @@ UIEditorWorkspaceModel BuildWorkspace() { "workspace-top", UIEditorWorkspaceSplitAxis::Horizontal, 0.15f, - BuildUIEditorWorkspacePanel( + BuildUIEditorWorkspaceSingleTabStack( "hierarchy-panel", "hierarchy", "Hierarchy", @@ -64,7 +64,7 @@ UIEditorWorkspaceModel BuildWorkspace() { false) }, 0u), - BuildUIEditorWorkspacePanel( + BuildUIEditorWorkspaceSingleTabStack( "inspector-panel", "inspector", "Inspector", diff --git a/new_editor/include/XCEditor/Shell/UIEditorDockHost.h b/new_editor/include/XCEditor/Shell/UIEditorDockHost.h index e8c2a87e..24bf03b9 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorDockHost.h +++ b/new_editor/include/XCEditor/Shell/UIEditorDockHost.h @@ -96,15 +96,6 @@ struct UIEditorDockHostSplitterLayout { bool active = false; }; -struct UIEditorDockHostPanelLayout { - std::string nodeId = {}; - std::string panelId = {}; - std::string title = {}; - bool active = false; - UIEditorPanelFrameState frameState = {}; - UIEditorPanelFrameLayout frameLayout = {}; -}; - struct UIEditorDockHostTabStackLayout { std::string nodeId = {}; std::string selectedPanelId = {}; @@ -119,7 +110,6 @@ struct UIEditorDockHostTabStackLayout { struct UIEditorDockHostLayout { ::XCEngine::UI::UIRect bounds = {}; std::vector splitters = {}; - std::vector panels = {}; std::vector tabStacks = {}; }; diff --git a/new_editor/include/XCEditor/Shell/UIEditorWorkspaceModel.h b/new_editor/include/XCEditor/Shell/UIEditorWorkspaceModel.h index 1043a0ee..71761e67 100644 --- a/new_editor/include/XCEditor/Shell/UIEditorWorkspaceModel.h +++ b/new_editor/include/XCEditor/Shell/UIEditorWorkspaceModel.h @@ -78,6 +78,12 @@ UIEditorWorkspaceNode BuildUIEditorWorkspacePanel( std::string title, bool placeholder = false); +UIEditorWorkspaceNode BuildUIEditorWorkspaceSingleTabStack( + std::string nodeId, + std::string panelId, + std::string title, + bool placeholder = false); + UIEditorWorkspaceNode BuildUIEditorWorkspaceTabStack( std::string nodeId, std::vector panels, @@ -93,6 +99,9 @@ UIEditorWorkspaceNode BuildUIEditorWorkspaceSplit( UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace( const UIEditorWorkspaceModel& workspace); +UIEditorWorkspaceModel CanonicalizeUIEditorWorkspaceModel( + UIEditorWorkspaceModel workspace); + std::vector CollectUIEditorWorkspaceVisiblePanels( const UIEditorWorkspaceModel& workspace); diff --git a/new_editor/src/Shell/UIEditorDockHost.cpp b/new_editor/src/Shell/UIEditorDockHost.cpp index 46074f02..1f167fd6 100644 --- a/new_editor/src/Shell/UIEditorDockHost.cpp +++ b/new_editor/src/Shell/UIEditorDockHost.cpp @@ -23,9 +23,6 @@ using ::XCEngine::UI::Layout::UITabStripMeasureItem; using ::XCEngine::UI::Layout::UILayoutAxis; using ::XCEngine::UI::Widgets::ExpandUISplitterHandleHitRect; -constexpr std::string_view kStandalonePanelActiveFooter = ""; -constexpr std::string_view kStandalonePanelInactiveFooter = ""; - struct DockMeasureResult { bool visible = false; UISize minimumSize = {}; @@ -102,17 +99,6 @@ UIEditorPanelFrameMetrics BuildTabContentFrameMetrics( return frameMetrics; } -UISize MeasurePanelMinimumSize( - const UIEditorDockHostMetrics& metrics) { - const UIEditorPanelFrameMetrics& frameMetrics = metrics.panelFrameMetrics; - return UISize( - metrics.minimumStandalonePanelBodySize.width + - ClampNonNegative(frameMetrics.contentPadding) * 2.0f, - ClampNonNegative(frameMetrics.headerHeight) + - metrics.minimumStandalonePanelBodySize.height + - ClampNonNegative(frameMetrics.contentPadding) * 2.0f); -} - UISize MeasureTabContentMinimumSize( const UIEditorDockHostMetrics& metrics) { const UIEditorPanelFrameMetrics frameMetrics = BuildTabContentFrameMetrics(metrics); @@ -205,14 +191,8 @@ DockMeasureResult MeasureNodeRecursive( const UIEditorWorkspaceSession& session, const UIEditorDockHostMetrics& metrics) { switch (node.kind) { - case UIEditorWorkspaceNodeKind::Panel: { - DockMeasureResult result = {}; - result.visible = IsPanelOpenAndVisible(session, node.panel.panelId); - if (result.visible) { - result.minimumSize = MeasurePanelMinimumSize(metrics); - } - return result; - } + case UIEditorWorkspaceNodeKind::Panel: + return {}; case UIEditorWorkspaceNodeKind::TabStack: return MeasureTabStackNode(node, panelRegistry, session, metrics); case UIEditorWorkspaceNodeKind::Split: @@ -269,38 +249,6 @@ UIEditorTabStripState BuildTabStripState( return tabState; } -UIEditorPanelFrameState BuildStandalonePanelFrameState( - const UIEditorDockHostState& state, - const UIEditorWorkspaceModel& workspace, - const UIEditorPanelDescriptor* descriptor, - const UIEditorWorkspaceNode& node) { - UIEditorPanelFrameState frameState = {}; - frameState.active = workspace.activePanelId == node.panel.panelId; - frameState.focused = state.focused && frameState.active; - frameState.closable = descriptor != nullptr ? descriptor->canClose : true; - frameState.pinnable = false; - frameState.showFooter = true; - - if (state.hoveredTarget.panelId != node.panel.panelId) { - return frameState; - } - - switch (state.hoveredTarget.kind) { - case UIEditorDockHostHitTargetKind::PanelHeader: - case UIEditorDockHostHitTargetKind::PanelBody: - case UIEditorDockHostHitTargetKind::PanelFooter: - case UIEditorDockHostHitTargetKind::PanelCloseButton: - frameState.hovered = true; - break; - default: - break; - } - - frameState.closeHovered = - state.hoveredTarget.kind == UIEditorDockHostHitTargetKind::PanelCloseButton; - return frameState; -} - UIEditorPanelFrameState BuildTabContentFrameState( const UIEditorDockHostState& state, const UIEditorWorkspaceModel& workspace, @@ -338,29 +286,6 @@ void LayoutNodeRecursive( const UIEditorDockHostMetrics& metrics, UIEditorDockHostLayout& layout); -void LayoutPanelNode( - const UIEditorWorkspaceNode& node, - const UIRect& bounds, - const UIEditorPanelRegistry& panelRegistry, - const UIEditorWorkspaceModel& workspace, - const UIEditorDockHostState& state, - const UIEditorDockHostMetrics& metrics, - UIEditorDockHostLayout& layout) { - const UIEditorPanelDescriptor* descriptor = - FindPanelDescriptor(panelRegistry, node.panel.panelId); - - UIEditorDockHostPanelLayout panelLayout = {}; - panelLayout.nodeId = node.nodeId; - panelLayout.panelId = node.panel.panelId; - panelLayout.title = node.panel.title; - panelLayout.active = workspace.activePanelId == node.panel.panelId; - panelLayout.frameState = - BuildStandalonePanelFrameState(state, workspace, descriptor, node); - panelLayout.frameLayout = - BuildUIEditorPanelFrameLayout(bounds, panelLayout.frameState, metrics.panelFrameMetrics); - layout.panels.push_back(std::move(panelLayout)); -} - void LayoutTabStackNode( const UIEditorWorkspaceNode& node, const UIRect& bounds, @@ -525,9 +450,6 @@ void LayoutNodeRecursive( UIEditorDockHostLayout& layout) { switch (node.kind) { case UIEditorWorkspaceNodeKind::Panel: - if (IsPanelOpenAndVisible(session, node.panel.panelId)) { - LayoutPanelNode(node, bounds, panelRegistry, workspace, state, metrics, layout); - } return; case UIEditorWorkspaceNodeKind::TabStack: LayoutTabStackNode(node, bounds, panelRegistry, workspace, session, state, metrics, layout); @@ -606,11 +528,13 @@ UIEditorDockHostLayout BuildUIEditorDockHostLayout( bounds.y, ClampNonNegative(bounds.width), ClampNonNegative(bounds.height)); + const UIEditorWorkspaceModel canonicalWorkspace = + CanonicalizeUIEditorWorkspaceModel(workspace); LayoutNodeRecursive( - workspace.root, + canonicalWorkspace.root, layout.bounds, panelRegistry, - workspace, + canonicalWorkspace, session, state, metrics, @@ -675,17 +599,6 @@ UIEditorDockHostHitTarget HitTestUIEditorDockHost( } } - for (std::size_t index = layout.panels.size(); index > 0u; --index) { - const UIEditorDockHostPanelLayout& panel = layout.panels[index - 1u]; - const UIEditorPanelFrameHitTarget hitTarget = HitTestUIEditorPanelFrame( - panel.frameLayout, - panel.frameState, - point); - if (hitTarget != UIEditorPanelFrameHitTarget::None) { - return MapPanelFrameHitTarget(hitTarget, panel.nodeId, panel.panelId); - } - } - return {}; } @@ -694,15 +607,6 @@ void AppendUIEditorDockHostBackground( const UIEditorDockHostLayout& layout, const UIEditorDockHostPalette& palette, const UIEditorDockHostMetrics& metrics) { - for (const UIEditorDockHostPanelLayout& panel : layout.panels) { - AppendUIEditorPanelFrameBackground( - drawList, - panel.frameLayout, - panel.frameState, - palette.panelFramePalette, - metrics.panelFrameMetrics); - } - const UIEditorPanelFrameMetrics tabContentFrameMetrics = BuildTabContentFrameMetrics(metrics); for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { @@ -744,23 +648,6 @@ void AppendUIEditorDockHostForeground( const UIEditorDockHostForegroundOptions& options, const UIEditorDockHostPalette& palette, const UIEditorDockHostMetrics& metrics) { - for (const UIEditorDockHostPanelLayout& panel : layout.panels) { - AppendUIEditorPanelFrameForeground( - drawList, - panel.frameLayout, - panel.frameState, - UIEditorPanelFrameText{ - panel.title, - {}, - panel.active ? kStandalonePanelActiveFooter : kStandalonePanelInactiveFooter - }, - palette.panelFramePalette, - metrics.panelFrameMetrics); - if (UsesExternalBodyPresentation(options, panel.panelId)) { - continue; - } - } - const UIEditorPanelFrameMetrics tabContentFrameMetrics = BuildTabContentFrameMetrics(metrics); for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { diff --git a/new_editor/src/Shell/UIEditorPanelContentHost.cpp b/new_editor/src/Shell/UIEditorPanelContentHost.cpp index d39f4db5..5dd0d853 100644 --- a/new_editor/src/Shell/UIEditorPanelContentHost.cpp +++ b/new_editor/src/Shell/UIEditorPanelContentHost.cpp @@ -12,12 +12,6 @@ namespace { const ::XCEngine::UI::UIRect* FindVisiblePanelBodyRect( const Widgets::UIEditorDockHostLayout& layout, std::string_view panelId) { - for (const Widgets::UIEditorDockHostPanelLayout& panel : layout.panels) { - if (panel.panelId == panelId) { - return &panel.frameLayout.bodyRect; - } - } - for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { if (tabStack.selectedPanelId == panelId) { return &tabStack.contentFrameLayout.bodyRect; diff --git a/new_editor/src/Shell/UIEditorWorkspaceController.cpp b/new_editor/src/Shell/UIEditorWorkspaceController.cpp index 073a2e27..33f63d09 100644 --- a/new_editor/src/Shell/UIEditorWorkspaceController.cpp +++ b/new_editor/src/Shell/UIEditorWorkspaceController.cpp @@ -75,9 +75,9 @@ UIEditorWorkspaceController::UIEditorWorkspaceController( UIEditorWorkspaceModel workspace, UIEditorWorkspaceSession session) : m_panelRegistry(std::move(panelRegistry)) - , m_baselineWorkspace(workspace) + , m_baselineWorkspace(CanonicalizeUIEditorWorkspaceModel(workspace)) , m_baselineSession(session) - , m_workspace(std::move(workspace)) + , m_workspace(m_baselineWorkspace) , m_session(std::move(session)) { } @@ -178,6 +178,10 @@ const UIEditorPanelDescriptor* UIEditorWorkspaceController::FindPanelDescriptor( 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()) { @@ -187,7 +191,7 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayou } const UIEditorWorkspaceValidationResult workspaceValidation = - ValidateUIEditorWorkspace(snapshot.workspace); + ValidateUIEditorWorkspace(canonicalSnapshot.workspace); if (!workspaceValidation.IsValid()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, @@ -195,15 +199,18 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayou } const UIEditorWorkspaceSessionValidationResult sessionValidation = - ValidateUIEditorWorkspaceSession(m_panelRegistry, snapshot.workspace, snapshot.session); + ValidateUIEditorWorkspaceSession( + m_panelRegistry, + canonicalSnapshot.workspace, + canonicalSnapshot.session); if (!sessionValidation.IsValid()) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::Rejected, "Layout session invalid: " + sessionValidation.message); } - if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, snapshot.workspace) && - AreUIEditorWorkspaceSessionsEquivalent(m_session, snapshot.session)) { + if (AreUIEditorWorkspaceModelsEquivalent(m_workspace, canonicalSnapshot.workspace) && + AreUIEditorWorkspaceSessionsEquivalent(m_session, canonicalSnapshot.session)) { return BuildLayoutOperationResult( UIEditorWorkspaceLayoutOperationStatus::NoOp, "Current state already matches the requested layout snapshot."); @@ -211,8 +218,8 @@ UIEditorWorkspaceLayoutOperationResult UIEditorWorkspaceController::RestoreLayou const UIEditorWorkspaceModel previousWorkspace = m_workspace; const UIEditorWorkspaceSession previousSession = m_session; - m_workspace = snapshot.workspace; - m_session = snapshot.session; + m_workspace = canonicalSnapshot.workspace; + m_session = canonicalSnapshot.session; const UIEditorWorkspaceControllerValidationResult validation = ValidateState(); if (!validation.IsValid()) { @@ -441,10 +448,12 @@ UIEditorWorkspaceCommandResult UIEditorWorkspaceController::Dispatch( UIEditorWorkspaceController BuildDefaultUIEditorWorkspaceController( const UIEditorPanelRegistry& panelRegistry, const UIEditorWorkspaceModel& workspace) { + const UIEditorWorkspaceModel canonicalWorkspace = + CanonicalizeUIEditorWorkspaceModel(workspace); return UIEditorWorkspaceController( panelRegistry, - workspace, - BuildDefaultUIEditorWorkspaceSession(panelRegistry, workspace)); + canonicalWorkspace, + BuildDefaultUIEditorWorkspaceSession(panelRegistry, canonicalWorkspace)); } } // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Shell/UIEditorWorkspaceLayoutPersistence.cpp b/new_editor/src/Shell/UIEditorWorkspaceLayoutPersistence.cpp index b1f17adf..3cafdc78 100644 --- a/new_editor/src/Shell/UIEditorWorkspaceLayoutPersistence.cpp +++ b/new_editor/src/Shell/UIEditorWorkspaceLayoutPersistence.cpp @@ -391,7 +391,7 @@ UIEditorWorkspaceLayoutSnapshot BuildUIEditorWorkspaceLayoutSnapshot( const UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceSession& session) { UIEditorWorkspaceLayoutSnapshot snapshot = {}; - snapshot.workspace = workspace; + snapshot.workspace = CanonicalizeUIEditorWorkspaceModel(workspace); snapshot.session = session; return snapshot; } @@ -461,6 +461,8 @@ UIEditorWorkspaceLayoutLoadResult DeserializeUIEditorWorkspaceLayoutSnapshot( return rootResult; } + snapshot.workspace = CanonicalizeUIEditorWorkspaceModel(std::move(snapshot.workspace)); + snapshot.session.panelStates.clear(); while (index < lines.size()) { UIEditorPanelSessionState state = {}; diff --git a/new_editor/src/Shell/UIEditorWorkspaceModel.cpp b/new_editor/src/Shell/UIEditorWorkspaceModel.cpp index 2d80894f..21de2318 100644 --- a/new_editor/src/Shell/UIEditorWorkspaceModel.cpp +++ b/new_editor/src/Shell/UIEditorWorkspaceModel.cpp @@ -22,6 +22,55 @@ 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 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) { + node = std::move(node.children.front()); + } +} + const UIEditorPanelDescriptor& RequirePanelDescriptor( const UIEditorPanelRegistry& registry, std::string_view panelId) { @@ -274,7 +323,7 @@ UIEditorWorkspaceModel BuildDefaultEditorShellWorkspaceModel() { RequirePanelDescriptor(registry, "editor-foundation-root"); UIEditorWorkspaceModel workspace = {}; - workspace.root = BuildUIEditorWorkspacePanel( + workspace.root = BuildUIEditorWorkspaceSingleTabStack( "editor-foundation-root-node", rootPanel.panelId, rootPanel.defaultTitle, @@ -297,6 +346,22 @@ UIEditorWorkspaceNode BuildUIEditorWorkspacePanel( 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, @@ -345,6 +410,12 @@ UIEditorWorkspaceValidationResult ValidateUIEditorWorkspace( return {}; } +UIEditorWorkspaceModel CanonicalizeUIEditorWorkspaceModel( + UIEditorWorkspaceModel workspace) { + CanonicalizeNodeRecursive(workspace.root, false); + return workspace; +} + std::vector CollectUIEditorWorkspaceVisiblePanels( const UIEditorWorkspaceModel& workspace) { std::vector visiblePanels = {}; diff --git a/tests/UI/Editor/integration/shell/dock_host_basic/main.cpp b/tests/UI/Editor/integration/shell/dock_host_basic/main.cpp index bcdba739..1cfa751c 100644 --- a/tests/UI/Editor/integration/shell/dock_host_basic/main.cpp +++ b/tests/UI/Editor/integration/shell/dock_host_basic/main.cpp @@ -586,7 +586,7 @@ private: DrawCard(drawList, m_introRect, "这个测试验证什么功能?", "只验证 DockHost 基础交互 contract,不做 editor 业务面板。"); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 72.0f), "1. 验证 splitter drag 是否只通过 DockHostInteraction + WorkspaceController 完成。", kTextPrimary, 12.0f); - drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f), "2. 验证 tab activate / tab close / standalone panel activate / panel close。", kTextPrimary, 12.0f); + drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f), "2. 验证 unified dock:tab activate / tab close / single-tab body activate。", kTextPrimary, 12.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 116.0f), "3. 验证 active panel、visible panels、split ratio 是否统一收口到 controller。", kTextPrimary, 12.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 138.0f), "4. 验证 pointer capture / release 请求是否通过 contract 明确返回。", kTextPrimary, 12.0f); drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 162.0f), "建议操作:先拖中间 splitter,再点 Document A。", kTextWeak, 11.0f); diff --git a/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp b/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp index d1bfac41..1ded79f5 100644 --- a/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp @@ -14,6 +14,7 @@ using XCEngine::UI::UIPoint; using XCEngine::UI::UIRect; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::TryHideUIEditorWorkspacePanel; @@ -62,8 +63,8 @@ UIEditorWorkspaceModel BuildWorkspace() { "right-split", UIEditorWorkspaceSplitAxis::Vertical, 0.6f, - BuildUIEditorWorkspacePanel("details-node", "details", "Details", true), - BuildUIEditorWorkspacePanel("console-node", "console", "Console", true))); + BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true), + BuildUIEditorWorkspaceSingleTabStack("console-node", "console", "Console", true))); workspace.activePanelId = "doc-b"; return workspace; } @@ -80,7 +81,7 @@ bool ContainsTextCommand(const UIDrawList& drawList, std::string_view text) { } // namespace -TEST(UIEditorDockHostTest, LayoutComposesSplitTabStackAndStandalonePanelsFromWorkspaceTree) { +TEST(UIEditorDockHostTest, LayoutComposesOnlyUnifiedTabStacksFromWorkspaceTree) { const UIEditorPanelRegistry registry = BuildPanelRegistry(); const UIEditorWorkspaceModel workspace = BuildWorkspace(); const UIEditorWorkspaceSession session = @@ -93,8 +94,7 @@ TEST(UIEditorDockHostTest, LayoutComposesSplitTabStackAndStandalonePanelsFromWor session); ASSERT_EQ(layout.splitters.size(), 2u); - ASSERT_EQ(layout.tabStacks.size(), 1u); - ASSERT_EQ(layout.panels.size(), 2u); + ASSERT_EQ(layout.tabStacks.size(), 3u); const auto* rootSplitter = FindUIEditorDockHostSplitterLayout(layout, "root-split"); ASSERT_NE(rootSplitter, nullptr); @@ -109,8 +109,10 @@ TEST(UIEditorDockHostTest, LayoutComposesSplitTabStackAndStandalonePanelsFromWor EXPECT_EQ(tabStack.items[1].panelId, "doc-b"); EXPECT_EQ(tabStack.tabStripState.selectedIndex, 1u); - EXPECT_EQ(layout.panels[0].panelId, "details"); - EXPECT_EQ(layout.panels[1].panelId, "console"); + EXPECT_EQ(layout.tabStacks[1].nodeId, "details-node"); + EXPECT_EQ(layout.tabStacks[1].selectedPanelId, "details"); + EXPECT_EQ(layout.tabStacks[2].nodeId, "console-node"); + EXPECT_EQ(layout.tabStacks[2].selectedPanelId, "console"); } TEST(UIEditorDockHostTest, HiddenBranchCollapsesAndVisibleBranchUsesFullBounds) { @@ -130,7 +132,6 @@ TEST(UIEditorDockHostTest, HiddenBranchCollapsesAndVisibleBranchUsesFullBounds) EXPECT_TRUE(layout.splitters.empty()); ASSERT_EQ(layout.tabStacks.size(), 1u); - EXPECT_TRUE(layout.panels.empty()); EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.x, 10.0f); EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.y, 20.0f); EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.width, 640.0f); @@ -158,7 +159,7 @@ TEST(UIEditorDockHostTest, HitTestPrioritizesSplitterThenTabCloseThenPanelBody) EXPECT_EQ(splitterHit.kind, UIEditorDockHostHitTargetKind::SplitterHandle); EXPECT_EQ(splitterHit.nodeId, "root-split"); - ASSERT_EQ(layout.tabStacks.size(), 1u); + ASSERT_EQ(layout.tabStacks.size(), 3u); const auto& closeRect = layout.tabStacks.front().tabStripLayout.closeButtonRects[1]; const auto tabCloseHit = HitTestUIEditorDockHost( layout, @@ -209,7 +210,7 @@ TEST(UIEditorDockHostTest, BackgroundAndForegroundEmitStableCompositeCommands) { EXPECT_GT(foreground.GetCommandCount(), 10u); } -TEST(UIEditorDockHostTest, ForegroundByDefaultStillDrawsPlaceholderText) { +TEST(UIEditorDockHostTest, ForegroundDrawsUnifiedTabTitlesAcrossAllLeafStacks) { const UIEditorPanelRegistry registry = BuildPanelRegistry(); const UIEditorWorkspaceModel workspace = BuildWorkspace(); const UIEditorWorkspaceSession session = @@ -224,11 +225,13 @@ TEST(UIEditorDockHostTest, ForegroundByDefaultStillDrawsPlaceholderText) { UIDrawList foreground("DockHostForegroundDefault"); AppendUIEditorDockHostForeground(foreground, layout); - EXPECT_TRUE(ContainsTextCommand(foreground, "DockHost tab content placeholder")); - EXPECT_TRUE(ContainsTextCommand(foreground, "DockHost standalone panel")); + EXPECT_TRUE(ContainsTextCommand(foreground, "Document A")); + EXPECT_TRUE(ContainsTextCommand(foreground, "Document B")); + EXPECT_TRUE(ContainsTextCommand(foreground, "Details")); + EXPECT_TRUE(ContainsTextCommand(foreground, "Console")); } -TEST(UIEditorDockHostTest, ForegroundSkipsPlaceholderForExternalBodyPanelId) { +TEST(UIEditorDockHostTest, ForegroundWithExternalBodyStillDrawsUnifiedTabTitles) { const UIEditorPanelRegistry registry = BuildPanelRegistry(); const UIEditorWorkspaceModel workspace = BuildWorkspace(); const UIEditorWorkspaceSession session = @@ -245,6 +248,8 @@ TEST(UIEditorDockHostTest, ForegroundSkipsPlaceholderForExternalBodyPanelId) { options.externalBodyPanelIds = { "doc-b" }; AppendUIEditorDockHostForeground(foreground, layout, options); - EXPECT_FALSE(ContainsTextCommand(foreground, "DockHost tab content placeholder")); - EXPECT_TRUE(ContainsTextCommand(foreground, "DockHost standalone panel")); + EXPECT_TRUE(ContainsTextCommand(foreground, "Document A")); + EXPECT_TRUE(ContainsTextCommand(foreground, "Document B")); + EXPECT_TRUE(ContainsTextCommand(foreground, "Details")); + EXPECT_TRUE(ContainsTextCommand(foreground, "Console")); } diff --git a/tests/UI/Editor/unit/test_ui_editor_dock_host_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_dock_host_interaction.cpp index 32fffb53..a2d58173 100644 --- a/tests/UI/Editor/unit/test_ui_editor_dock_host_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_dock_host_interaction.cpp @@ -15,6 +15,7 @@ using XCEngine::UI::UIPointerButton; using XCEngine::Input::KeyCode; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::FindUIEditorPanelSessionState; @@ -24,6 +25,8 @@ using XCEngine::UI::Editor::UIEditorWorkspaceModel; using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; using XCEngine::UI::Editor::UpdateUIEditorDockHostInteraction; using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout; +using XCEngine::UI::Editor::Widgets::UIEditorDockHostTabStackLayout; UIEditorPanelRegistry BuildPanelRegistry() { UIEditorPanelRegistry registry = {}; @@ -53,8 +56,8 @@ UIEditorWorkspaceModel BuildWorkspace() { "right-split", UIEditorWorkspaceSplitAxis::Vertical, 0.6f, - BuildUIEditorWorkspacePanel("details-node", "details", "Details", true), - BuildUIEditorWorkspacePanel("console-node", "console", "Console", true))); + BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true), + BuildUIEditorWorkspaceSingleTabStack("console-node", "console", "Console", true))); workspace.activePanelId = "doc-b"; return workspace; } @@ -99,6 +102,18 @@ UIPoint RectCenter(const UIRect& rect) { return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f); } +const UIEditorDockHostTabStackLayout* FindTabStackByNodeId( + const UIEditorDockHostLayout& layout, + std::string_view nodeId) { + for (const UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { + if (tabStack.nodeId == nodeId) { + return &tabStack; + } + } + + return nullptr; +} + } // namespace TEST(UIEditorDockHostInteractionTest, SplitterDragUpdatesWorkspaceSplitRatio) { @@ -182,8 +197,9 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) { controller, UIRect(0.0f, 0.0f, 800.0f, 600.0f), {}); - ASSERT_EQ(frame.layout.tabStacks.size(), 1u); - const UIRect docARect = frame.layout.tabStacks.front().tabStripLayout.tabHeaderRects[0]; + const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs"); + ASSERT_NE(documentStack, nullptr); + const UIRect docARect = documentStack->tabStripLayout.tabHeaderRects[0]; frame = UpdateUIEditorDockHostInteraction( state, @@ -209,8 +225,9 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) { EXPECT_TRUE(frame.result.consumed); EXPECT_TRUE(frame.result.commandExecuted); EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a"); - ASSERT_EQ(frame.layout.tabStacks.size(), 1u); - EXPECT_EQ(frame.layout.tabStacks.front().selectedPanelId, "doc-a"); + documentStack = FindTabStackByNodeId(frame.layout, "document-tabs"); + ASSERT_NE(documentStack, nullptr); + EXPECT_EQ(documentStack->selectedPanelId, "doc-a"); } TEST(UIEditorDockHostInteractionTest, ClickingTabCloseClosesPanelThroughController) { @@ -223,8 +240,9 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabCloseClosesPanelThroughControll controller, UIRect(0.0f, 0.0f, 800.0f, 600.0f), {}); - ASSERT_EQ(frame.layout.tabStacks.size(), 1u); - const UIRect closeRect = frame.layout.tabStacks.front().tabStripLayout.closeButtonRects[1]; + const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs"); + ASSERT_NE(documentStack, nullptr); + const UIRect closeRect = documentStack->tabStripLayout.closeButtonRects[1]; const UIPoint closeCenter = RectCenter(closeRect); frame = UpdateUIEditorDockHostInteraction( @@ -243,6 +261,14 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabCloseClosesPanelThroughControll EXPECT_TRUE(frame.result.consumed); EXPECT_FALSE(frame.result.commandExecuted); + frame = UpdateUIEditorDockHostInteraction( + state, + controller, + UIRect(0.0f, 0.0f, 800.0f, 600.0f), + { MakePointerDown(closeCenter.x, closeCenter.y) }); + EXPECT_TRUE(frame.result.consumed); + EXPECT_FALSE(frame.result.commandExecuted); + frame = UpdateUIEditorDockHostInteraction( state, controller, @@ -268,8 +294,9 @@ TEST(UIEditorDockHostInteractionTest, FocusedTabStripHandlesKeyboardNavigationTh controller, UIRect(0.0f, 0.0f, 800.0f, 600.0f), {}); - ASSERT_EQ(frame.layout.tabStacks.size(), 1u); - const UIRect docARect = frame.layout.tabStacks.front().tabStripLayout.tabHeaderRects[0]; + const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs"); + ASSERT_NE(documentStack, nullptr); + const UIRect docARect = documentStack->tabStripLayout.tabHeaderRects[0]; const UIPoint docACenter = RectCenter(docARect); frame = UpdateUIEditorDockHostInteraction( @@ -302,8 +329,9 @@ TEST(UIEditorDockHostInteractionTest, FocusedTabStripHandlesKeyboardNavigationTh EXPECT_TRUE(frame.result.consumed); EXPECT_TRUE(frame.result.commandExecuted); EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-b"); - ASSERT_EQ(frame.layout.tabStacks.size(), 1u); - EXPECT_EQ(frame.layout.tabStacks.front().selectedPanelId, "doc-b"); + documentStack = FindTabStackByNodeId(frame.layout, "document-tabs"); + ASSERT_NE(documentStack, nullptr); + EXPECT_EQ(documentStack->selectedPanelId, "doc-b"); } TEST(UIEditorDockHostInteractionTest, BatchedPointerMoveDownUpActivatesTabInSameUpdateCall) { @@ -316,9 +344,10 @@ TEST(UIEditorDockHostInteractionTest, BatchedPointerMoveDownUpActivatesTabInSame controller, UIRect(0.0f, 0.0f, 800.0f, 600.0f), {}); - ASSERT_EQ(frame.layout.tabStacks.size(), 1u); + const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs"); + ASSERT_NE(documentStack, nullptr); const UIPoint docACenter = - RectCenter(frame.layout.tabStacks.front().tabStripLayout.tabHeaderRects[0]); + RectCenter(documentStack->tabStripLayout.tabHeaderRects[0]); frame = UpdateUIEditorDockHostInteraction( state, @@ -332,11 +361,12 @@ TEST(UIEditorDockHostInteractionTest, BatchedPointerMoveDownUpActivatesTabInSame EXPECT_TRUE(frame.result.consumed); EXPECT_TRUE(frame.result.commandExecuted); EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-a"); - ASSERT_EQ(frame.layout.tabStacks.size(), 1u); - EXPECT_EQ(frame.layout.tabStacks.front().selectedPanelId, "doc-a"); + documentStack = FindTabStackByNodeId(frame.layout, "document-tabs"); + ASSERT_NE(documentStack, nullptr); + EXPECT_EQ(documentStack->selectedPanelId, "doc-a"); } -TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelBodyActivatesTargetPanel) { +TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackBodyActivatesTargetPanel) { auto controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); UIEditorDockHostInteractionState state = {}; @@ -346,8 +376,9 @@ TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelBodyActivatesTarget controller, UIRect(0.0f, 0.0f, 800.0f, 600.0f), {}); - ASSERT_EQ(frame.layout.panels.size(), 2u); - const UIRect detailsBodyRect = frame.layout.panels[0].frameLayout.bodyRect; + const auto* detailsStack = FindTabStackByNodeId(frame.layout, "details-node"); + ASSERT_NE(detailsStack, nullptr); + const UIRect detailsBodyRect = detailsStack->contentFrameLayout.bodyRect; const UIPoint detailsBodyCenter = RectCenter(detailsBodyRect); frame = UpdateUIEditorDockHostInteraction( @@ -368,7 +399,7 @@ TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelBodyActivatesTarget EXPECT_EQ(controller.GetWorkspace().activePanelId, "details"); } -TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelCloseClosesPanelThroughController) { +TEST(UIEditorDockHostInteractionTest, ClickingSingleTabStackTabCloseClosesPanelThroughController) { auto controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); UIEditorDockHostInteractionState state = {}; @@ -378,8 +409,9 @@ TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelCloseClosesPanelThr controller, UIRect(0.0f, 0.0f, 800.0f, 600.0f), {}); - ASSERT_EQ(frame.layout.panels.size(), 2u); - const UIRect closeRect = frame.layout.panels[1].frameLayout.closeButtonRect; + const auto* consoleStack = FindTabStackByNodeId(frame.layout, "console-node"); + ASSERT_NE(consoleStack, nullptr); + const UIRect closeRect = consoleStack->tabStripLayout.closeButtonRects[0]; const UIPoint closeCenter = RectCenter(closeRect); frame = UpdateUIEditorDockHostInteraction( @@ -387,7 +419,7 @@ TEST(UIEditorDockHostInteractionTest, ClickingStandalonePanelCloseClosesPanelThr controller, UIRect(0.0f, 0.0f, 800.0f, 600.0f), { MakePointerMove(closeCenter.x, closeCenter.y) }); - EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::PanelCloseButton); + EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::TabCloseButton); EXPECT_EQ(frame.result.hitTarget.panelId, "console"); frame = UpdateUIEditorDockHostInteraction( diff --git a/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp b/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp index 54c29bbb..835a85a5 100644 --- a/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp @@ -9,6 +9,7 @@ namespace { using XCEngine::UI::UIRect; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::CollectMountedUIEditorPanelContentHostPanelIds; @@ -50,7 +51,7 @@ UIEditorWorkspaceModel BuildWorkspace() { BuildUIEditorWorkspacePanel("console-node", "console", "Console", true) }, 1u), - BuildUIEditorWorkspacePanel("inspector-node", "inspector", "Inspector")); + BuildUIEditorWorkspaceSingleTabStack("inspector-node", "inspector", "Inspector")); workspace.activePanelId = "doc-b"; return workspace; } diff --git a/tests/UI/Editor/unit/test_ui_editor_workspace_layout_persistence.cpp b/tests/UI/Editor/unit/test_ui_editor_workspace_layout_persistence.cpp index 3ba4de1f..6383bcda 100644 --- a/tests/UI/Editor/unit/test_ui_editor_workspace_layout_persistence.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_workspace_layout_persistence.cpp @@ -14,6 +14,7 @@ using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; using XCEngine::UI::Editor::BuildUIEditorWorkspaceLayoutSnapshot; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::DeserializeUIEditorWorkspaceLayoutSnapshot; @@ -28,6 +29,7 @@ using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind; using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus; using XCEngine::UI::Editor::UIEditorWorkspaceModel; using XCEngine::UI::Editor::UIEditorWorkspaceSession; +using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind; using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; UIEditorPanelRegistry BuildPanelRegistry() { @@ -53,7 +55,7 @@ UIEditorWorkspaceModel BuildWorkspace() { BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true) }, 0u), - BuildUIEditorWorkspacePanel("details-node", "details", "Details", true)); + BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true)); workspace.activePanelId = "doc-a"; return workspace; } @@ -128,6 +130,35 @@ TEST(UIEditorWorkspaceLayoutPersistenceTest, DeserializeRejectsMissingSessionRec EXPECT_EQ(loadResult.code, UIEditorWorkspaceLayoutLoadCode::InvalidWorkspaceSession); } +TEST(UIEditorWorkspaceLayoutPersistenceTest, DeserializeUpgradesLegacyStandalonePanelLeaves) { + const UIEditorPanelRegistry registry = BuildPanelRegistry(); + const std::string legacySerialized = + "XCUI_EDITOR_WORKSPACE_LAYOUT 1\n" + "active \"doc-a\"\n" + "node_split \"root-split\" \"horizontal\" 0.66\n" + "node_tabstack \"document-tabs\" 0 2\n" + "node_panel \"doc-a-node\" \"doc-a\" \"Document A\" 1\n" + "node_panel \"doc-b-node\" \"doc-b\" \"Document B\" 1\n" + "node_panel \"details-node\" \"details\" \"Details\" 1\n" + "session \"doc-a\" 1 1\n" + "session \"doc-b\" 1 1\n" + "session \"details\" 1 1\n"; + + const auto loadResult = + DeserializeUIEditorWorkspaceLayoutSnapshot(registry, legacySerialized); + + ASSERT_TRUE(loadResult.IsValid()) << loadResult.message; + ASSERT_EQ(loadResult.snapshot.workspace.root.kind, UIEditorWorkspaceNodeKind::Split); + ASSERT_EQ(loadResult.snapshot.workspace.root.children.size(), 2u); + EXPECT_EQ( + loadResult.snapshot.workspace.root.children[1].kind, + UIEditorWorkspaceNodeKind::TabStack); + ASSERT_EQ(loadResult.snapshot.workspace.root.children[1].children.size(), 1u); + EXPECT_EQ( + loadResult.snapshot.workspace.root.children[1].children[0].panel.panelId, + "details"); +} + TEST(UIEditorWorkspaceLayoutPersistenceTest, RestoreSerializedLayoutRestoresSavedStateAfterFurtherMutations) { const UIEditorPanelRegistry registry = BuildPanelRegistry(); UIEditorWorkspaceController controller = diff --git a/tests/UI/Editor/unit/test_ui_editor_workspace_model.cpp b/tests/UI/Editor/unit/test_ui_editor_workspace_model.cpp index 3f95542d..84370cc2 100644 --- a/tests/UI/Editor/unit/test_ui_editor_workspace_model.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_workspace_model.cpp @@ -9,8 +9,10 @@ namespace { using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; +using XCEngine::UI::Editor::CanonicalizeUIEditorWorkspaceModel; using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels; using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel; using XCEngine::UI::Editor::FindUIEditorWorkspaceActivePanel; @@ -73,7 +75,7 @@ TEST(UIEditorWorkspaceModelTest, VisiblePanelsOnlyIncludeSelectedTabsAcrossSplit "root-split", UIEditorWorkspaceSplitAxis::Horizontal, 0.68f, - BuildUIEditorWorkspacePanel("left-panel-node", "left-panel", "Left Panel", true), + BuildUIEditorWorkspaceSingleTabStack("left-panel-node", "left-panel", "Left Panel", true), BuildUIEditorWorkspaceSplit( "right-split", UIEditorWorkspaceSplitAxis::Vertical, @@ -85,7 +87,11 @@ TEST(UIEditorWorkspaceModelTest, VisiblePanelsOnlyIncludeSelectedTabsAcrossSplit BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true) }, 1u), - BuildUIEditorWorkspacePanel("bottom-panel-node", "bottom-panel", "Bottom Panel", true))); + BuildUIEditorWorkspaceSingleTabStack( + "bottom-panel-node", + "bottom-panel", + "Bottom Panel", + true))); workspace.activePanelId = "doc-b"; const auto validation = ValidateUIEditorWorkspace(workspace); @@ -116,7 +122,7 @@ TEST(UIEditorWorkspaceModelTest, ActivatingHiddenPanelSelectsContainingTabAndUpd BuildUIEditorWorkspacePanel("doc-c-node", "doc-c", "Document C", true) }, 0u), - BuildUIEditorWorkspacePanel("details-node", "details", "Details", true)); + BuildUIEditorWorkspaceSingleTabStack("details-node", "details", "Details", true)); ASSERT_TRUE(ContainsUIEditorWorkspacePanel(workspace, "doc-b")); ASSERT_TRUE(TryActivateUIEditorWorkspacePanel(workspace, "doc-b")); @@ -150,8 +156,8 @@ TEST(UIEditorWorkspaceModelTest, SplitRatioMutationTargetsSplitNodeAndRejectsInv "root-split", UIEditorWorkspaceSplitAxis::Horizontal, 0.62f, - BuildUIEditorWorkspacePanel("left-node", "left", "Left", true), - BuildUIEditorWorkspacePanel("right-node", "right", "Right", true)); + BuildUIEditorWorkspaceSingleTabStack("left-node", "left", "Left", true), + BuildUIEditorWorkspaceSingleTabStack("right-node", "right", "Right", true)); ASSERT_TRUE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 0.35f)); const auto* splitNode = FindUIEditorWorkspaceNode(workspace, "root-split"); @@ -163,3 +169,29 @@ TEST(UIEditorWorkspaceModelTest, SplitRatioMutationTargetsSplitNodeAndRejectsInv EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "missing", 0.5f)); EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 1.0f)); } + +TEST(UIEditorWorkspaceModelTest, CanonicalizeWrapsStandalonePanelsIntoSingleTabStacks) { + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "root-split", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.5f, + BuildUIEditorWorkspacePanel("left-node", "left", "Left", true), + BuildUIEditorWorkspacePanel("right-node", "right", "Right", true)); + + const UIEditorWorkspaceModel canonicalWorkspace = + CanonicalizeUIEditorWorkspaceModel(workspace); + + ASSERT_EQ(canonicalWorkspace.root.kind, UIEditorWorkspaceNodeKind::Split); + ASSERT_EQ(canonicalWorkspace.root.children.size(), 2u); + EXPECT_EQ(canonicalWorkspace.root.children[0].kind, UIEditorWorkspaceNodeKind::TabStack); + EXPECT_EQ(canonicalWorkspace.root.children[0].nodeId, "left-node"); + ASSERT_EQ(canonicalWorkspace.root.children[0].children.size(), 1u); + EXPECT_EQ( + canonicalWorkspace.root.children[0].children[0].kind, + UIEditorWorkspaceNodeKind::Panel); + EXPECT_EQ( + canonicalWorkspace.root.children[0].children[0].panel.panelId, + "left"); + EXPECT_EQ(canonicalWorkspace.root.children[1].kind, UIEditorWorkspaceNodeKind::TabStack); +}