Lay groundwork for detached editor windows

This commit is contained in:
2026-04-14 15:07:52 +08:00
parent 804e5138d7
commit 3f871a4f45
21 changed files with 1820 additions and 97 deletions

View File

@@ -61,6 +61,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_workspace_layout_persistence.cpp
test_ui_editor_workspace_model.cpp
test_ui_editor_workspace_session.cpp
test_ui_editor_window_workspace_controller.cpp
)
add_executable(editor_ui_tests ${EDITOR_UI_UNIT_TEST_SOURCES})
@@ -88,7 +89,6 @@ if(MSVC)
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"
VS_GLOBAL_UseMultiToolTask "false"
)
set_property(TARGET editor_ui_tests PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")

View File

@@ -352,6 +352,57 @@ TEST(UIEditorDockHostInteractionTest, ClickingTabActivatesTargetPanel) {
EXPECT_EQ(documentStack->selectedPanelId, "doc-a");
}
TEST(UIEditorDockHostInteractionTest, ReleasingActiveTabDragOutsideDockHostRequestsDetach) {
auto controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildThreeDocumentWorkspace());
UIEditorDockHostInteractionState state = {};
auto frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{});
const auto* documentStack = FindTabStackByNodeId(frame.layout, "document-tabs");
ASSERT_NE(documentStack, nullptr);
const UIRect draggedTabRect = documentStack->tabStripLayout.tabHeaderRects[2];
const UIPoint draggedTabCenter = RectCenter(draggedTabRect);
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerMove(draggedTabCenter.x, draggedTabCenter.y) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorDockHostHitTargetKind::Tab);
EXPECT_EQ(frame.result.hitTarget.panelId, "doc-c");
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerDown(draggedTabCenter.x, draggedTabCenter.y) });
EXPECT_TRUE(frame.result.consumed);
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerMove(860.0f, draggedTabCenter.y) });
EXPECT_EQ(state.activeTabDragPanelId, "doc-c");
frame = UpdateUIEditorDockHostInteraction(
state,
controller,
UIRect(0.0f, 0.0f, 800.0f, 600.0f),
{ MakePointerUp(860.0f, draggedTabCenter.y) });
EXPECT_TRUE(frame.result.detachRequested);
EXPECT_TRUE(frame.result.consumed);
EXPECT_EQ(frame.result.detachedNodeId, "document-tabs");
EXPECT_EQ(frame.result.detachedPanelId, "doc-c");
EXPECT_TRUE(state.activeTabDragNodeId.empty());
EXPECT_TRUE(state.activeTabDragPanelId.empty());
EXPECT_FALSE(state.dockHostState.dropPreview.visible);
}
TEST(UIEditorDockHostInteractionTest, FocusedTabStripHandlesKeyboardNavigationThroughTabStripInteraction) {
auto controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());

View File

@@ -0,0 +1,173 @@
#include <gtest/gtest.h>
#include <XCEditor/Shell/UIEditorWindowWorkspaceController.h>
namespace {
using XCEngine::UI::Editor::BuildDefaultUIEditorWindowWorkspaceController;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSingleTabStack;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
using XCEngine::UI::Editor::ContainsUIEditorWorkspacePanel;
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
using XCEngine::UI::Editor::FindUIEditorWindowWorkspaceState;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
using XCEngine::UI::Editor::UIEditorWindowWorkspaceController;
using XCEngine::UI::Editor::UIEditorWindowWorkspaceOperationStatus;
using XCEngine::UI::Editor::UIEditorWorkspaceDockPlacement;
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;
}
std::vector<std::string> CollectVisiblePanelIds(
const UIEditorWorkspaceModel& workspace,
const XCEngine::UI::Editor::UIEditorWorkspaceSession& session) {
const auto panels = CollectUIEditorWorkspaceVisiblePanels(workspace, session);
std::vector<std::string> ids = {};
ids.reserve(panels.size());
for (const auto& panel : panels) {
ids.push_back(panel.panelId);
}
return ids;
}
} // namespace
TEST(UIEditorWindowWorkspaceControllerTest, DetachPanelCreatesNewDetachedWindowAndMovesSessionState) {
UIEditorWindowWorkspaceController controller =
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
const auto result = controller.DetachPanelToNewWindow(
"main-window",
"document-tabs",
"doc-b",
"doc-b-window");
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
EXPECT_EQ(result.targetWindowId, "doc-b-window");
EXPECT_EQ(result.activeWindowId, "doc-b-window");
ASSERT_EQ(result.windowIds.size(), 2u);
const auto* mainWindow =
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
ASSERT_NE(mainWindow, nullptr);
EXPECT_FALSE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
EXPECT_EQ(FindUIEditorPanelSessionState(mainWindow->session, "doc-b"), nullptr);
const auto* detachedWindow =
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "doc-b-window");
ASSERT_NE(detachedWindow, nullptr);
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(detachedWindow->workspace, "doc-b"));
ASSERT_NE(FindUIEditorPanelSessionState(detachedWindow->session, "doc-b"), nullptr);
EXPECT_EQ(detachedWindow->workspace.activePanelId, "doc-b");
const auto mainVisibleIds =
CollectVisiblePanelIds(mainWindow->workspace, mainWindow->session);
ASSERT_EQ(mainVisibleIds.size(), 2u);
EXPECT_EQ(mainVisibleIds[0], "doc-a");
EXPECT_EQ(mainVisibleIds[1], "inspector");
}
TEST(UIEditorWindowWorkspaceControllerTest, MovingSinglePanelDetachedWindowBackToMainClosesSourceWindow) {
UIEditorWindowWorkspaceController controller =
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
ASSERT_EQ(
controller.DetachPanelToNewWindow(
"main-window",
"document-tabs",
"doc-b",
"doc-b-window").status,
UIEditorWindowWorkspaceOperationStatus::Changed);
const auto result = controller.MovePanelToStack(
"doc-b-window",
"doc-b-window-root",
"doc-b",
"main-window",
"document-tabs",
1u);
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
EXPECT_EQ(result.activeWindowId, "main-window");
ASSERT_EQ(result.windowIds.size(), 1u);
EXPECT_EQ(result.windowIds[0], "main-window");
EXPECT_EQ(
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "doc-b-window"),
nullptr);
const auto* mainWindow =
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
ASSERT_NE(mainWindow, nullptr);
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
ASSERT_NE(FindUIEditorPanelSessionState(mainWindow->session, "doc-b"), nullptr);
EXPECT_EQ(mainWindow->workspace.activePanelId, "doc-b");
}
TEST(UIEditorWindowWorkspaceControllerTest, DockingDetachedPanelIntoMainWindowAlsoClosesSourceWindow) {
UIEditorWindowWorkspaceController controller =
BuildDefaultUIEditorWindowWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
ASSERT_EQ(
controller.DetachPanelToNewWindow(
"main-window",
"document-tabs",
"doc-b",
"doc-b-window").status,
UIEditorWindowWorkspaceOperationStatus::Changed);
const auto result = controller.DockPanelRelative(
"doc-b-window",
"doc-b-window-root",
"doc-b",
"main-window",
"inspector-panel",
UIEditorWorkspaceDockPlacement::Left,
0.4f);
EXPECT_EQ(result.status, UIEditorWindowWorkspaceOperationStatus::Changed);
EXPECT_EQ(result.activeWindowId, "main-window");
ASSERT_EQ(result.windowIds.size(), 1u);
const auto* mainWindow =
FindUIEditorWindowWorkspaceState(controller.GetWindowSet(), "main-window");
ASSERT_NE(mainWindow, nullptr);
ASSERT_TRUE(ContainsUIEditorWorkspacePanel(mainWindow->workspace, "doc-b"));
ASSERT_NE(FindUIEditorPanelSessionState(mainWindow->session, "doc-b"), nullptr);
const auto visibleIds =
CollectVisiblePanelIds(mainWindow->workspace, mainWindow->session);
ASSERT_EQ(visibleIds.size(), 3u);
EXPECT_EQ(visibleIds[0], "doc-a");
EXPECT_EQ(visibleIds[1], "doc-b");
EXPECT_EQ(visibleIds[2], "inspector");
}