Build XCEditor dock host workspace shell compose

This commit is contained in:
2026-04-07 02:21:43 +08:00
parent 3b2a05a098
commit 6c90bb4eca
11 changed files with 2099 additions and 17 deletions

View File

@@ -8,6 +8,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_menu_session.cpp
test_ui_editor_panel_registry.cpp
test_ui_editor_collection_primitives.cpp
test_ui_editor_dock_host.cpp
test_ui_editor_panel_chrome.cpp
test_ui_editor_panel_frame.cpp
test_ui_editor_tab_strip.cpp

View File

@@ -0,0 +1,209 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEditor/Core/UIEditorPanelRegistry.h>
#include <XCEditor/Core/UIEditorWorkspaceModel.h>
#include <XCEditor/Core/UIEditorWorkspaceSession.h>
#include <XCEditor/Widgets/UIEditorDockHost.h>
namespace {
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::TryHideUIEditorWorkspacePanel;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
using XCEngine::UI::Editor::Widgets::AppendUIEditorDockHostBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorDockHostForeground;
using XCEngine::UI::Editor::Widgets::BuildUIEditorDockHostLayout;
using XCEngine::UI::Editor::Widgets::FindUIEditorDockHostSplitterLayout;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorDockHost;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostLayout;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostMetrics;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostState;
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "doc-a", "Document A", {}, true, true, true },
{ "doc-b", "Document B", {}, true, true, true },
{ "details", "Details", {}, true, true, true },
{ "console", "Console", {}, true, true, true }
};
return registry;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-split",
UIEditorWorkspaceSplitAxis::Horizontal,
0.5f,
BuildUIEditorWorkspaceTabStack(
"document-tabs",
{
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
},
1u),
BuildUIEditorWorkspaceSplit(
"right-split",
UIEditorWorkspaceSplitAxis::Vertical,
0.6f,
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true),
BuildUIEditorWorkspacePanel("console-node", "console", "Console", true)));
workspace.activePanelId = "doc-b";
return workspace;
}
} // namespace
TEST(UIEditorDockHostTest, LayoutComposesSplitTabStackAndStandalonePanelsFromWorkspaceTree) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
registry,
workspace,
session);
ASSERT_EQ(layout.splitters.size(), 2u);
ASSERT_EQ(layout.tabStacks.size(), 1u);
ASSERT_EQ(layout.panels.size(), 2u);
const auto* rootSplitter = FindUIEditorDockHostSplitterLayout(layout, "root-split");
ASSERT_NE(rootSplitter, nullptr);
EXPECT_FLOAT_EQ(rootSplitter->splitterLayout.handleRect.x, 395.0f);
EXPECT_FLOAT_EQ(rootSplitter->splitterLayout.handleRect.width, 10.0f);
const auto& tabStack = layout.tabStacks.front();
EXPECT_EQ(tabStack.nodeId, "document-tabs");
EXPECT_EQ(tabStack.selectedPanelId, "doc-b");
ASSERT_EQ(tabStack.items.size(), 2u);
EXPECT_EQ(tabStack.items[0].panelId, "doc-a");
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");
}
TEST(UIEditorDockHostTest, HiddenBranchCollapsesAndVisibleBranchUsesFullBounds) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = BuildWorkspace();
UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "details"));
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "console"));
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
UIRect(10.0f, 20.0f, 640.0f, 480.0f),
registry,
workspace,
session);
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);
EXPECT_FLOAT_EQ(layout.tabStacks.front().bounds.height, 480.0f);
}
TEST(UIEditorDockHostTest, HitTestPrioritizesSplitterThenTabCloseThenPanelBody) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
UIEditorDockHostState state = {};
state.focused = true;
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
registry,
workspace,
session,
state);
const auto splitterHit = HitTestUIEditorDockHost(
layout,
UIPoint(396.0f, 120.0f));
EXPECT_EQ(splitterHit.kind, UIEditorDockHostHitTargetKind::SplitterHandle);
EXPECT_EQ(splitterHit.nodeId, "root-split");
ASSERT_EQ(layout.tabStacks.size(), 1u);
const auto& closeRect = layout.tabStacks.front().tabStripLayout.closeButtonRects[1];
const auto tabCloseHit = HitTestUIEditorDockHost(
layout,
UIPoint(closeRect.x + closeRect.width * 0.5f, closeRect.y + closeRect.height * 0.5f));
EXPECT_EQ(tabCloseHit.kind, UIEditorDockHostHitTargetKind::TabCloseButton);
EXPECT_EQ(tabCloseHit.nodeId, "document-tabs");
EXPECT_EQ(tabCloseHit.panelId, "doc-b");
EXPECT_EQ(tabCloseHit.index, 1u);
const auto panelBodyHit = HitTestUIEditorDockHost(
layout,
UIPoint(40.0f, 90.0f));
EXPECT_EQ(panelBodyHit.kind, UIEditorDockHostHitTargetKind::PanelBody);
EXPECT_EQ(panelBodyHit.nodeId, "document-tabs");
EXPECT_EQ(panelBodyHit.panelId, "doc-b");
}
TEST(UIEditorDockHostTest, BackgroundAndForegroundEmitStableCompositeCommands) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
UIEditorDockHostState state = {};
state.focused = true;
state.hoveredTarget = UIEditorDockHostHitTarget{
UIEditorDockHostHitTargetKind::TabCloseButton,
"document-tabs",
"doc-b",
1u
};
state.activeSplitterNodeId = "root-split";
const UIEditorDockHostLayout layout = BuildUIEditorDockHostLayout(
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
registry,
workspace,
session,
state);
UIDrawList background("DockHostBackground");
AppendUIEditorDockHostBackground(background, layout);
EXPECT_GT(background.GetCommandCount(), 10u);
EXPECT_EQ(background.GetCommands().front().type, UIDrawCommandType::FilledRect);
UIDrawList foreground("DockHostForeground");
AppendUIEditorDockHostForeground(foreground, layout);
EXPECT_GT(foreground.GetCommandCount(), 10u);
bool foundPlaceholderText = false;
for (const auto& command : foreground.GetCommands()) {
if (command.type == UIDrawCommandType::Text &&
command.text == "DockHost tab content placeholder") {
foundPlaceholderText = true;
break;
}
}
EXPECT_TRUE(foundPlaceholderText);
}

View File

@@ -14,7 +14,9 @@ using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel;
using XCEngine::UI::Editor::FindUIEditorWorkspaceActivePanel;
using XCEngine::UI::Editor::FindUIEditorWorkspaceNode;
using XCEngine::UI::Editor::TryActivateUIEditorWorkspacePanel;
using XCEngine::UI::Editor::TrySetUIEditorWorkspaceSplitRatio;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
@@ -141,3 +143,23 @@ TEST(UIEditorWorkspaceModelTest, ValidationRejectsActivePanelHiddenByCurrentTabS
const auto result = ValidateUIEditorWorkspace(workspace);
EXPECT_EQ(result.code, UIEditorWorkspaceValidationCode::InvalidActivePanelId);
}
TEST(UIEditorWorkspaceModelTest, SplitRatioMutationTargetsSplitNodeAndRejectsInvalidValues) {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-split",
UIEditorWorkspaceSplitAxis::Horizontal,
0.62f,
BuildUIEditorWorkspacePanel("left-node", "left", "Left", true),
BuildUIEditorWorkspacePanel("right-node", "right", "Right", true));
ASSERT_TRUE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 0.35f));
const auto* splitNode = FindUIEditorWorkspaceNode(workspace, "root-split");
ASSERT_NE(splitNode, nullptr);
EXPECT_FLOAT_EQ(splitNode->splitRatio, 0.35f);
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 0.35f));
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "left-node", 0.5f));
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "missing", 0.5f));
EXPECT_FALSE(TrySetUIEditorWorkspaceSplitRatio(workspace, "root-split", 1.0f));
}