Refactor editor window synchronization flow
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
Reference in New Issue
Block a user