From 3c0dedcc5ff7d9f05b8669926fc9f7432e3de66a Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 06:14:58 +0800 Subject: [PATCH] Build XCEditor workspace viewport compose foundation --- new_editor/CMakeLists.txt | 1 + .../XCEditor/Core/UIEditorPanelRegistry.h | 3 +- .../XCEditor/Core/UIEditorWorkspaceCompose.h | 99 ++ .../XCEditor/Widgets/UIEditorDockHost.h | 24 + .../src/Core/UIEditorWorkspaceCompose.cpp | 301 ++++++ new_editor/src/Widgets/UIEditorDockHost.cpp | 59 +- tests/UI/Editor/integration/CMakeLists.txt | 5 + tests/UI/Editor/integration/README.md | 11 +- .../Editor/integration/shell/CMakeLists.txt | 3 + .../workspace_viewport_compose/CMakeLists.txt | 30 + .../captures/.gitkeep | 1 + .../shell/workspace_viewport_compose/main.cpp | 957 ++++++++++++++++++ tests/UI/Editor/unit/CMakeLists.txt | 1 + .../Editor/unit/test_ui_editor_dock_host.cpp | 61 +- .../unit/test_ui_editor_workspace_compose.cpp | 269 +++++ 15 files changed, 1809 insertions(+), 16 deletions(-) create mode 100644 new_editor/include/XCEditor/Core/UIEditorWorkspaceCompose.h create mode 100644 new_editor/src/Core/UIEditorWorkspaceCompose.cpp create mode 100644 tests/UI/Editor/integration/shell/workspace_viewport_compose/CMakeLists.txt create mode 100644 tests/UI/Editor/integration/shell/workspace_viewport_compose/captures/.gitkeep create mode 100644 tests/UI/Editor/integration/shell/workspace_viewport_compose/main.cpp create mode 100644 tests/UI/Editor/unit/test_ui_editor_workspace_compose.cpp diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index afdd04a9..0ec179cb 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(XCUIEditorLib STATIC src/Core/UIEditorShortcutManager.cpp src/Core/UIEditorViewportInputBridge.cpp src/Core/UIEditorViewportShell.cpp + src/Core/UIEditorWorkspaceCompose.cpp src/Core/UIEditorWorkspaceLayoutPersistence.cpp src/Core/UIEditorWorkspaceController.cpp src/Core/UIEditorWorkspaceModel.cpp diff --git a/new_editor/include/XCEditor/Core/UIEditorPanelRegistry.h b/new_editor/include/XCEditor/Core/UIEditorPanelRegistry.h index 084df44c..e9bbaede 100644 --- a/new_editor/include/XCEditor/Core/UIEditorPanelRegistry.h +++ b/new_editor/include/XCEditor/Core/UIEditorPanelRegistry.h @@ -8,7 +8,8 @@ namespace XCEngine::UI::Editor { enum class UIEditorPanelPresentationKind : std::uint8_t { - Placeholder = 0 + Placeholder = 0, + ViewportShell }; struct UIEditorPanelDescriptor { diff --git a/new_editor/include/XCEditor/Core/UIEditorWorkspaceCompose.h b/new_editor/include/XCEditor/Core/UIEditorWorkspaceCompose.h new file mode 100644 index 00000000..e103da21 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorWorkspaceCompose.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor { + +struct UIEditorWorkspacePanelPresentationModel { + std::string panelId = {}; + UIEditorPanelPresentationKind kind = UIEditorPanelPresentationKind::Placeholder; + UIEditorViewportShellModel viewportShellModel = {}; +}; + +struct UIEditorWorkspacePanelPresentationState { + std::string panelId = {}; + UIEditorViewportShellState viewportShellState = {}; +}; + +struct UIEditorWorkspaceComposeState { + std::vector panelStates = {}; +}; + +struct UIEditorWorkspaceViewportComposeRequest { + std::string panelId = {}; + ::XCEngine::UI::UIRect bounds = {}; + UIEditorViewportShellRequest viewportShellRequest = {}; +}; + +struct UIEditorWorkspaceComposeRequest { + Widgets::UIEditorDockHostLayout dockHostLayout = {}; + std::vector viewportRequests = {}; +}; + +struct UIEditorWorkspaceViewportComposeFrame { + std::string panelId = {}; + ::XCEngine::UI::UIRect bounds = {}; + UIEditorViewportShellModel viewportShellModel = {}; + UIEditorViewportShellFrame viewportShellFrame = {}; +}; + +struct UIEditorWorkspaceComposeFrame { + Widgets::UIEditorDockHostLayout dockHostLayout = {}; + std::vector viewportFrames = {}; +}; + +const UIEditorWorkspacePanelPresentationModel* FindUIEditorWorkspacePanelPresentationModel( + const std::vector& presentations, + std::string_view panelId); + +const UIEditorWorkspacePanelPresentationState* FindUIEditorWorkspacePanelPresentationState( + const UIEditorWorkspaceComposeState& state, + std::string_view panelId); + +const UIEditorWorkspaceViewportComposeRequest* FindUIEditorWorkspaceViewportPresentationRequest( + const UIEditorWorkspaceComposeRequest& request, + std::string_view panelId); + +const UIEditorWorkspaceViewportComposeFrame* FindUIEditorWorkspaceViewportPresentationFrame( + const UIEditorWorkspaceComposeFrame& frame, + std::string_view panelId); + +UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const std::vector& presentations, + const Widgets::UIEditorDockHostState& dockHostState = {}, + const Widgets::UIEditorDockHostMetrics& dockHostMetrics = {}); + +UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( + UIEditorWorkspaceComposeState& state, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const std::vector& presentations, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorDockHostState& dockHostState = {}, + const Widgets::UIEditorDockHostMetrics& dockHostMetrics = {}); + +std::vector CollectUIEditorWorkspaceComposeExternalBodyPanelIds( + const UIEditorWorkspaceComposeFrame& frame); + +void AppendUIEditorWorkspaceCompose( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorWorkspaceComposeFrame& frame, + const Widgets::UIEditorDockHostPalette& dockHostPalette = {}, + const Widgets::UIEditorDockHostMetrics& dockHostMetrics = {}); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Widgets/UIEditorDockHost.h b/new_editor/include/XCEditor/Widgets/UIEditorDockHost.h index 79b66bb1..66df474d 100644 --- a/new_editor/include/XCEditor/Widgets/UIEditorDockHost.h +++ b/new_editor/include/XCEditor/Widgets/UIEditorDockHost.h @@ -117,6 +117,12 @@ struct UIEditorDockHostLayout { std::vector tabStacks = {}; }; +// Allows higher-level compose to own panel body presentation while DockHost +// keeps drawing the surrounding chrome/frame. +struct UIEditorDockHostForegroundOptions { + std::vector externalBodyPanelIds = {}; +}; + const UIEditorDockHostSplitterLayout* FindUIEditorDockHostSplitterLayout( const UIEditorDockHostLayout& layout, std::string_view nodeId); @@ -142,9 +148,16 @@ void AppendUIEditorDockHostBackground( void AppendUIEditorDockHostForeground( ::XCEngine::UI::UIDrawList& drawList, const UIEditorDockHostLayout& layout, + const UIEditorDockHostForegroundOptions& options = {}, const UIEditorDockHostPalette& palette = {}, const UIEditorDockHostMetrics& metrics = {}); +void AppendUIEditorDockHostForeground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorDockHostLayout& layout, + const UIEditorDockHostPalette& palette, + const UIEditorDockHostMetrics& metrics = {}); + void AppendUIEditorDockHost( ::XCEngine::UI::UIDrawList& drawList, const ::XCEngine::UI::UIRect& bounds, @@ -152,7 +165,18 @@ void AppendUIEditorDockHost( const UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceSession& session, const UIEditorDockHostState& state = {}, + const UIEditorDockHostForegroundOptions& foregroundOptions = {}, const UIEditorDockHostPalette& palette = {}, const UIEditorDockHostMetrics& metrics = {}); +void AppendUIEditorDockHost( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const UIEditorDockHostState& state, + const UIEditorDockHostPalette& palette, + const UIEditorDockHostMetrics& metrics = {}); + } // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Core/UIEditorWorkspaceCompose.cpp b/new_editor/src/Core/UIEditorWorkspaceCompose.cpp new file mode 100644 index 00000000..10b71185 --- /dev/null +++ b/new_editor/src/Core/UIEditorWorkspaceCompose.cpp @@ -0,0 +1,301 @@ +#include + +#include +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using Widgets::AppendUIEditorDockHostBackground; +using Widgets::AppendUIEditorDockHostForeground; +using Widgets::AppendUIEditorViewportSlotBackground; +using Widgets::AppendUIEditorViewportSlotForeground; +using Widgets::BuildUIEditorDockHostLayout; +using Widgets::UIEditorDockHostForegroundOptions; + +const UIEditorWorkspacePanelPresentationState* FindPanelStateImpl( + const UIEditorWorkspaceComposeState& state, + std::string_view panelId) { + for (const UIEditorWorkspacePanelPresentationState& panelState : state.panelStates) { + if (panelState.panelId == panelId) { + return &panelState; + } + } + + return nullptr; +} + +UIEditorWorkspacePanelPresentationState* FindMutablePanelStateImpl( + UIEditorWorkspaceComposeState& state, + std::string_view panelId) { + for (UIEditorWorkspacePanelPresentationState& panelState : state.panelStates) { + if (panelState.panelId == panelId) { + return &panelState; + } + } + + return nullptr; +} + +bool SupportsExternalViewportPresentation( + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspacePanelPresentationModel& presentation) { + if (presentation.kind != UIEditorPanelPresentationKind::ViewportShell) { + return false; + } + + const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(panelRegistry, presentation.panelId); + return descriptor != nullptr && + descriptor->presentationKind == UIEditorPanelPresentationKind::ViewportShell; +} + +UIEditorWorkspacePanelPresentationState& EnsurePanelState( + UIEditorWorkspaceComposeState& state, + std::string_view panelId) { + for (UIEditorWorkspacePanelPresentationState& panelState : state.panelStates) { + if (panelState.panelId == panelId) { + return panelState; + } + } + + UIEditorWorkspacePanelPresentationState panelState = {}; + panelState.panelId = std::string(panelId); + state.panelStates.push_back(std::move(panelState)); + return state.panelStates.back(); +} + +void ResetHiddenViewportPresentationState( + UIEditorWorkspaceComposeState& state, + std::string_view panelId) { + UIEditorWorkspacePanelPresentationState* panelState = + FindMutablePanelStateImpl(state, panelId); + if (panelState == nullptr) { + return; + } + + panelState->viewportShellState = {}; +} + +void TrimObsoleteViewportPresentationStates( + UIEditorWorkspaceComposeState& state, + const UIEditorPanelRegistry& panelRegistry, + const std::vector& presentations) { + state.panelStates.erase( + std::remove_if( + state.panelStates.begin(), + state.panelStates.end(), + [&](const UIEditorWorkspacePanelPresentationState& panelState) { + for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { + if (presentation.panelId == panelState.panelId && + SupportsExternalViewportPresentation(panelRegistry, presentation)) { + return false; + } + } + + return true; + }), + state.panelStates.end()); +} + +const ::XCEngine::UI::UIRect* FindVisiblePanelBodyRect( + const Widgets::UIEditorDockHostLayout& layout, + std::string_view panelId) { + for (const auto& panel : layout.panels) { + if (panel.panelId == panelId) { + return &panel.frameLayout.bodyRect; + } + } + + for (const auto& tabStack : layout.tabStacks) { + if (tabStack.selectedPanelId == panelId) { + return &tabStack.contentFrameLayout.bodyRect; + } + } + + return nullptr; +} + +} // namespace + +const UIEditorWorkspacePanelPresentationModel* FindUIEditorWorkspacePanelPresentationModel( + const std::vector& presentations, + std::string_view panelId) { + for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { + if (presentation.panelId == panelId) { + return &presentation; + } + } + + return nullptr; +} + +const UIEditorWorkspacePanelPresentationState* FindUIEditorWorkspacePanelPresentationState( + const UIEditorWorkspaceComposeState& state, + std::string_view panelId) { + return FindPanelStateImpl(state, panelId); +} + +const UIEditorWorkspaceViewportComposeRequest* FindUIEditorWorkspaceViewportPresentationRequest( + const UIEditorWorkspaceComposeRequest& request, + std::string_view panelId) { + for (const UIEditorWorkspaceViewportComposeRequest& viewportRequest : request.viewportRequests) { + if (viewportRequest.panelId == panelId) { + return &viewportRequest; + } + } + + return nullptr; +} + +const UIEditorWorkspaceViewportComposeFrame* FindUIEditorWorkspaceViewportPresentationFrame( + const UIEditorWorkspaceComposeFrame& frame, + std::string_view panelId) { + for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.viewportFrames) { + if (viewportFrame.panelId == panelId) { + return &viewportFrame; + } + } + + return nullptr; +} + +UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const std::vector& presentations, + const Widgets::UIEditorDockHostState& dockHostState, + const Widgets::UIEditorDockHostMetrics& dockHostMetrics) { + UIEditorWorkspaceComposeRequest request = {}; + request.dockHostLayout = BuildUIEditorDockHostLayout( + bounds, + panelRegistry, + workspace, + session, + dockHostState, + dockHostMetrics); + + for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { + if (!SupportsExternalViewportPresentation(panelRegistry, presentation)) { + continue; + } + + const auto* bodyRect = FindVisiblePanelBodyRect(request.dockHostLayout, presentation.panelId); + if (bodyRect == nullptr) { + continue; + } + + UIEditorWorkspaceViewportComposeRequest viewportRequest = {}; + viewportRequest.panelId = presentation.panelId; + viewportRequest.bounds = *bodyRect; + viewportRequest.viewportShellRequest = ResolveUIEditorViewportShellRequest( + *bodyRect, + presentation.viewportShellModel.spec); + request.viewportRequests.push_back(std::move(viewportRequest)); + } + + return request; +} + +UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( + UIEditorWorkspaceComposeState& state, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const std::vector& presentations, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorDockHostState& dockHostState, + const Widgets::UIEditorDockHostMetrics& dockHostMetrics) { + UIEditorWorkspaceComposeFrame frame = {}; + frame.dockHostLayout = BuildUIEditorDockHostLayout( + bounds, + panelRegistry, + workspace, + session, + dockHostState, + dockHostMetrics); + TrimObsoleteViewportPresentationStates(state, panelRegistry, presentations); + + for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { + if (!SupportsExternalViewportPresentation(panelRegistry, presentation)) { + continue; + } + + const auto* bodyRect = FindVisiblePanelBodyRect(frame.dockHostLayout, presentation.panelId); + if (bodyRect == nullptr) { + ResetHiddenViewportPresentationState(state, presentation.panelId); + continue; + } + + UIEditorWorkspacePanelPresentationState& panelState = + EnsurePanelState(state, presentation.panelId); + + UIEditorWorkspaceViewportComposeFrame viewportFrame = {}; + viewportFrame.panelId = presentation.panelId; + viewportFrame.bounds = *bodyRect; + viewportFrame.viewportShellModel = presentation.viewportShellModel; + viewportFrame.viewportShellFrame = UpdateUIEditorViewportShell( + panelState.viewportShellState, + *bodyRect, + presentation.viewportShellModel, + inputEvents); + frame.viewportFrames.push_back(std::move(viewportFrame)); + } + + return frame; +} + +std::vector CollectUIEditorWorkspaceComposeExternalBodyPanelIds( + const UIEditorWorkspaceComposeFrame& frame) { + std::vector panelIds = {}; + panelIds.reserve(frame.viewportFrames.size()); + for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.viewportFrames) { + panelIds.push_back(viewportFrame.panelId); + } + return panelIds; +} + +void AppendUIEditorWorkspaceCompose( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorWorkspaceComposeFrame& frame, + const Widgets::UIEditorDockHostPalette& dockHostPalette, + const Widgets::UIEditorDockHostMetrics& dockHostMetrics) { + AppendUIEditorDockHostBackground( + drawList, + frame.dockHostLayout, + dockHostPalette, + dockHostMetrics); + + for (const UIEditorWorkspaceViewportComposeFrame& viewportFrame : frame.viewportFrames) { + AppendUIEditorViewportSlotBackground( + drawList, + viewportFrame.viewportShellFrame.slotLayout, + viewportFrame.viewportShellModel.spec.toolItems, + viewportFrame.viewportShellModel.spec.statusSegments, + viewportFrame.viewportShellFrame.slotState); + AppendUIEditorViewportSlotForeground( + drawList, + viewportFrame.viewportShellFrame.slotLayout, + viewportFrame.viewportShellModel.spec.chrome, + viewportFrame.viewportShellModel.frame, + viewportFrame.viewportShellModel.spec.toolItems, + viewportFrame.viewportShellModel.spec.statusSegments, + viewportFrame.viewportShellFrame.slotState); + } + + UIEditorDockHostForegroundOptions foregroundOptions = {}; + foregroundOptions.externalBodyPanelIds = + CollectUIEditorWorkspaceComposeExternalBodyPanelIds(frame); + AppendUIEditorDockHostForeground( + drawList, + frame.dockHostLayout, + foregroundOptions, + dockHostPalette, + dockHostMetrics); +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Widgets/UIEditorDockHost.cpp b/new_editor/src/Widgets/UIEditorDockHost.cpp index dd932ffc..f15e8d84 100644 --- a/new_editor/src/Widgets/UIEditorDockHost.cpp +++ b/new_editor/src/Widgets/UIEditorDockHost.cpp @@ -51,6 +51,15 @@ bool IsPointInsideRect( point.y <= rect.y + rect.height; } +bool UsesExternalBodyPresentation( + const UIEditorDockHostForegroundOptions& options, + std::string_view panelId) { + return std::find( + options.externalBodyPanelIds.begin(), + options.externalBodyPanelIds.end(), + panelId) != options.externalBodyPanelIds.end(); +} + bool IsPanelOpenAndVisible( const UIEditorWorkspaceSession& session, std::string_view panelId) { @@ -740,6 +749,7 @@ void AppendUIEditorDockHostBackground( void AppendUIEditorDockHostForeground( UIDrawList& drawList, const UIEditorDockHostLayout& layout, + const UIEditorDockHostForegroundOptions& options, const UIEditorDockHostPalette& palette, const UIEditorDockHostMetrics& metrics) { for (const UIEditorDockHostPanelLayout& panel : layout.panels) { @@ -755,6 +765,9 @@ void AppendUIEditorDockHostForeground( text, palette.panelFramePalette, metrics.panelFrameMetrics); + if (UsesExternalBodyPresentation(options, panel.panelId)) { + continue; + } AppendPlaceholderText( drawList, panel.frameLayout.bodyRect, @@ -801,6 +814,9 @@ void AppendUIEditorDockHostForeground( } } + if (UsesExternalBodyPresentation(options, tabStack.selectedPanelId)) { + continue; + } AppendPlaceholderText( drawList, tabStack.contentFrameLayout.bodyRect, @@ -812,6 +828,35 @@ void AppendUIEditorDockHostForeground( } } +void AppendUIEditorDockHostForeground( + UIDrawList& drawList, + const UIEditorDockHostLayout& layout, + const UIEditorDockHostPalette& palette, + const UIEditorDockHostMetrics& metrics) { + AppendUIEditorDockHostForeground( + drawList, + layout, + UIEditorDockHostForegroundOptions{}, + palette, + metrics); +} + +void AppendUIEditorDockHost( + UIDrawList& drawList, + const UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const UIEditorDockHostState& state, + const UIEditorDockHostForegroundOptions& foregroundOptions, + const UIEditorDockHostPalette& palette, + const UIEditorDockHostMetrics& metrics) { + const UIEditorDockHostLayout layout = + BuildUIEditorDockHostLayout(bounds, panelRegistry, workspace, session, state, metrics); + AppendUIEditorDockHostBackground(drawList, layout, palette, metrics); + AppendUIEditorDockHostForeground(drawList, layout, foregroundOptions, palette, metrics); +} + void AppendUIEditorDockHost( UIDrawList& drawList, const UIRect& bounds, @@ -821,10 +866,16 @@ void AppendUIEditorDockHost( const UIEditorDockHostState& state, const UIEditorDockHostPalette& palette, const UIEditorDockHostMetrics& metrics) { - const UIEditorDockHostLayout layout = - BuildUIEditorDockHostLayout(bounds, panelRegistry, workspace, session, state, metrics); - AppendUIEditorDockHostBackground(drawList, layout, palette, metrics); - AppendUIEditorDockHostForeground(drawList, layout, palette, metrics); + AppendUIEditorDockHost( + drawList, + bounds, + panelRegistry, + workspace, + session, + state, + UIEditorDockHostForegroundOptions{}, + palette, + metrics); } } // namespace XCEngine::UI::Editor::Widgets diff --git a/tests/UI/Editor/integration/CMakeLists.txt b/tests/UI/Editor/integration/CMakeLists.txt index 5d12b4d0..963a2084 100644 --- a/tests/UI/Editor/integration/CMakeLists.txt +++ b/tests/UI/Editor/integration/CMakeLists.txt @@ -34,6 +34,11 @@ if(TARGET editor_ui_viewport_shell_basic_validation) editor_ui_viewport_shell_basic_validation) endif() +if(TARGET editor_ui_workspace_viewport_compose_validation) + list(APPEND EDITOR_UI_INTEGRATION_TARGETS + editor_ui_workspace_viewport_compose_validation) +endif() + if(TARGET editor_ui_viewport_input_bridge_basic_validation) list(APPEND EDITOR_UI_INTEGRATION_TARGETS editor_ui_viewport_input_bridge_basic_validation) diff --git a/tests/UI/Editor/integration/README.md b/tests/UI/Editor/integration/README.md index f81e40de..38a3f698 100644 --- a/tests/UI/Editor/integration/README.md +++ b/tests/UI/Editor/integration/README.md @@ -22,6 +22,7 @@ Layout: - `shell/tab_strip_basic/`: tab strip layout/state/hit-test/close/navigation only - `shell/viewport_slot_basic/`: viewport slot chrome/surface/status only - `shell/viewport_shell_basic/`: viewport shell request/state compose only +- `shell/workspace_viewport_compose/`: workspace body external presentation compose only - `state/panel_session_flow/`: panel session state flow only - `state/layout_persistence/`: layout save/load/reset only - `state/shortcut_dispatch/`: shortcut match/suppression/dispatch only @@ -69,6 +70,11 @@ Scenarios: Executable: `XCUIEditorViewportShellBasicValidation.exe` Scope: `ResolveUIEditorViewportShellRequest(...)` + `UpdateUIEditorViewportShell(...)` basic contract, TopBar / BottomBar request-size sync, input rect + hover/focus/capture state sync only +- `editor.shell.workspace_viewport_compose` + Build target: `editor_ui_workspace_viewport_compose_validation` + Executable: `XCUIEditorWorkspaceViewportComposeValidation.exe` + Scope: `ResolveUIEditorWorkspaceComposeRequest(...)` + `UpdateUIEditorWorkspaceCompose(...)` body presentation contract only; selected Scene tab body is hosted by `ViewportShell`, switching back to Document restores DockHost placeholder + - `editor.state.panel_session_flow` Build target: `editor_ui_panel_session_flow_validation` Executable: `XCUIEditorPanelSessionFlowValidation.exe` @@ -121,11 +127,14 @@ Selected controls: Click `Document A / B / C`, click `X` on closable tabs, click content to focus, press `Left / Right / Home / End`, press `Reset`, press `F12`. - `shell/viewport_slot_basic/` - Hover toolbar / surface / status bar, click surface to focus, hold and release left mouse to inspect capture, toggle `TopBar / 状态条 / Texture / 方形比例`, press `F12`. + Hover toolbar / surface / status bar, click surface to focus, hold and release left mouse to inspect capture, toggle `TopBar / StatusBar / Texture / Aspect`, press `F12`. - `shell/viewport_shell_basic/` Hover / click / drag the viewport shell surface, toggle `TopBar / BottomBar / Texture`, inspect left-side `Request Size / Input Rect / Hover Hit / Hover / Focus / Capture / Result`, press `Reset`, press `截图` or `F12`. +- `shell/workspace_viewport_compose/` + Click `切到 Scene / 切到 Document`, toggle `TopBar / BottomBar / Texture`, hover or click the center viewport only when `Scene` is selected, inspect `Selected Presentation / Request Size / Hover / Focus / Capture / Result`, press `Reset`, press `截图` or `F12`. + - `state/panel_session_flow/` Click `Hide Active / Show Doc A / Close Doc B / Open Doc B / Activate Details / Reset`, press `F12`. diff --git a/tests/UI/Editor/integration/shell/CMakeLists.txt b/tests/UI/Editor/integration/shell/CMakeLists.txt index eb1682fe..870fa604 100644 --- a/tests/UI/Editor/integration/shell/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/CMakeLists.txt @@ -16,3 +16,6 @@ endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/viewport_shell_basic/CMakeLists.txt") add_subdirectory(viewport_shell_basic) endif() +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/workspace_viewport_compose/CMakeLists.txt") + add_subdirectory(workspace_viewport_compose) +endif() diff --git a/tests/UI/Editor/integration/shell/workspace_viewport_compose/CMakeLists.txt b/tests/UI/Editor/integration/shell/workspace_viewport_compose/CMakeLists.txt new file mode 100644 index 00000000..7904f616 --- /dev/null +++ b/tests/UI/Editor/integration/shell/workspace_viewport_compose/CMakeLists.txt @@ -0,0 +1,30 @@ +add_executable(editor_ui_workspace_viewport_compose_validation WIN32 + main.cpp +) + +target_include_directories(editor_ui_workspace_viewport_compose_validation PRIVATE + ${CMAKE_SOURCE_DIR}/new_editor/include + ${CMAKE_SOURCE_DIR}/new_editor/app + ${CMAKE_SOURCE_DIR}/engine/include +) + +target_compile_definitions(editor_ui_workspace_viewport_compose_validation PRIVATE + UNICODE + _UNICODE + XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}" +) + +if(MSVC) + target_compile_options(editor_ui_workspace_viewport_compose_validation PRIVATE /utf-8 /FS) + set_property(TARGET editor_ui_workspace_viewport_compose_validation PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +target_link_libraries(editor_ui_workspace_viewport_compose_validation PRIVATE + XCUIEditorLib + XCUIEditorHost +) + +set_target_properties(editor_ui_workspace_viewport_compose_validation PROPERTIES + OUTPUT_NAME "XCUIEditorWorkspaceViewportComposeValidation" +) diff --git a/tests/UI/Editor/integration/shell/workspace_viewport_compose/captures/.gitkeep b/tests/UI/Editor/integration/shell/workspace_viewport_compose/captures/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/UI/Editor/integration/shell/workspace_viewport_compose/captures/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/UI/Editor/integration/shell/workspace_viewport_compose/main.cpp b/tests/UI/Editor/integration/shell/workspace_viewport_compose/main.cpp new file mode 100644 index 00000000..0c79e735 --- /dev/null +++ b/tests/UI/Editor/integration/shell/workspace_viewport_compose/main.cpp @@ -0,0 +1,957 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include +#include +#include +#include "Host/AutoScreenshot.h" +#include "Host/InputModifierTracker.h" +#include "Host/NativeRenderer.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT +#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "." +#endif + +namespace { + +using XCEngine::UI::UIColor; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::UISize; +using XCEngine::UI::Editor::AppendUIEditorWorkspaceCompose; +using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController; +using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; +using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationFrame; +using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationRequest; +using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName; +using XCEngine::UI::Editor::ResolveUIEditorWorkspaceComposeRequest; +using XCEngine::UI::Editor::UIEditorPanelPresentationKind; +using XCEngine::UI::Editor::UIEditorPanelRegistry; +using XCEngine::UI::Editor::UIEditorViewportShellModel; +using XCEngine::UI::Editor::UIEditorWorkspaceCommand; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandResult; +using XCEngine::UI::Editor::UIEditorWorkspaceComposeFrame; +using XCEngine::UI::Editor::UIEditorWorkspaceComposeRequest; +using XCEngine::UI::Editor::UIEditorWorkspaceComposeState; +using XCEngine::UI::Editor::UIEditorWorkspaceController; +using XCEngine::UI::Editor::UIEditorWorkspaceModel; +using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel; +using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; +using XCEngine::UI::Editor::UpdateUIEditorWorkspaceCompose; +using XCEngine::UI::Editor::Host::AutoScreenshotController; +using XCEngine::UI::Editor::Host::InputModifierTracker; +using XCEngine::UI::Editor::Host::NativeRenderer; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorViewportSlot; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTarget; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem; +using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot; + +constexpr const wchar_t* kWindowClassName = L"XCUIEditorWorkspaceViewportComposeValidation"; +constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Workspace Viewport Compose"; + +constexpr UIColor kWindowBg(0.11f, 0.11f, 0.11f, 1.0f); +constexpr UIColor kCardBg(0.17f, 0.17f, 0.17f, 1.0f); +constexpr UIColor kCardBorder(0.28f, 0.28f, 0.28f, 1.0f); +constexpr UIColor kPreviewBg(0.14f, 0.14f, 0.14f, 1.0f); +constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f); +constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f); +constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f); +constexpr UIColor kButtonBg(0.24f, 0.24f, 0.24f, 1.0f); +constexpr UIColor kButtonOnBg(0.36f, 0.36f, 0.36f, 1.0f); +constexpr UIColor kButtonBorder(0.46f, 0.46f, 0.46f, 1.0f); +constexpr UIColor kSuccess(0.46f, 0.72f, 0.50f, 1.0f); +constexpr UIColor kDanger(0.78f, 0.35f, 0.35f, 1.0f); + +enum class ActionId : unsigned char { + ActivateScene = 0, + ActivateDocument, + ToggleTopBar, + ToggleBottomBar, + ToggleTexture, + Reset, + Capture +}; + +struct ButtonState { + ActionId action = ActionId::ActivateScene; + std::string label = {}; + UIRect rect = {}; + bool selected = false; +}; + +std::filesystem::path ResolveRepoRootPath(); +bool ContainsPoint(const UIRect& rect, float x, float y); +std::string BoolText(bool value); +std::string FormatFloat(float value); +std::string FormatSize(const UISize& size); +std::string FormatRect(const UIRect& rect); +std::string DescribeHitTarget(const UIEditorViewportSlotHitTarget& hit); +void DrawCard(UIDrawList& drawList, const UIRect& rect, std::string_view title, std::string_view subtitle = {}); +void DrawButton(UIDrawList& drawList, const ButtonState& button); +UIEditorPanelRegistry BuildPanelRegistry(); +UIEditorWorkspaceModel BuildWorkspace(); + +class ScenarioApp { +public: + int Run(HINSTANCE hInstance, int nCmdShow); + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam); + + bool Initialize(HINSTANCE hInstance, int nCmdShow); + void Shutdown(); + void ResetScenario(); + void UpdateLayoutForCurrentWindow(); + std::vector BuildPresentationModels() const; + void QueuePointerEvent(UIInputEventType type, UIPointerButton button, WPARAM wParam, LPARAM lParam); + void QueuePointerLeaveEvent(); + void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam); + void QueueFocusEvent(UIInputEventType type); + void HandleMouseMove(WPARAM wParam, LPARAM lParam); + void HandlePointerDown(UIPointerButton button, WPARAM wParam, LPARAM lParam); + void HandlePointerUp(UIPointerButton button, WPARAM wParam, LPARAM lParam); + void ExecuteAction(ActionId action); + void DispatchWorkspaceCommand(UIEditorWorkspaceCommandKind kind, std::string_view panelId, std::string_view label); + std::string GetSelectedTabPanelId() const; + void UpdateComposeFrame(); + void UpdateLastResult(const XCEngine::UI::Editor::UIEditorWorkspaceViewportComposeFrame* viewportFrame); + void RenderFrame(); + + HWND m_hwnd = nullptr; + ATOM m_windowClassAtom = 0; + NativeRenderer m_renderer = {}; + AutoScreenshotController m_autoScreenshot = {}; + InputModifierTracker m_inputModifierTracker = {}; + std::filesystem::path m_captureRoot = {}; + UIEditorWorkspaceController m_controller = {}; + UIEditorWorkspaceComposeState m_composeState = {}; + UIEditorWorkspaceComposeRequest m_composeRequest = {}; + UIEditorWorkspaceComposeFrame m_composeFrame = {}; + std::vector m_pendingInputEvents = {}; + std::vector m_buttons = {}; + UIRect m_introRect = {}; + UIRect m_controlsRect = {}; + UIRect m_stateRect = {}; + UIRect m_previewRect = {}; + UIRect m_workspaceRect = {}; + UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); + UIEditorViewportSlotHitTarget m_hoverHit = {}; + bool m_showTopBar = true; + bool m_showBottomBar = true; + bool m_textureEnabled = true; + bool m_previousHovered = false; + bool m_previousFocused = false; + bool m_previousCaptured = false; + bool m_previousViewportVisible = false; + bool m_hasSnapshot = false; + std::string m_lastResult = {}; +}; + +std::filesystem::path ResolveRepoRootPath() { + std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT; + if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { + root = root.substr(1u, root.size() - 2u); + } + + return std::filesystem::path(root).lexically_normal(); +} + +bool ContainsPoint(const UIRect& rect, float x, float y) { + return x >= rect.x && + x <= rect.x + rect.width && + y >= rect.y && + y <= rect.y + rect.height; +} + +std::string BoolText(bool value) { + return value ? "On" : "Off"; +} + +std::string FormatFloat(float value) { + std::ostringstream stream = {}; + stream.setf(std::ios::fixed, std::ios::floatfield); + stream.precision(1); + stream << value; + return stream.str(); +} + +std::string FormatSize(const UISize& size) { + return FormatFloat(size.width) + " x " + FormatFloat(size.height); +} + +std::string FormatRect(const UIRect& rect) { + return "x=" + FormatFloat(rect.x) + + " y=" + FormatFloat(rect.y) + + " w=" + FormatFloat(rect.width) + + " h=" + FormatFloat(rect.height); +} + +std::string DescribeHitTarget(const UIEditorViewportSlotHitTarget& hit) { + switch (hit.kind) { + case UIEditorViewportSlotHitTargetKind::TopBar: + return "TopBar"; + case UIEditorViewportSlotHitTargetKind::Title: + return "Title"; + case UIEditorViewportSlotHitTargetKind::ToolItem: + return "ToolItem[" + std::to_string(hit.index) + "]"; + case UIEditorViewportSlotHitTargetKind::Surface: + return "Surface"; + case UIEditorViewportSlotHitTargetKind::BottomBar: + return "BottomBar"; + case UIEditorViewportSlotHitTargetKind::StatusSegment: + return "StatusSegment[" + std::to_string(hit.index) + "]"; + case UIEditorViewportSlotHitTargetKind::StatusSeparator: + return "StatusSeparator[" + std::to_string(hit.index) + "]"; + case UIEditorViewportSlotHitTargetKind::None: + default: + return "None"; + } +} + +void DrawCard( + UIDrawList& drawList, + const UIRect& rect, + std::string_view title, + std::string_view subtitle) { + drawList.AddFilledRect(rect, kCardBg, 10.0f); + drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 14.0f), + std::string(title), + kTextPrimary, + 17.0f); + if (!subtitle.empty()) { + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 38.0f), + std::string(subtitle), + kTextMuted, + 12.0f); + } +} + +void DrawButton(UIDrawList& drawList, const ButtonState& button) { + drawList.AddFilledRect( + button.rect, + button.selected ? kButtonOnBg : kButtonBg, + 8.0f); + drawList.AddRectOutline(button.rect, kButtonBorder, 1.0f, 8.0f); + drawList.AddText( + UIPoint(button.rect.x + 12.0f, button.rect.y + 10.0f), + button.label, + kTextPrimary, + 12.0f); +} + +UIEditorPanelRegistry BuildPanelRegistry() { + UIEditorPanelRegistry registry = {}; + registry.panels = { + { "hierarchy", "Hierarchy", UIEditorPanelPresentationKind::Placeholder, true, true, false }, + { "scene", "Scene", UIEditorPanelPresentationKind::ViewportShell, false, true, false }, + { "document", "Document", UIEditorPanelPresentationKind::Placeholder, true, true, true }, + { "inspector", "Inspector", UIEditorPanelPresentationKind::Placeholder, true, true, true } + }; + return registry; +} + +UIEditorWorkspaceModel BuildWorkspace() { + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "root-left-main", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.23f, + BuildUIEditorWorkspacePanel("hierarchy-node", "hierarchy", "Hierarchy", true), + BuildUIEditorWorkspaceSplit( + "main-center-right", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.72f, + BuildUIEditorWorkspaceTabStack( + "center-tabs", + { + BuildUIEditorWorkspacePanel("scene-node", "scene", "Scene"), + BuildUIEditorWorkspacePanel("document-node", "document", "Document", true) + }, + 0u), + BuildUIEditorWorkspacePanel("inspector-node", "inspector", "Inspector", true))); + workspace.activePanelId = "scene"; + return workspace; +} + +int ScenarioApp::Run(HINSTANCE hInstance, int nCmdShow) { + if (!Initialize(hInstance, nCmdShow)) { + Shutdown(); + return 1; + } + + MSG message = {}; + while (message.message != WM_QUIT) { + if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + TranslateMessage(&message); + DispatchMessageW(&message); + continue; + } + + RenderFrame(); + Sleep(8); + } + + Shutdown(); + return static_cast(message.wParam); +} + +LRESULT CALLBACK ScenarioApp::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->m_renderer.Resize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + case WM_MOUSEMOVE: + if (app != nullptr) { + app->HandleMouseMove(wParam, lParam); + return 0; + } + break; + case WM_MOUSELEAVE: + if (app != nullptr) { + app->QueuePointerLeaveEvent(); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + break; + case WM_LBUTTONDOWN: + if (app != nullptr) { + SetFocus(hwnd); + app->HandlePointerDown(UIPointerButton::Left, wParam, lParam); + return 0; + } + break; + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandlePointerUp(UIPointerButton::Left, wParam, lParam); + return 0; + } + break; + case WM_MOUSEWHEEL: + if (app != nullptr) { + app->QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + break; + case WM_SETFOCUS: + if (app != nullptr) { + app->m_inputModifierTracker.SyncFromSystemState(); + app->QueueFocusEvent(UIInputEventType::FocusGained); + return 0; + } + break; + case WM_KILLFOCUS: + if (app != nullptr) { + if (GetCapture() == hwnd) { + ReleaseCapture(); + } + app->m_inputModifierTracker.Reset(); + app->QueueFocusEvent(UIInputEventType::FocusLost); + return 0; + } + break; + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr && wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + InvalidateRect(hwnd, nullptr, FALSE); + UpdateWindow(hwnd); + return 0; + } + break; + case WM_PAINT: + if (app != nullptr) { + PAINTSTRUCT paintStruct = {}; + BeginPaint(hwnd, &paintStruct); + app->RenderFrame(); + EndPaint(hwnd, &paintStruct); + return 0; + } + break; + case WM_ERASEBKGND: + return 1; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + default: + break; + } + + return DefWindowProcW(hwnd, message, wParam, lParam); +} + +bool ScenarioApp::Initialize(HINSTANCE hInstance, int nCmdShow) { + m_captureRoot = + ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/workspace_viewport_compose/captures"; + m_autoScreenshot.Initialize(m_captureRoot); + + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(windowClass); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = &ScenarioApp::WndProc; + windowClass.hInstance = hInstance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.lpszClassName = kWindowClassName; + + m_windowClassAtom = RegisterClassExW(&windowClass); + if (m_windowClassAtom == 0) { + return false; + } + + m_hwnd = CreateWindowExW( + 0, + kWindowClassName, + kWindowTitle, + WS_OVERLAPPEDWINDOW | WS_VISIBLE, + CW_USEDEFAULT, + CW_USEDEFAULT, + 1520, + 940, + nullptr, + nullptr, + hInstance, + this); + if (m_hwnd == nullptr) { + return false; + } + + if (!m_renderer.Initialize(m_hwnd)) { + return false; + } + + ShowWindow(m_hwnd, nCmdShow); + ResetScenario(); + return true; +} + +void ScenarioApp::Shutdown() { + m_autoScreenshot.Shutdown(); + m_renderer.Shutdown(); + + if (m_hwnd != nullptr && IsWindow(m_hwnd)) { + DestroyWindow(m_hwnd); + } + m_hwnd = nullptr; + + if (m_windowClassAtom != 0) { + UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr)); + m_windowClassAtom = 0; + } +} + +void ScenarioApp::ResetScenario() { + m_controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + m_composeState = {}; + m_composeRequest = {}; + m_composeFrame = {}; + m_pendingInputEvents.clear(); + m_inputModifierTracker.Reset(); + m_buttons.clear(); + m_mousePosition = UIPoint(-1000.0f, -1000.0f); + m_hoverHit = {}; + m_showTopBar = true; + m_showBottomBar = true; + m_textureEnabled = true; + m_lastResult = "Ready"; + m_previousHovered = false; + m_previousFocused = false; + m_previousCaptured = false; + m_previousViewportVisible = false; + m_hasSnapshot = false; +} + +void ScenarioApp::UpdateLayoutForCurrentWindow() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast(std::max(1L, clientRect.right - clientRect.left)); + const float height = static_cast(std::max(1L, clientRect.bottom - clientRect.top)); + + constexpr float outerPadding = 20.0f; + constexpr float leftColumnWidth = 460.0f; + m_introRect = UIRect(outerPadding, outerPadding, leftColumnWidth, 184.0f); + m_controlsRect = UIRect(outerPadding, 220.0f, leftColumnWidth, 340.0f); + m_stateRect = UIRect(outerPadding, 576.0f, leftColumnWidth, height - 596.0f); + m_previewRect = UIRect( + leftColumnWidth + outerPadding * 2.0f, + outerPadding, + width - leftColumnWidth - outerPadding * 3.0f, + height - outerPadding * 2.0f); + m_workspaceRect = UIRect( + m_previewRect.x + 18.0f, + m_previewRect.y + 54.0f, + m_previewRect.width - 36.0f, + m_previewRect.height - 72.0f); + + const float buttonHeight = 32.0f; + const float gap = 8.0f; + const float left = m_controlsRect.x + 16.0f; + const float top = m_controlsRect.y + 64.0f; + const float widthAvailable = m_controlsRect.width - 32.0f; + const bool sceneSelected = GetSelectedTabPanelId() == "scene"; + m_buttons = { + { ActionId::ActivateScene, "切到 Scene", UIRect(left, top, widthAvailable, buttonHeight), sceneSelected }, + { ActionId::ActivateDocument, "切到 Document", UIRect(left, top + (buttonHeight + gap), widthAvailable, buttonHeight), !sceneSelected }, + { ActionId::ToggleTopBar, std::string("TopBar: ") + (m_showTopBar ? "开" : "关"), UIRect(left, top + (buttonHeight + gap) * 2.0f, widthAvailable, buttonHeight), m_showTopBar }, + { ActionId::ToggleBottomBar, std::string("BottomBar: ") + (m_showBottomBar ? "开" : "关"), UIRect(left, top + (buttonHeight + gap) * 3.0f, widthAvailable, buttonHeight), m_showBottomBar }, + { ActionId::ToggleTexture, std::string("Texture: ") + (m_textureEnabled ? "开" : "关"), UIRect(left, top + (buttonHeight + gap) * 4.0f, widthAvailable, buttonHeight), m_textureEnabled }, + { ActionId::Reset, "Reset", UIRect(left, top + (buttonHeight + gap) * 5.0f, widthAvailable, buttonHeight), false }, + { ActionId::Capture, "截图", UIRect(left, top + (buttonHeight + gap) * 6.0f, widthAvailable, buttonHeight), false } + }; +} + +std::vector ScenarioApp::BuildPresentationModels() const { + UIEditorViewportShellModel viewportModel = {}; + viewportModel.spec.chrome.title = "Scene"; + viewportModel.spec.chrome.subtitle = "WorkspaceCompose 基础层"; + viewportModel.spec.chrome.showTopBar = m_showTopBar; + viewportModel.spec.chrome.showBottomBar = m_showBottomBar; + viewportModel.spec.chrome.topBarHeight = 40.0f; + viewportModel.spec.chrome.bottomBarHeight = 28.0f; + viewportModel.spec.toolItems = { + { "mode", "Perspective", UIEditorViewportSlotToolSlot::Leading, true, true, 98.0f }, + { "focus", "WorkspaceCompose", UIEditorViewportSlotToolSlot::Trailing, true, false, 132.0f } + }; + viewportModel.spec.statusSegments = { + { "chrome-top", std::string("TopBar ") + BoolText(m_showTopBar), UIEditorStatusBarSlot::Leading, {}, true, true, 96.0f }, + { "chrome-bottom", std::string("BottomBar ") + BoolText(m_showBottomBar), UIEditorStatusBarSlot::Leading, {}, true, false, 126.0f }, + { "branch", m_textureEnabled ? "Texture" : "Fallback", UIEditorStatusBarSlot::Trailing, {}, true, true, 100.0f } + }; + + if (m_textureEnabled) { + viewportModel.frame.hasTexture = true; + viewportModel.frame.texture = { 1u, 1280u, 720u }; + viewportModel.frame.presentedSize = UISize(1280.0f, 720.0f); + viewportModel.frame.statusText = "Fake viewport frame"; + } else { + viewportModel.frame.hasTexture = false; + viewportModel.frame.statusText = "这里只验证 WorkspaceCompose contract,不接 Scene/Game 业务。"; + } + + UIEditorWorkspacePanelPresentationModel model = {}; + model.panelId = "scene"; + model.kind = UIEditorPanelPresentationKind::ViewportShell; + model.viewportShellModel = std::move(viewportModel); + return { model }; +} + +void ScenarioApp::QueuePointerEvent(UIInputEventType type, UIPointerButton button, WPARAM wParam, LPARAM lParam) { + UIInputEvent event = {}; + event.type = type; + event.pointerButton = button; + event.position = UIPoint( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast(wParam)); + m_pendingInputEvents.push_back(event); + m_mousePosition = event.position; +} + +void ScenarioApp::QueuePointerLeaveEvent() { + UIInputEvent event = {}; + event.type = UIInputEventType::PointerLeave; + if (m_hwnd != nullptr) { + POINT clientPoint = {}; + GetCursorPos(&clientPoint); + ScreenToClient(m_hwnd, &clientPoint); + event.position = UIPoint(static_cast(clientPoint.x), static_cast(clientPoint.y)); + m_mousePosition = event.position; + } + m_pendingInputEvents.push_back(event); +} + +void ScenarioApp::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) { + if (m_hwnd == nullptr) { + return; + } + + POINT screenPoint = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; + ScreenToClient(m_hwnd, &screenPoint); + + UIInputEvent event = {}; + event.type = UIInputEventType::PointerWheel; + event.position = UIPoint(static_cast(screenPoint.x), static_cast(screenPoint.y)); + event.wheelDelta = static_cast(wheelDelta); + event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast(wParam)); + m_pendingInputEvents.push_back(event); + m_mousePosition = event.position; +} + +void ScenarioApp::QueueFocusEvent(UIInputEventType type) { + UIInputEvent event = {}; + event.type = type; + event.modifiers = m_inputModifierTracker.GetCurrentModifiers(); + m_pendingInputEvents.push_back(event); +} + +void ScenarioApp::HandleMouseMove(WPARAM wParam, LPARAM lParam) { + QueuePointerEvent(UIInputEventType::PointerMove, UIPointerButton::None, wParam, lParam); + TRACKMOUSEEVENT event = {}; + event.cbSize = sizeof(event); + event.dwFlags = TME_LEAVE; + event.hwndTrack = m_hwnd; + TrackMouseEvent(&event); + InvalidateRect(m_hwnd, nullptr, FALSE); +} + +void ScenarioApp::HandlePointerDown(UIPointerButton button, WPARAM wParam, LPARAM lParam) { + QueuePointerEvent(UIInputEventType::PointerButtonDown, button, wParam, lParam); + + const UIPoint point( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + const auto* viewportFrame = FindUIEditorWorkspaceViewportPresentationFrame(m_composeFrame, "scene"); + if (viewportFrame != nullptr && + ContainsPoint(viewportFrame->viewportShellFrame.slotLayout.inputRect, point.x, point.y)) { + SetCapture(m_hwnd); + } + + InvalidateRect(m_hwnd, nullptr, FALSE); +} + +void ScenarioApp::HandlePointerUp(UIPointerButton button, WPARAM wParam, LPARAM lParam) { + QueuePointerEvent(UIInputEventType::PointerButtonUp, button, wParam, lParam); + + const UIPoint point( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + if (GetCapture() == m_hwnd) { + ReleaseCapture(); + } + + for (const ButtonState& buttonState : m_buttons) { + if (!ContainsPoint(buttonState.rect, point.x, point.y)) { + continue; + } + + ExecuteAction(buttonState.action); + break; + } + + InvalidateRect(m_hwnd, nullptr, FALSE); +} + +void ScenarioApp::ExecuteAction(ActionId action) { + switch (action) { + case ActionId::ActivateScene: + DispatchWorkspaceCommand(UIEditorWorkspaceCommandKind::ActivatePanel, "scene", "Activate Scene"); + break; + case ActionId::ActivateDocument: + DispatchWorkspaceCommand(UIEditorWorkspaceCommandKind::ActivatePanel, "document", "Activate Document"); + break; + case ActionId::ToggleTopBar: + m_showTopBar = !m_showTopBar; + m_lastResult = m_showTopBar ? "TopBar 已打开" : "TopBar 已关闭"; + break; + case ActionId::ToggleBottomBar: + m_showBottomBar = !m_showBottomBar; + m_lastResult = m_showBottomBar ? "BottomBar 已打开" : "BottomBar 已关闭"; + break; + case ActionId::ToggleTexture: + m_textureEnabled = !m_textureEnabled; + m_lastResult = m_textureEnabled ? "切到 Texture 分支" : "切到 Fallback 分支"; + break; + case ActionId::Reset: + ResetScenario(); + m_lastResult = "状态已重置"; + break; + case ActionId::Capture: + m_autoScreenshot.RequestCapture("manual_button"); + m_lastResult = "截图已排队"; + InvalidateRect(m_hwnd, nullptr, FALSE); + UpdateWindow(m_hwnd); + break; + } +} + +void ScenarioApp::DispatchWorkspaceCommand( + UIEditorWorkspaceCommandKind kind, + std::string_view panelId, + std::string_view label) { + UIEditorWorkspaceCommand command = {}; + command.kind = kind; + command.panelId = std::string(panelId); + + const UIEditorWorkspaceCommandResult result = m_controller.Dispatch(command); + m_lastResult = + std::string(label) + " -> " + + std::string(GetUIEditorWorkspaceCommandStatusName(result.status)) + + " | " + + result.message; +} + +std::string ScenarioApp::GetSelectedTabPanelId() const { + if (!m_composeFrame.dockHostLayout.tabStacks.empty()) { + return m_composeFrame.dockHostLayout.tabStacks.front().selectedPanelId; + } + + return m_controller.GetWorkspace().activePanelId; +} + +void ScenarioApp::UpdateComposeFrame() { + const auto presentations = BuildPresentationModels(); + m_composeRequest = ResolveUIEditorWorkspaceComposeRequest( + m_workspaceRect, + m_controller.GetPanelRegistry(), + m_controller.GetWorkspace(), + m_controller.GetSession(), + presentations); + + std::vector frameEvents = std::move(m_pendingInputEvents); + m_pendingInputEvents.clear(); + m_composeFrame = UpdateUIEditorWorkspaceCompose( + m_composeState, + m_workspaceRect, + m_controller.GetPanelRegistry(), + m_controller.GetWorkspace(), + m_controller.GetSession(), + presentations, + frameEvents); + + const auto* viewportFrame = + FindUIEditorWorkspaceViewportPresentationFrame(m_composeFrame, "scene"); + if (viewportFrame != nullptr) { + m_hoverHit = HitTestUIEditorViewportSlot( + viewportFrame->viewportShellFrame.slotLayout, + m_mousePosition); + } else { + m_hoverHit = {}; + } + + UpdateLastResult(viewportFrame); +} + +void ScenarioApp::UpdateLastResult( + const XCEngine::UI::Editor::UIEditorWorkspaceViewportComposeFrame* viewportFrame) { + const bool viewportVisible = viewportFrame != nullptr; + if (!viewportVisible) { + if (m_hasSnapshot && m_previousViewportVisible) { + m_lastResult = "Viewport body 已切回 DockHost placeholder"; + } + m_previousViewportVisible = false; + m_previousHovered = false; + m_previousFocused = false; + m_previousCaptured = false; + m_hasSnapshot = true; + return; + } + + const auto& inputFrame = viewportFrame->viewportShellFrame.inputFrame; + if (inputFrame.captureStarted) { + m_lastResult = "CaptureStarted"; + } else if (inputFrame.captureEnded) { + m_lastResult = "CaptureEnded"; + } else if (inputFrame.focusLost) { + m_lastResult = "FocusLost"; + } else if (inputFrame.focusGained) { + m_lastResult = "FocusGained"; + } else if (inputFrame.pointerPressedInside) { + m_lastResult = "PointerDownInside"; + } else if (inputFrame.pointerReleasedInside) { + m_lastResult = "PointerUpInside"; + } else if (inputFrame.wheelDelta != 0.0f) { + m_lastResult = "Wheel " + FormatFloat(inputFrame.wheelDelta); + } else if (m_hasSnapshot && !m_previousViewportVisible) { + m_lastResult = "Viewport body 接管成功"; + } else if (m_hasSnapshot && m_previousHovered != inputFrame.hovered) { + m_lastResult = std::string("Hover ") + BoolText(inputFrame.hovered); + } + + m_previousViewportVisible = true; + m_previousHovered = inputFrame.hovered; + m_previousFocused = inputFrame.focused; + m_previousCaptured = inputFrame.captured; + m_hasSnapshot = true; +} + +void ScenarioApp::RenderFrame() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast(std::max(1L, clientRect.right - clientRect.left)); + const float height = static_cast(std::max(1L, clientRect.bottom - clientRect.top)); + + UpdateLayoutForCurrentWindow(); + UpdateComposeFrame(); + UpdateLayoutForCurrentWindow(); + + const auto* viewportRequest = + FindUIEditorWorkspaceViewportPresentationRequest(m_composeRequest, "scene"); + const auto* viewportFrame = + FindUIEditorWorkspaceViewportPresentationFrame(m_composeFrame, "scene"); + const std::string selectedTabId = GetSelectedTabPanelId(); + const std::string selectedPresentation = + selectedTabId == "scene" && viewportFrame != nullptr ? "ViewportShell" : "Placeholder"; + const auto validation = m_controller.ValidateState(); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("WorkspaceViewportCompose"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); + + DrawCard( + drawList, + m_introRect, + "测试功能:WorkspaceCompose body 接管", + "只验证 DockHost body 被 ViewportShell 接管这条基础 contract。"); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 66.0f), + "检查 1:切到 Scene 时,中间 tab body 必须由 ViewportShell 接管;切到 Document 时,必须退回 DockHost placeholder。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 86.0f), + "检查 2:TopBar / BottomBar 只改变 viewport request size,不应破坏 workspace shell 与左右占位面板。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 106.0f), + "检查 3:hover / focus / capture 只在 Scene 可见时生效,切回 Document 后不能残留脏状态。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 126.0f), + "操作:先切 Scene,hover / click / drag 右侧中间 viewport;再切 Document,看 body 是否恢复 placeholder。", + kTextMuted, + 11.0f); + drawList.AddText( + UIPoint(m_introRect.x + 16.0f, m_introRect.y + 146.0f), + "操作:切换 TopBar / BottomBar / Texture,观察左侧 Request Size、右侧 viewport chrome、以及 workspace 外壳是否同步。", + kTextMuted, + 11.0f); + + DrawCard(drawList, m_controlsRect, "操作", "这里只放当前这个 compose contract 真正需要检查的 7 个控件。"); + for (const ButtonState& button : m_buttons) { + DrawButton(drawList, button); + } + + DrawCard(drawList, m_stateRect, "状态", "左侧直接回显当前 compose 结果,便于人工检查。"); + float stateY = m_stateRect.y + 66.0f; + auto addStateLine = [&](std::string text, const UIColor& color, float fontSize = 12.0f) { + drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY), std::move(text), color, fontSize); + stateY += 18.0f; + }; + + addStateLine("Active Panel: " + m_controller.GetWorkspace().activePanelId, kTextPrimary); + addStateLine("Selected Tab: " + selectedTabId, kTextPrimary); + addStateLine("Selected Presentation: " + selectedPresentation, kTextPrimary); + addStateLine("Viewport Visible: " + BoolText(viewportFrame != nullptr), kTextPrimary); + addStateLine( + "Texture Branch: " + std::string(m_textureEnabled ? "Texture" : "Fallback"), + kTextPrimary); + addStateLine( + "Request Size: " + + (viewportRequest != nullptr + ? FormatSize(viewportRequest->viewportShellRequest.requestedViewportSize) + : std::string("n/a")), + kTextPrimary); + addStateLine( + "Input Rect: " + + (viewportFrame != nullptr + ? FormatRect(viewportFrame->viewportShellFrame.slotLayout.inputRect) + : std::string("n/a")), + kTextMuted, + 11.0f); + addStateLine( + "Hover Hit: " + + (viewportFrame != nullptr ? DescribeHitTarget(m_hoverHit) : std::string("n/a")), + kTextPrimary); + addStateLine( + "Hover: " + + (viewportFrame != nullptr + ? BoolText(viewportFrame->viewportShellFrame.inputFrame.hovered) + : std::string("n/a")), + kTextPrimary); + addStateLine( + "Focus: " + + (viewportFrame != nullptr + ? BoolText(viewportFrame->viewportShellFrame.inputFrame.focused) + : std::string("n/a")), + kTextPrimary); + addStateLine( + "Capture: " + + (viewportFrame != nullptr + ? BoolText(viewportFrame->viewportShellFrame.inputFrame.captured) + : std::string("n/a")), + kTextPrimary); + addStateLine("Result: " + m_lastResult, kTextMuted); + addStateLine( + validation.IsValid() ? "Workspace Validation: OK" : "Workspace Validation: " + validation.message, + validation.IsValid() ? kSuccess : kDanger, + 11.0f); + + const std::string captureSummary = + m_autoScreenshot.HasPendingCapture() + ? "截图排队中..." + : (m_autoScreenshot.GetLastCaptureSummary().empty() + ? std::string("截图:F12 或按钮 -> workspace_viewport_compose/captures/") + : m_autoScreenshot.GetLastCaptureSummary()); + addStateLine(captureSummary, kTextWeak, 11.0f); + + DrawCard( + drawList, + m_previewRect, + "Preview", + "这里只有一个 WorkspaceCompose 试验场,不混入任何 editor 业务面板。"); + drawList.AddFilledRect(m_workspaceRect, kPreviewBg, 10.0f); + AppendUIEditorWorkspaceCompose(drawList, m_composeFrame); + + const bool framePresented = m_renderer.Render(drawData); + m_autoScreenshot.CaptureIfRequested( + m_renderer, + drawData, + static_cast(width), + static_cast(height), + framePresented); +} + +} // namespace + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { + return ScenarioApp().Run(hInstance, nCmdShow); +} diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index 4499a321..f6c07fed 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -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 diff --git a/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp b/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp index 79079e76..b5c7232f 100644 --- a/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_dock_host.cpp @@ -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")); } diff --git a/tests/UI/Editor/unit/test_ui_editor_workspace_compose.cpp b/tests/UI/Editor/unit/test_ui_editor_workspace_compose.cpp new file mode 100644 index 00000000..9a579e1b --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_workspace_compose.cpp @@ -0,0 +1,269 @@ +#include + +#include + +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 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 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 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 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 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); +}