Refactor editor window synchronization flow

This commit is contained in:
2026-04-26 00:19:58 +08:00
parent 12b71a319f
commit 5b6c46d382
32 changed files with 1787 additions and 320 deletions

View File

@@ -98,6 +98,50 @@ gtest_discover_tests(editor_ui_tests
DISCOVERY_MODE PRE_TEST
)
add_executable(editor_windowing_phase1_tests
test_editor_window_synchronization_planner.cpp
)
target_link_libraries(editor_windowing_phase1_tests
PRIVATE
XCUIEditorLib
GTest::gtest_main
)
target_sources(editor_windowing_phase1_tests PRIVATE
${CMAKE_SOURCE_DIR}/editor/app/Composition/EditorWindowWorkspaceStore.cpp
${CMAKE_SOURCE_DIR}/editor/app/Windowing/System/EditorWindowPresentationPolicy.cpp
${CMAKE_SOURCE_DIR}/editor/app/Windowing/System/EditorWindowSynchronizationPlanner.cpp
${CMAKE_SOURCE_DIR}/editor/app/Windowing/System/EditorWindowSystem.cpp
)
target_include_directories(editor_windowing_phase1_tests
PRIVATE
${CMAKE_SOURCE_DIR}/editor/app
${CMAKE_SOURCE_DIR}/editor/include
${CMAKE_SOURCE_DIR}/editor/src
${CMAKE_SOURCE_DIR}/engine/include
)
if(MSVC)
target_compile_options(editor_windowing_phase1_tests PRIVATE /utf-8 /FS)
set_target_properties(editor_windowing_phase1_tests PROPERTIES
MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>"
COMPILE_PDB_NAME "editor_windowing_phase1_tests-compile"
COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb"
COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Debug"
COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Release"
COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/MinSizeRel"
COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/RelWithDebInfo"
)
set_property(TARGET editor_windowing_phase1_tests PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
gtest_discover_tests(editor_windowing_phase1_tests
DISCOVERY_MODE PRE_TEST
)
if(TARGET XCUIEditorAppLib)
set(EDITOR_APP_FEATURE_TEST_SOURCES
test_editor_project_runtime.cpp

View File

@@ -0,0 +1,391 @@
#include <gtest/gtest.h>
#include "Windowing/System/EditorWindowSystem.h"
#include <XCEditor/Workspace/UIEditorWindowWorkspaceController.h>
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
#include <XCEditor/Workspace/UIEditorWorkspaceQueries.h>
namespace {
using XCEngine::UI::Editor::App::EditorWindowHostSnapshot;
using XCEngine::UI::Editor::App::EditorWindowSynchronizationActionKind;
using XCEngine::UI::Editor::App::EditorWindowSynchronizationPlannerInput;
using XCEngine::UI::Editor::App::EditorWindowSystem;
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::FindUIEditorWindowWorkspaceState;
using XCEngine::UI::Editor::UIEditorWorkspaceCommand;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
using XCEngine::UI::Editor::UIEditorWindowWorkspaceController;
using XCEngine::UI::Editor::UIEditorWindowWorkspaceOperationStatus;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "doc-a", "Document A", {}, true, true, true },
{ "doc-b", "Document B", {}, true, true, true },
{ "inspector", "Inspector", {}, true, true, true },
};
return registry;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-split",
UIEditorWorkspaceSplitAxis::Horizontal,
0.7f,
BuildUIEditorWorkspaceTabStack(
"document-tabs",
{
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true),
},
0u),
BuildUIEditorWorkspaceSingleTabStack(
"inspector-panel",
"inspector",
"Inspector",
true));
workspace.activePanelId = "doc-a";
return workspace;
}
EditorWindowHostSnapshot BuildWorkspaceSnapshot(
std::string windowId,
bool primary,
const XCEngine::UI::Editor::UIEditorWorkspaceController& workspaceController,
std::wstring title) {
EditorWindowHostSnapshot snapshot = {};
snapshot.windowId = std::move(windowId);
snapshot.workspaceWindow = true;
snapshot.primary = primary;
snapshot.running = true;
snapshot.hasNativeWindow = true;
snapshot.hasWorkspaceProjection = true;
snapshot.workspaceState.windowId = snapshot.windowId;
snapshot.workspaceState.workspace = workspaceController.GetWorkspace();
snapshot.workspaceState.session = workspaceController.GetSession();
snapshot.title = std::move(title);
return snapshot;
}
EditorWindowSystem BuildSystem() {
return EditorWindowSystem(BuildPanelRegistry());
}
} // namespace
TEST(EditorWindowSynchronizationPlannerTest, ProducesNoActionsWhenHostAlreadyMatchesAuthoritativeWindowSet) {
EditorWindowSystem system = BuildSystem();
const auto workspaceController =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
std::string error = {};
ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error;
auto plan = system.BuildSynchronizationPlan(
EditorWindowSynchronizationPlannerInput{
.targetWindowSet = &system.GetWindowSet(),
.primaryWindowTitle = L"Main Scene - XCEngine Editor",
.hostWindows = {
BuildWorkspaceSnapshot(
"main",
true,
workspaceController,
L"Main Scene - XCEngine Editor"),
},
},
error);
ASSERT_TRUE(plan.valid) << error;
EXPECT_TRUE(plan.actions.empty());
}
TEST(EditorWindowSynchronizationPlannerTest, ProducesCreateActionForDetachedWindowMutation) {
EditorWindowSystem system = BuildSystem();
const auto workspaceController =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
std::string error = {};
ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error;
UIEditorWindowWorkspaceController mutationController =
system.BuildWorkspaceMutationController();
const auto detachResult = mutationController.DetachPanelToNewWindow(
"main",
"document-tabs",
"doc-b",
"doc-b-window");
ASSERT_EQ(detachResult.status, UIEditorWindowWorkspaceOperationStatus::Changed);
auto plan = system.BuildSynchronizationPlan(
EditorWindowSynchronizationPlannerInput{
.targetWindowSet = &mutationController.GetWindowSet(),
.primaryWindowTitle = L"Main Scene - XCEngine Editor",
.preferredNewWindowId = "doc-b-window",
.hostWindows = {
BuildWorkspaceSnapshot(
"main",
true,
workspaceController,
L"Main Scene - XCEngine Editor"),
},
},
error);
ASSERT_TRUE(plan.valid) << error;
ASSERT_EQ(plan.actions.size(), 2u);
EXPECT_EQ(plan.actions[0].kind, EditorWindowSynchronizationActionKind::UpdateWorkspaceWindow);
EXPECT_EQ(plan.actions[1].kind, EditorWindowSynchronizationActionKind::CreateWorkspaceWindow);
EXPECT_EQ(plan.actions[1].create.windowState.windowId, "doc-b-window");
}
TEST(EditorWindowSynchronizationPlannerTest, ProducesCloseActionForRemovedDetachedWindow) {
EditorWindowSystem system = BuildSystem();
const auto workspaceController =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
std::string error = {};
ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error;
UIEditorWindowWorkspaceController mutationController =
system.BuildWorkspaceMutationController();
ASSERT_EQ(
mutationController.DetachPanelToNewWindow(
"main",
"document-tabs",
"doc-b",
"doc-b-window").status,
UIEditorWindowWorkspaceOperationStatus::Changed);
const auto* detachedWindow =
FindUIEditorWindowWorkspaceState(mutationController.GetWindowSet(), "doc-b-window");
ASSERT_NE(detachedWindow, nullptr);
auto plan = system.BuildSynchronizationPlan(
EditorWindowSynchronizationPlannerInput{
.targetWindowSet = &system.GetWindowSet(),
.primaryWindowTitle = L"Main Scene - XCEngine Editor",
.hostWindows = {
BuildWorkspaceSnapshot(
"main",
true,
workspaceController,
L"Main Scene - XCEngine Editor"),
EditorWindowHostSnapshot{
.windowId = "doc-b-window",
.workspaceWindow = true,
.primary = false,
.running = true,
.hasNativeWindow = true,
.hasWorkspaceProjection = true,
.workspaceState = *detachedWindow,
.title = L"Document B - XCEngine Editor",
},
},
},
error);
ASSERT_TRUE(plan.valid) << error;
ASSERT_EQ(plan.actions.size(), 1u);
EXPECT_EQ(plan.actions[0].kind, EditorWindowSynchronizationActionKind::CloseWorkspaceWindow);
EXPECT_EQ(plan.actions[0].close.windowId, "doc-b-window");
}
TEST(EditorWindowSynchronizationPlannerTest, ProducesUpdateActionWhenPrimaryWindowChanges) {
EditorWindowSystem system = BuildSystem();
const auto workspaceController =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
std::string error = {};
ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error;
UIEditorWindowWorkspaceController mutationController =
system.BuildWorkspaceMutationController();
ASSERT_EQ(
mutationController.DetachPanelToNewWindow(
"main",
"document-tabs",
"doc-b",
"doc-b-window").status,
UIEditorWindowWorkspaceOperationStatus::Changed);
auto nextWindowSet = mutationController.GetWindowSet();
nextWindowSet.primaryWindowId = "doc-b-window";
nextWindowSet.activeWindowId = "doc-b-window";
const auto* mainWindow = FindUIEditorWindowWorkspaceState(nextWindowSet, "main");
const auto* detachedWindow = FindUIEditorWindowWorkspaceState(nextWindowSet, "doc-b-window");
ASSERT_NE(mainWindow, nullptr);
ASSERT_NE(detachedWindow, nullptr);
auto plan = system.BuildSynchronizationPlan(
EditorWindowSynchronizationPlannerInput{
.targetWindowSet = &nextWindowSet,
.primaryWindowTitle = L"Main Scene - XCEngine Editor",
.hostWindows = {
EditorWindowHostSnapshot{
.windowId = "main",
.workspaceWindow = true,
.primary = true,
.running = true,
.hasNativeWindow = true,
.hasWorkspaceProjection = true,
.workspaceState = *mainWindow,
.title = L"Main Scene - XCEngine Editor",
},
EditorWindowHostSnapshot{
.windowId = "doc-b-window",
.workspaceWindow = true,
.primary = false,
.running = true,
.hasNativeWindow = true,
.hasWorkspaceProjection = true,
.workspaceState = *detachedWindow,
.title = L"Document B - XCEngine Editor",
},
},
},
error);
ASSERT_TRUE(plan.valid) << error;
ASSERT_EQ(plan.actions.size(), 2u);
EXPECT_EQ(plan.actions[0].kind, EditorWindowSynchronizationActionKind::UpdateWorkspaceWindow);
EXPECT_EQ(plan.actions[0].update.windowState.windowId, "main");
EXPECT_FALSE(plan.actions[0].update.primary);
EXPECT_EQ(plan.actions[1].kind, EditorWindowSynchronizationActionKind::UpdateWorkspaceWindow);
EXPECT_EQ(plan.actions[1].update.windowState.windowId, "doc-b-window");
EXPECT_TRUE(plan.actions[1].update.primary);
EXPECT_EQ(plan.actions[1].update.title, L"Main Scene - XCEngine Editor");
}
TEST(EditorWindowSynchronizationPlannerTest, RejectsInvalidTargetWindowSet) {
EditorWindowSystem system = BuildSystem();
XCEngine::UI::Editor::UIEditorWindowWorkspaceSet invalidWindowSet = {};
invalidWindowSet.primaryWindowId = "main";
invalidWindowSet.activeWindowId = "main";
std::string error = {};
auto plan = system.BuildSynchronizationPlan(
EditorWindowSynchronizationPlannerInput{
.targetWindowSet = &invalidWindowSet,
.primaryWindowTitle = L"Main Scene - XCEngine Editor",
},
error);
EXPECT_FALSE(plan.valid);
EXPECT_FALSE(error.empty());
}
TEST(EditorWindowSynchronizationPlannerTest, LiveWindowMutationBuildsCommitPlanWithoutPlatformWriteback) {
EditorWindowSystem system = BuildSystem();
const auto workspaceController =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
std::string error = {};
ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error;
auto liveController = workspaceController;
const auto commandResult = liveController.Dispatch(
UIEditorWorkspaceCommand{
.kind = UIEditorWorkspaceCommandKind::ActivatePanel,
.panelId = "doc-b",
});
ASSERT_EQ(commandResult.status, UIEditorWorkspaceCommandStatus::Changed);
auto plan = system.BuildPlanForLiveWindowMutation(
"main",
liveController,
{
BuildWorkspaceSnapshot(
"main",
true,
liveController,
L"Main Scene - XCEngine Editor"),
},
L"Main Scene - XCEngine Editor",
error);
ASSERT_TRUE(plan.valid) << error;
EXPECT_TRUE(plan.actions.empty());
const auto* mutatedState = FindUIEditorWindowWorkspaceState(plan.targetWindowSet, "main");
ASSERT_NE(mutatedState, nullptr);
EXPECT_EQ(mutatedState->workspace.activePanelId, "doc-b");
ASSERT_TRUE(system.CommitSynchronizationPlan(plan, error)) << error;
EXPECT_EQ(system.GetWindowSet().windows.front().workspace.activePanelId, "doc-b");
}
TEST(EditorWindowSynchronizationPlannerTest, DestroyedPrimaryWindowProducesCloseActionsForRemainingDetachedWindows) {
EditorWindowSystem system = BuildSystem();
const auto workspaceController =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
std::string error = {};
ASSERT_TRUE(system.BootstrapPrimaryWindow("main", workspaceController, error)) << error;
UIEditorWindowWorkspaceController mutationController =
system.BuildWorkspaceMutationController();
ASSERT_EQ(
mutationController.DetachPanelToNewWindow(
"main",
"document-tabs",
"doc-b",
"doc-b-window").status,
UIEditorWindowWorkspaceOperationStatus::Changed);
auto detachedPlan = system.BuildPlanForWindowSet(
mutationController.GetWindowSet(),
{
BuildWorkspaceSnapshot(
"main",
true,
workspaceController,
L"Main Scene - XCEngine Editor"),
},
L"Main Scene - XCEngine Editor",
"doc-b-window",
{},
error);
ASSERT_TRUE(detachedPlan.valid) << error;
ASSERT_TRUE(system.CommitSynchronizationPlan(detachedPlan, error)) << error;
const auto* detachedState =
FindUIEditorWindowWorkspaceState(system.GetWindowSet(), "doc-b-window");
ASSERT_NE(detachedState, nullptr);
auto plan = system.BuildPlanForDestroyedWindow(
"main",
{
EditorWindowHostSnapshot{
.windowId = "main",
.workspaceWindow = true,
.primary = true,
.running = false,
.destroyed = true,
.hasNativeWindow = false,
},
EditorWindowHostSnapshot{
.windowId = "doc-b-window",
.workspaceWindow = true,
.primary = false,
.running = true,
.destroyed = false,
.hasNativeWindow = true,
.hasWorkspaceProjection = true,
.workspaceState = *detachedState,
.title = L"Document B - XCEngine Editor",
},
},
L"Main Scene - XCEngine Editor",
error);
ASSERT_TRUE(plan.valid) << error;
ASSERT_EQ(plan.actions.size(), 1u);
EXPECT_EQ(plan.actions[0].kind, EditorWindowSynchronizationActionKind::CloseWorkspaceWindow);
EXPECT_EQ(plan.actions[0].close.windowId, "doc-b-window");
EXPECT_TRUE(plan.targetWindowSet.windows.empty());
ASSERT_TRUE(system.CommitSynchronizationPlan(plan, error)) << error;
EXPECT_TRUE(system.GetWindowSet().windows.empty());
}