Build XCEditor workspace viewport compose foundation

This commit is contained in:
2026-04-07 06:14:58 +08:00
parent 044240d2f1
commit 3c0dedcc5f
15 changed files with 1809 additions and 16 deletions

View File

@@ -18,6 +18,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_viewport_input_bridge.cpp
test_ui_editor_viewport_shell.cpp
test_ui_editor_viewport_slot.cpp
test_ui_editor_workspace_compose.cpp
test_ui_editor_shortcut_manager.cpp
test_ui_editor_workspace_controller.cpp
test_ui_editor_workspace_layout_persistence.cpp

View File

@@ -30,6 +30,7 @@ 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::UIEditorDockHostForegroundOptions;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostState;
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
@@ -67,6 +68,16 @@ UIEditorWorkspaceModel BuildWorkspace() {
return workspace;
}
bool ContainsTextCommand(const UIDrawList& drawList, std::string_view text) {
for (const auto& command : drawList.GetCommands()) {
if (command.type == UIDrawCommandType::Text && command.text == text) {
return true;
}
}
return false;
}
} // namespace
TEST(UIEditorDockHostTest, LayoutComposesSplitTabStackAndStandalonePanelsFromWorkspaceTree) {
@@ -196,14 +207,44 @@ TEST(UIEditorDockHostTest, BackgroundAndForegroundEmitStableCompositeCommands) {
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);
}
TEST(UIEditorDockHostTest, ForegroundByDefaultStillDrawsPlaceholderText) {
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);
UIDrawList foreground("DockHostForegroundDefault");
AppendUIEditorDockHostForeground(foreground, layout);
EXPECT_TRUE(ContainsTextCommand(foreground, "DockHost tab content placeholder"));
EXPECT_TRUE(ContainsTextCommand(foreground, "DockHost standalone panel"));
}
TEST(UIEditorDockHostTest, ForegroundSkipsPlaceholderForExternalBodyPanelId) {
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);
UIDrawList foreground("DockHostForegroundExternalBody");
UIEditorDockHostForegroundOptions options = {};
options.externalBodyPanelIds = { "doc-b" };
AppendUIEditorDockHostForeground(foreground, layout, options);
EXPECT_FALSE(ContainsTextCommand(foreground, "DockHost tab content placeholder"));
EXPECT_TRUE(ContainsTextCommand(foreground, "DockHost standalone panel"));
}

View File

@@ -0,0 +1,269 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorWorkspaceCompose.h>
namespace {
using XCEngine::UI::UIRect;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::FindUIEditorWorkspacePanelPresentationState;
using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationFrame;
using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationRequest;
using XCEngine::UI::Editor::ResolveUIEditorViewportShellRequest;
using XCEngine::UI::Editor::ResolveUIEditorWorkspaceComposeRequest;
using XCEngine::UI::Editor::UIEditorPanelPresentationKind;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
using XCEngine::UI::Editor::UIEditorWorkspaceComposeFrame;
using XCEngine::UI::Editor::UIEditorWorkspaceComposeState;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
using XCEngine::UI::Editor::UpdateUIEditorWorkspaceCompose;
UIEditorPanelRegistry BuildRegistryWithViewportPanels() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "viewport", "Viewport", UIEditorPanelPresentationKind::ViewportShell, false, true, true },
{ "doc", "Document", UIEditorPanelPresentationKind::Placeholder, true, true, true },
{ "details", "Details", UIEditorPanelPresentationKind::Placeholder, true, true, true }
};
return registry;
}
UIEditorPanelRegistry BuildRegistryWithTwoViewportTabs() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "viewport-a", "Viewport A", UIEditorPanelPresentationKind::ViewportShell, false, true, true },
{ "viewport-b", "Viewport B", UIEditorPanelPresentationKind::ViewportShell, false, true, true }
};
return registry;
}
UIEditorWorkspaceModel BuildViewportWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root",
UIEditorWorkspaceSplitAxis::Horizontal,
0.7f,
BuildUIEditorWorkspaceTabStack(
"tab-stack",
{
BuildUIEditorWorkspacePanel("viewport-node", "viewport", "Viewport"),
BuildUIEditorWorkspacePanel("doc-node", "doc", "Document", true)
},
0u),
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
workspace.activePanelId = "viewport";
return workspace;
}
UIEditorWorkspaceModel BuildTwoViewportTabWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceTabStack(
"tab-stack",
{
BuildUIEditorWorkspacePanel("viewport-a-node", "viewport-a", "Viewport A"),
BuildUIEditorWorkspacePanel("viewport-b-node", "viewport-b", "Viewport B")
},
1u);
workspace.activePanelId = "viewport-b";
return workspace;
}
XCEngine::UI::Editor::UIEditorViewportShellModel BuildViewportShellModel(std::string title) {
XCEngine::UI::Editor::UIEditorViewportShellModel model = {};
model.spec.chrome.title = std::move(title);
model.spec.chrome.subtitle = "Compose";
model.spec.chrome.showTopBar = true;
model.spec.chrome.showBottomBar = true;
model.frame.hasTexture = false;
model.frame.statusText = "Viewport shell";
return model;
}
UIEditorWorkspacePanelPresentationModel BuildViewportPresentationModel(
std::string panelId,
std::string title) {
UIEditorWorkspacePanelPresentationModel model = {};
model.panelId = std::move(panelId);
model.kind = UIEditorPanelPresentationKind::ViewportShell;
model.viewportShellModel = BuildViewportShellModel(std::move(title));
return model;
}
} // namespace
TEST(UIEditorWorkspaceComposeTest, ResolveRequestMapsViewportPresentationToVisiblePanelBodyRect) {
const auto registry = BuildRegistryWithViewportPanels();
const UIEditorWorkspaceModel workspace = BuildViewportWorkspace();
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
const std::vector<UIEditorWorkspacePanelPresentationModel> presentationModels = {
BuildViewportPresentationModel("viewport", "Viewport")
};
const auto request = ResolveUIEditorWorkspaceComposeRequest(
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
registry,
workspace,
session,
presentationModels);
ASSERT_EQ(request.viewportRequests.size(), 1u);
const auto* viewportRequest =
FindUIEditorWorkspaceViewportPresentationRequest(request, "viewport");
ASSERT_NE(viewportRequest, nullptr);
ASSERT_EQ(request.dockHostLayout.tabStacks.size(), 1u);
const UIRect expectedBodyRect =
request.dockHostLayout.tabStacks.front().contentFrameLayout.bodyRect;
EXPECT_FLOAT_EQ(viewportRequest->bounds.x, expectedBodyRect.x);
EXPECT_FLOAT_EQ(viewportRequest->bounds.y, expectedBodyRect.y);
EXPECT_FLOAT_EQ(viewportRequest->bounds.width, expectedBodyRect.width);
EXPECT_FLOAT_EQ(viewportRequest->bounds.height, expectedBodyRect.height);
const auto expectedShellRequest = ResolveUIEditorViewportShellRequest(
expectedBodyRect,
presentationModels.front().viewportShellModel.spec);
EXPECT_FLOAT_EQ(
viewportRequest->viewportShellRequest.requestedViewportSize.width,
expectedShellRequest.requestedViewportSize.width);
EXPECT_FLOAT_EQ(
viewportRequest->viewportShellRequest.requestedViewportSize.height,
expectedShellRequest.requestedViewportSize.height);
}
TEST(UIEditorWorkspaceComposeTest, UpdateComposeOnlyBuildsFrameForSelectedViewportTab) {
const auto registry = BuildRegistryWithTwoViewportTabs();
const UIEditorWorkspaceModel workspace = BuildTwoViewportTabWorkspace();
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
const std::vector<UIEditorWorkspacePanelPresentationModel> presentationModels = {
BuildViewportPresentationModel("viewport-a", "Viewport A"),
BuildViewportPresentationModel("viewport-b", "Viewport B")
};
UIEditorWorkspaceComposeState state = {};
const UIEditorWorkspaceComposeFrame frame = UpdateUIEditorWorkspaceCompose(
state,
UIRect(0.0f, 0.0f, 960.0f, 640.0f),
registry,
workspace,
session,
presentationModels,
{});
ASSERT_EQ(frame.viewportFrames.size(), 1u);
EXPECT_EQ(frame.viewportFrames.front().panelId, "viewport-b");
EXPECT_EQ(
FindUIEditorWorkspaceViewportPresentationFrame(frame, "viewport-a"),
nullptr);
EXPECT_NE(
FindUIEditorWorkspaceViewportPresentationFrame(frame, "viewport-b"),
nullptr);
}
TEST(UIEditorWorkspaceComposeTest, PlaceholderPanelsDoNotGenerateExternalViewportFrames) {
const auto registry = BuildRegistryWithViewportPanels();
const UIEditorWorkspaceModel workspace = BuildViewportWorkspace();
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
std::vector<UIEditorWorkspacePanelPresentationModel> presentationModels = {
BuildViewportPresentationModel("details", "Details")
};
UIEditorWorkspaceComposeState state = {};
const auto request = ResolveUIEditorWorkspaceComposeRequest(
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
registry,
workspace,
session,
presentationModels);
EXPECT_TRUE(request.viewportRequests.empty());
const UIEditorWorkspaceComposeFrame frame = UpdateUIEditorWorkspaceCompose(
state,
UIRect(0.0f, 0.0f, 1280.0f, 720.0f),
registry,
workspace,
session,
presentationModels,
{});
EXPECT_TRUE(frame.viewportFrames.empty());
}
TEST(UIEditorWorkspaceComposeTest, HiddenViewportTabResetsCapturedAndFocusedState) {
const auto registry = BuildRegistryWithTwoViewportTabs();
UIEditorWorkspaceModel workspace = BuildTwoViewportTabWorkspace();
const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
const std::vector<UIEditorWorkspacePanelPresentationModel> presentationModels = {
BuildViewportPresentationModel("viewport-a", "Viewport A"),
BuildViewportPresentationModel("viewport-b", "Viewport B")
};
const auto initialRequest = ResolveUIEditorWorkspaceComposeRequest(
UIRect(0.0f, 0.0f, 960.0f, 640.0f),
registry,
workspace,
session,
presentationModels);
ASSERT_EQ(initialRequest.viewportRequests.size(), 1u);
const auto* selectedViewportRequest =
FindUIEditorWorkspaceViewportPresentationRequest(initialRequest, "viewport-b");
ASSERT_NE(selectedViewportRequest, nullptr);
const UIRect bounds = selectedViewportRequest->bounds;
const UIPoint center(
bounds.x + bounds.width * 0.5f,
bounds.y + bounds.height * 0.5f);
const std::vector<UIInputEvent> inputEvents = {
[] (const UIPoint& point) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerButtonDown;
event.pointerButton = UIPointerButton::Left;
event.position = point;
return event;
}(center)
};
UIEditorWorkspaceComposeState state = {};
UpdateUIEditorWorkspaceCompose(
state,
UIRect(0.0f, 0.0f, 960.0f, 640.0f),
registry,
workspace,
session,
presentationModels,
inputEvents);
const auto* viewportBStateBeforeHide =
FindUIEditorWorkspacePanelPresentationState(state, "viewport-b");
ASSERT_NE(viewportBStateBeforeHide, nullptr);
EXPECT_TRUE(viewportBStateBeforeHide->viewportShellState.inputBridgeState.focused);
EXPECT_TRUE(viewportBStateBeforeHide->viewportShellState.inputBridgeState.captured);
workspace.root.selectedTabIndex = 0u;
workspace.activePanelId = "viewport-a";
const UIEditorWorkspaceComposeFrame frame = UpdateUIEditorWorkspaceCompose(
state,
UIRect(0.0f, 0.0f, 960.0f, 640.0f),
registry,
workspace,
session,
presentationModels,
{});
ASSERT_EQ(frame.viewportFrames.size(), 1u);
EXPECT_EQ(frame.viewportFrames.front().panelId, "viewport-a");
const auto* viewportBStateAfterHide =
FindUIEditorWorkspacePanelPresentationState(state, "viewport-b");
ASSERT_NE(viewportBStateAfterHide, nullptr);
EXPECT_FALSE(viewportBStateAfterHide->viewportShellState.inputBridgeState.focused);
EXPECT_FALSE(viewportBStateAfterHide->viewportShellState.inputBridgeState.captured);
}