diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 789e4adc..7567c671 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(XCUIEditorLib STATIC src/Core/UIEditorDockHostInteraction.cpp src/Core/UIEditorMenuModel.cpp src/Core/UIEditorMenuSession.cpp + src/Core/UIEditorPanelContentHost.cpp src/Core/UIEditorPanelHostLifecycle.cpp src/Core/UIEditorPanelRegistry.cpp src/Core/UIEditorShellCompose.cpp diff --git a/new_editor/include/XCEditor/Core/UIEditorPanelContentHost.h b/new_editor/include/XCEditor/Core/UIEditorPanelContentHost.h new file mode 100644 index 00000000..6fbace91 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorPanelContentHost.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor { + +enum class UIEditorPanelContentHostEventKind : std::uint8_t { + Mounted = 0, + Unmounted, + BoundsChanged +}; + +std::string_view GetUIEditorPanelContentHostEventKindName( + UIEditorPanelContentHostEventKind kind); + +bool IsUIEditorPanelPresentationExternallyHosted(UIEditorPanelPresentationKind kind); + +struct UIEditorPanelContentHostBinding { + std::string panelId = {}; + UIEditorPanelPresentationKind kind = UIEditorPanelPresentationKind::Placeholder; +}; + +struct UIEditorPanelContentHostMountRequest { + std::string panelId = {}; + UIEditorPanelPresentationKind kind = UIEditorPanelPresentationKind::Placeholder; + ::XCEngine::UI::UIRect bounds = {}; +}; + +struct UIEditorPanelContentHostRequest { + std::vector mountRequests = {}; +}; + +struct UIEditorPanelContentHostPanelState { + std::string panelId = {}; + UIEditorPanelPresentationKind kind = UIEditorPanelPresentationKind::Placeholder; + bool mounted = false; + ::XCEngine::UI::UIRect bounds = {}; +}; + +struct UIEditorPanelContentHostState { + std::vector panelStates = {}; +}; + +struct UIEditorPanelContentHostEvent { + UIEditorPanelContentHostEventKind kind = UIEditorPanelContentHostEventKind::Mounted; + std::string panelId = {}; + UIEditorPanelPresentationKind presentationKind = UIEditorPanelPresentationKind::Placeholder; + ::XCEngine::UI::UIRect bounds = {}; +}; + +struct UIEditorPanelContentHostFrame { + std::vector panelStates = {}; + std::vector events = {}; +}; + +const UIEditorPanelContentHostMountRequest* FindUIEditorPanelContentHostMountRequest( + const UIEditorPanelContentHostRequest& request, + std::string_view panelId); + +const UIEditorPanelContentHostPanelState* FindUIEditorPanelContentHostPanelState( + const UIEditorPanelContentHostState& state, + std::string_view panelId); + +const UIEditorPanelContentHostPanelState* FindUIEditorPanelContentHostPanelState( + const UIEditorPanelContentHostFrame& frame, + std::string_view panelId); + +UIEditorPanelContentHostRequest ResolveUIEditorPanelContentHostRequest( + const Widgets::UIEditorDockHostLayout& dockHostLayout, + const UIEditorPanelRegistry& panelRegistry, + const std::vector& bindings); + +UIEditorPanelContentHostFrame UpdateUIEditorPanelContentHost( + UIEditorPanelContentHostState& state, + const UIEditorPanelContentHostRequest& request, + const UIEditorPanelRegistry& panelRegistry, + const std::vector& bindings); + +std::vector CollectMountedUIEditorPanelContentHostPanelIds( + const UIEditorPanelContentHostFrame& frame); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Core/UIEditorPanelRegistry.h b/new_editor/include/XCEditor/Core/UIEditorPanelRegistry.h index e9bbaede..37f9a3af 100644 --- a/new_editor/include/XCEditor/Core/UIEditorPanelRegistry.h +++ b/new_editor/include/XCEditor/Core/UIEditorPanelRegistry.h @@ -9,7 +9,8 @@ namespace XCEngine::UI::Editor { enum class UIEditorPanelPresentationKind : std::uint8_t { Placeholder = 0, - ViewportShell + ViewportShell, + HostedContent }; struct UIEditorPanelDescriptor { diff --git a/new_editor/include/XCEditor/Core/UIEditorWorkspaceCompose.h b/new_editor/include/XCEditor/Core/UIEditorWorkspaceCompose.h index e103da21..2a899e84 100644 --- a/new_editor/include/XCEditor/Core/UIEditorWorkspaceCompose.h +++ b/new_editor/include/XCEditor/Core/UIEditorWorkspaceCompose.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -25,6 +26,7 @@ struct UIEditorWorkspacePanelPresentationState { }; struct UIEditorWorkspaceComposeState { + UIEditorPanelContentHostState contentHostState = {}; std::vector panelStates = {}; }; @@ -36,6 +38,7 @@ struct UIEditorWorkspaceViewportComposeRequest { struct UIEditorWorkspaceComposeRequest { Widgets::UIEditorDockHostLayout dockHostLayout = {}; + UIEditorPanelContentHostRequest contentHostRequest = {}; std::vector viewportRequests = {}; }; @@ -48,6 +51,7 @@ struct UIEditorWorkspaceViewportComposeFrame { struct UIEditorWorkspaceComposeFrame { Widgets::UIEditorDockHostLayout dockHostLayout = {}; + UIEditorPanelContentHostFrame contentHostFrame = {}; std::vector viewportFrames = {}; }; diff --git a/new_editor/src/Core/UIEditorPanelContentHost.cpp b/new_editor/src/Core/UIEditorPanelContentHost.cpp new file mode 100644 index 00000000..835c07a4 --- /dev/null +++ b/new_editor/src/Core/UIEditorPanelContentHost.cpp @@ -0,0 +1,241 @@ +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor { + +namespace { + +const ::XCEngine::UI::UIRect* FindVisiblePanelBodyRect( + const Widgets::UIEditorDockHostLayout& layout, + std::string_view panelId) { + for (const Widgets::UIEditorDockHostPanelLayout& panel : layout.panels) { + if (panel.panelId == panelId) { + return &panel.frameLayout.bodyRect; + } + } + + for (const Widgets::UIEditorDockHostTabStackLayout& tabStack : layout.tabStacks) { + if (tabStack.selectedPanelId == panelId) { + return &tabStack.contentFrameLayout.bodyRect; + } + } + + return nullptr; +} + +bool AreRectsEquivalent( + const ::XCEngine::UI::UIRect& lhs, + const ::XCEngine::UI::UIRect& rhs) { + return lhs.x == rhs.x && + lhs.y == rhs.y && + lhs.width == rhs.width && + lhs.height == rhs.height; +} + +bool SupportsBinding( + const UIEditorPanelRegistry& panelRegistry, + const UIEditorPanelContentHostBinding& binding) { + if (!IsUIEditorPanelPresentationExternallyHosted(binding.kind)) { + return false; + } + + const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(panelRegistry, binding.panelId); + return descriptor != nullptr && descriptor->presentationKind == binding.kind; +} + +UIEditorPanelContentHostPanelState* FindMutablePanelState( + UIEditorPanelContentHostState& state, + std::string_view panelId) { + for (UIEditorPanelContentHostPanelState& panelState : state.panelStates) { + if (panelState.panelId == panelId) { + return &panelState; + } + } + + return nullptr; +} + +UIEditorPanelContentHostPanelState& EnsurePanelState( + UIEditorPanelContentHostState& state, + std::string_view panelId, + UIEditorPanelPresentationKind kind) { + if (UIEditorPanelContentHostPanelState* existing = + FindMutablePanelState(state, panelId)) { + existing->kind = kind; + return *existing; + } + + UIEditorPanelContentHostPanelState panelState = {}; + panelState.panelId = std::string(panelId); + panelState.kind = kind; + state.panelStates.push_back(std::move(panelState)); + return state.panelStates.back(); +} + +} // namespace + +std::string_view GetUIEditorPanelContentHostEventKindName( + UIEditorPanelContentHostEventKind kind) { + switch (kind) { + case UIEditorPanelContentHostEventKind::Mounted: + return "Mounted"; + case UIEditorPanelContentHostEventKind::Unmounted: + return "Unmounted"; + case UIEditorPanelContentHostEventKind::BoundsChanged: + return "BoundsChanged"; + } + + return "Unknown"; +} + +bool IsUIEditorPanelPresentationExternallyHosted(UIEditorPanelPresentationKind kind) { + return kind == UIEditorPanelPresentationKind::ViewportShell || + kind == UIEditorPanelPresentationKind::HostedContent; +} + +const UIEditorPanelContentHostMountRequest* FindUIEditorPanelContentHostMountRequest( + const UIEditorPanelContentHostRequest& request, + std::string_view panelId) { + for (const UIEditorPanelContentHostMountRequest& mountRequest : request.mountRequests) { + if (mountRequest.panelId == panelId) { + return &mountRequest; + } + } + + return nullptr; +} + +const UIEditorPanelContentHostPanelState* FindUIEditorPanelContentHostPanelState( + const UIEditorPanelContentHostState& state, + std::string_view panelId) { + for (const UIEditorPanelContentHostPanelState& panelState : state.panelStates) { + if (panelState.panelId == panelId) { + return &panelState; + } + } + + return nullptr; +} + +const UIEditorPanelContentHostPanelState* FindUIEditorPanelContentHostPanelState( + const UIEditorPanelContentHostFrame& frame, + std::string_view panelId) { + for (const UIEditorPanelContentHostPanelState& panelState : frame.panelStates) { + if (panelState.panelId == panelId) { + return &panelState; + } + } + + return nullptr; +} + +UIEditorPanelContentHostRequest ResolveUIEditorPanelContentHostRequest( + const Widgets::UIEditorDockHostLayout& dockHostLayout, + const UIEditorPanelRegistry& panelRegistry, + const std::vector& bindings) { + UIEditorPanelContentHostRequest request = {}; + for (const UIEditorPanelContentHostBinding& binding : bindings) { + if (!SupportsBinding(panelRegistry, binding)) { + continue; + } + + const ::XCEngine::UI::UIRect* bodyRect = + FindVisiblePanelBodyRect(dockHostLayout, binding.panelId); + if (bodyRect == nullptr) { + continue; + } + + UIEditorPanelContentHostMountRequest mountRequest = {}; + mountRequest.panelId = binding.panelId; + mountRequest.kind = binding.kind; + mountRequest.bounds = *bodyRect; + request.mountRequests.push_back(std::move(mountRequest)); + } + + return request; +} + +UIEditorPanelContentHostFrame UpdateUIEditorPanelContentHost( + UIEditorPanelContentHostState& state, + const UIEditorPanelContentHostRequest& request, + const UIEditorPanelRegistry& panelRegistry, + const std::vector& bindings) { + UIEditorPanelContentHostFrame frame = {}; + std::unordered_set supportedPanelIds = {}; + for (const UIEditorPanelContentHostBinding& binding : bindings) { + if (!SupportsBinding(panelRegistry, binding)) { + continue; + } + + supportedPanelIds.insert(binding.panelId); + EnsurePanelState(state, binding.panelId, binding.kind); + } + + state.panelStates.erase( + std::remove_if( + state.panelStates.begin(), + state.panelStates.end(), + [&](const UIEditorPanelContentHostPanelState& panelState) { + return !supportedPanelIds.contains(panelState.panelId); + }), + state.panelStates.end()); + + for (UIEditorPanelContentHostPanelState& panelState : state.panelStates) { + const UIEditorPanelContentHostMountRequest* mountRequest = + FindUIEditorPanelContentHostMountRequest(request, panelState.panelId); + + const bool wasMounted = panelState.mounted; + const ::XCEngine::UI::UIRect previousBounds = panelState.bounds; + + panelState.mounted = mountRequest != nullptr; + panelState.bounds = mountRequest != nullptr ? mountRequest->bounds : ::XCEngine::UI::UIRect{}; + if (mountRequest != nullptr) { + panelState.kind = mountRequest->kind; + } + + if (!wasMounted && panelState.mounted) { + frame.events.push_back({ + UIEditorPanelContentHostEventKind::Mounted, + panelState.panelId, + panelState.kind, + panelState.bounds + }); + } else if (wasMounted && !panelState.mounted) { + frame.events.push_back({ + UIEditorPanelContentHostEventKind::Unmounted, + panelState.panelId, + panelState.kind, + previousBounds + }); + } else if (panelState.mounted && + !AreRectsEquivalent(previousBounds, panelState.bounds)) { + frame.events.push_back({ + UIEditorPanelContentHostEventKind::BoundsChanged, + panelState.panelId, + panelState.kind, + panelState.bounds + }); + } + } + + frame.panelStates = state.panelStates; + return frame; +} + +std::vector CollectMountedUIEditorPanelContentHostPanelIds( + const UIEditorPanelContentHostFrame& frame) { + std::vector panelIds = {}; + for (const UIEditorPanelContentHostPanelState& panelState : frame.panelStates) { + if (panelState.mounted) { + panelIds.push_back(panelState.panelId); + } + } + return panelIds; +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Core/UIEditorWorkspaceCompose.cpp b/new_editor/src/Core/UIEditorWorkspaceCompose.cpp index 10b71185..87ae9782 100644 --- a/new_editor/src/Core/UIEditorWorkspaceCompose.cpp +++ b/new_editor/src/Core/UIEditorWorkspaceCompose.cpp @@ -51,6 +51,28 @@ bool SupportsExternalViewportPresentation( descriptor->presentationKind == UIEditorPanelPresentationKind::ViewportShell; } +std::vector BuildContentHostBindings( + const UIEditorPanelRegistry& panelRegistry, + const std::vector& presentations) { + std::vector bindings = {}; + bindings.reserve(presentations.size()); + for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { + const UIEditorPanelDescriptor* descriptor = + FindUIEditorPanelDescriptor(panelRegistry, presentation.panelId); + if (descriptor == nullptr || + descriptor->presentationKind != presentation.kind || + !IsUIEditorPanelPresentationExternallyHosted(presentation.kind)) { + continue; + } + + UIEditorPanelContentHostBinding binding = {}; + binding.panelId = presentation.panelId; + binding.kind = presentation.kind; + bindings.push_back(std::move(binding)); + } + return bindings; +} + UIEditorWorkspacePanelPresentationState& EnsurePanelState( UIEditorWorkspaceComposeState& state, std::string_view panelId) { @@ -99,24 +121,6 @@ void TrimObsoleteViewportPresentationStates( 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( @@ -177,23 +181,32 @@ UIEditorWorkspaceComposeRequest ResolveUIEditorWorkspaceComposeRequest( session, dockHostState, dockHostMetrics); + const std::vector contentHostBindings = + BuildContentHostBindings(panelRegistry, presentations); + request.contentHostRequest = ResolveUIEditorPanelContentHostRequest( + request.dockHostLayout, + panelRegistry, + contentHostBindings); - for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { - if (!SupportsExternalViewportPresentation(panelRegistry, presentation)) { + for (const UIEditorPanelContentHostMountRequest& mountRequest : + request.contentHostRequest.mountRequests) { + if (mountRequest.kind != UIEditorPanelPresentationKind::ViewportShell) { continue; } - const auto* bodyRect = FindVisiblePanelBodyRect(request.dockHostLayout, presentation.panelId); - if (bodyRect == nullptr) { + const UIEditorWorkspacePanelPresentationModel* presentation = + FindUIEditorWorkspacePanelPresentationModel(presentations, mountRequest.panelId); + if (presentation == nullptr || + !SupportsExternalViewportPresentation(panelRegistry, *presentation)) { continue; } UIEditorWorkspaceViewportComposeRequest viewportRequest = {}; - viewportRequest.panelId = presentation.panelId; - viewportRequest.bounds = *bodyRect; + viewportRequest.panelId = mountRequest.panelId; + viewportRequest.bounds = mountRequest.bounds; viewportRequest.viewportShellRequest = ResolveUIEditorViewportShellRequest( - *bodyRect, - presentation.viewportShellModel.spec); + mountRequest.bounds, + presentation->viewportShellModel.spec); request.viewportRequests.push_back(std::move(viewportRequest)); } @@ -218,6 +231,18 @@ UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( session, dockHostState, dockHostMetrics); + const std::vector contentHostBindings = + BuildContentHostBindings(panelRegistry, presentations); + const UIEditorPanelContentHostRequest contentHostRequest = + ResolveUIEditorPanelContentHostRequest( + frame.dockHostLayout, + panelRegistry, + contentHostBindings); + frame.contentHostFrame = UpdateUIEditorPanelContentHost( + state.contentHostState, + contentHostRequest, + panelRegistry, + contentHostBindings); TrimObsoleteViewportPresentationStates(state, panelRegistry, presentations); for (const UIEditorWorkspacePanelPresentationModel& presentation : presentations) { @@ -225,8 +250,9 @@ UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( continue; } - const auto* bodyRect = FindVisiblePanelBodyRect(frame.dockHostLayout, presentation.panelId); - if (bodyRect == nullptr) { + const UIEditorPanelContentHostPanelState* contentHostPanelState = + FindUIEditorPanelContentHostPanelState(frame.contentHostFrame, presentation.panelId); + if (contentHostPanelState == nullptr || !contentHostPanelState->mounted) { ResetHiddenViewportPresentationState(state, presentation.panelId); continue; } @@ -236,11 +262,11 @@ UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( UIEditorWorkspaceViewportComposeFrame viewportFrame = {}; viewportFrame.panelId = presentation.panelId; - viewportFrame.bounds = *bodyRect; + viewportFrame.bounds = contentHostPanelState->bounds; viewportFrame.viewportShellModel = presentation.viewportShellModel; viewportFrame.viewportShellFrame = UpdateUIEditorViewportShell( panelState.viewportShellState, - *bodyRect, + contentHostPanelState->bounds, presentation.viewportShellModel, inputEvents); frame.viewportFrames.push_back(std::move(viewportFrame)); @@ -251,12 +277,7 @@ UIEditorWorkspaceComposeFrame UpdateUIEditorWorkspaceCompose( 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; + return CollectMountedUIEditorPanelContentHostPanelIds(frame.contentHostFrame); } void AppendUIEditorWorkspaceCompose( diff --git a/tests/UI/Editor/integration/README.md b/tests/UI/Editor/integration/README.md index 797d02ab..77757c4e 100644 --- a/tests/UI/Editor/integration/README.md +++ b/tests/UI/Editor/integration/README.md @@ -18,6 +18,7 @@ Layout: - `shell/editor_shell_compose/`: editor root shell compose only - `shell/editor_shell_interaction/`: editor root shell interaction only - `shell/dock_host_basic/`: DockHost interaction contract only +- `shell/panel_content_host_basic/`: external panel body mount/switch/unmount only - `shell/menu_bar_basic/`: menu bar open/close/hover/dispatch only - `shell/context_menu_basic/`: context menu root/submenu/dismiss/dispatch only - `shell/panel_frame_basic/`: panel frame layout/state/hit-test only @@ -55,6 +56,11 @@ Scenarios: Executable: `XCUIEditorDockHostBasicValidation.exe` Scope: `UpdateUIEditorDockHostInteraction(...)` basic contract only; splitter drag, tab activate/close, standalone panel activate/close, pointer capture/release request, workspace active-panel sync +- `editor.shell.panel_content_host_basic` + Build target: `editor_ui_panel_content_host_basic_validation` + Executable: `XCUIEditorPanelContentHostBasicValidation.exe` + Scope: external HostedContent body mount/switch/unmount only; DockHost 只画 frame chrome,真实 body 由 content host 接管 + - `editor.shell.menu_bar_basic` Build target: `editor_ui_menu_bar_basic_validation` Executable: `XCUIEditorMenuBarBasicValidation.exe` @@ -150,6 +156,9 @@ Selected controls: - `shell/dock_host_basic/` Drag `root-split`, click `Document A`, close `Document B`, click `Details`, close `Console`, inspect `Hover / Result / Active Panel / Visible Panels / Capture / split ratio`, press `Reset`, `Capture`, or `F12`. +- `shell/panel_content_host_basic/` + Click `Activate Doc A / Activate Doc B / Activate Console / Close Inspector / Open Inspector`, inspect `Mounted Panels / Events` 和蓝色 external body,按 `Capture` 或 `F12`。 + - `shell/menu_bar_basic/` Click `File / Window / Layout`, move the mouse across menu items, click outside the menu or press `Esc`, press `F12`. diff --git a/tests/UI/Editor/integration/shell/CMakeLists.txt b/tests/UI/Editor/integration/shell/CMakeLists.txt index e89ee6a2..e92d49a7 100644 --- a/tests/UI/Editor/integration/shell/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/CMakeLists.txt @@ -13,6 +13,9 @@ endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/dock_host_basic/CMakeLists.txt") add_subdirectory(dock_host_basic) endif() +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/panel_content_host_basic/CMakeLists.txt") + add_subdirectory(panel_content_host_basic) +endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/status_bar_basic/CMakeLists.txt") add_subdirectory(status_bar_basic) endif() diff --git a/tests/UI/Editor/integration/shell/panel_content_host_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/panel_content_host_basic/CMakeLists.txt new file mode 100644 index 00000000..9c10c45e --- /dev/null +++ b/tests/UI/Editor/integration/shell/panel_content_host_basic/CMakeLists.txt @@ -0,0 +1,31 @@ +add_executable(editor_ui_panel_content_host_basic_validation WIN32 + main.cpp +) + +target_include_directories(editor_ui_panel_content_host_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/new_editor/include + ${CMAKE_SOURCE_DIR}/new_editor/app + ${CMAKE_SOURCE_DIR}/new_editor/src +) + +target_compile_definitions(editor_ui_panel_content_host_basic_validation PRIVATE + UNICODE + _UNICODE + XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}" +) + +if(MSVC) + target_compile_options(editor_ui_panel_content_host_basic_validation PRIVATE /utf-8 /FS) + set_property(TARGET editor_ui_panel_content_host_basic_validation PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +target_link_libraries(editor_ui_panel_content_host_basic_validation PRIVATE + XCUIEditorLib + XCUIEditorHost +) + +set_target_properties(editor_ui_panel_content_host_basic_validation PROPERTIES + OUTPUT_NAME "XCUIEditorPanelContentHostBasicValidation" +) diff --git a/tests/UI/Editor/integration/shell/panel_content_host_basic/captures/.gitkeep b/tests/UI/Editor/integration/shell/panel_content_host_basic/captures/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/UI/Editor/integration/shell/panel_content_host_basic/captures/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/UI/Editor/integration/shell/panel_content_host_basic/main.cpp b/tests/UI/Editor/integration/shell/panel_content_host_basic/main.cpp new file mode 100644 index 00000000..1b9cc069 --- /dev/null +++ b/tests/UI/Editor/integration/shell/panel_content_host_basic/main.cpp @@ -0,0 +1,610 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include +#include "Host/AutoScreenshot.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::UIPoint; +using XCEngine::UI::UIRect; +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::CollectUIEditorWorkspaceComposeExternalBodyPanelIds; +using XCEngine::UI::Editor::GetUIEditorPanelContentHostEventKindName; +using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName; +using XCEngine::UI::Editor::Host::AutoScreenshotController; +using XCEngine::UI::Editor::Host::NativeRenderer; +using XCEngine::UI::Editor::UIEditorPanelContentHostPanelState; +using XCEngine::UI::Editor::UIEditorPanelPresentationKind; +using XCEngine::UI::Editor::UIEditorPanelRegistry; +using XCEngine::UI::Editor::UIEditorWorkspaceCommand; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind; +using XCEngine::UI::Editor::UIEditorWorkspaceComposeFrame; +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; + +constexpr const wchar_t* kWindowClassName = L"XCUIEditorPanelContentHostBasicValidation"; +constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Panel Content Host"; + +constexpr UIColor kWindowBg(0.12f, 0.12f, 0.12f, 1.0f); +constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f); +constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 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 kSuccess(0.46f, 0.72f, 0.50f, 1.0f); +constexpr UIColor kWarning(0.82f, 0.68f, 0.36f, 1.0f); +constexpr UIColor kDanger(0.78f, 0.35f, 0.35f, 1.0f); +constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f); +constexpr UIColor kButtonHover(0.33f, 0.33f, 0.33f, 1.0f); +constexpr UIColor kButtonBorder(0.46f, 0.46f, 0.46f, 1.0f); +constexpr UIColor kMountedFill(0.22f, 0.28f, 0.36f, 1.0f); +constexpr UIColor kMountedBorder(0.66f, 0.76f, 0.86f, 1.0f); + +enum class ActionId : unsigned char { + ActivateDocA = 0, + ActivateDocB, + ActivateConsole, + ActivateInspector, + CloseInspector, + OpenInspector, + Reset, + Capture +}; + +struct ButtonState { + ActionId action = ActionId::ActivateDocA; + std::string label = {}; + UIRect rect = {}; + bool hovered = false; +}; + +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; +} + +UIEditorPanelRegistry BuildPanelRegistry() { + UIEditorPanelRegistry registry = {}; + registry.panels = { + { "doc-a", "Document A", UIEditorPanelPresentationKind::HostedContent, false, true, true }, + { "doc-b", "Document B", UIEditorPanelPresentationKind::HostedContent, false, true, true }, + { "console", "Console", UIEditorPanelPresentationKind::Placeholder, true, true, true }, + { "inspector", "Inspector", UIEditorPanelPresentationKind::HostedContent, false, true, true } + }; + return registry; +} + +UIEditorWorkspaceModel BuildWorkspace() { + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "root", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.68f, + BuildUIEditorWorkspaceTabStack( + "documents", + { + BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A"), + BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B"), + BuildUIEditorWorkspacePanel("console-node", "console", "Console", true) + }, + 0u), + BuildUIEditorWorkspacePanel("inspector-node", "inspector", "Inspector")); + workspace.activePanelId = "doc-a"; + return workspace; +} + +std::vector BuildPresentationModels() { + std::vector models = {}; + for (std::string_view panelId : { "doc-a", "doc-b", "inspector" }) { + UIEditorWorkspacePanelPresentationModel model = {}; + model.panelId = std::string(panelId); + model.kind = UIEditorPanelPresentationKind::HostedContent; + models.push_back(std::move(model)); + } + return models; +} + +std::string JoinExternalBodyPanelIds(const UIEditorWorkspaceComposeFrame& frame) { + const auto panelIds = CollectUIEditorWorkspaceComposeExternalBodyPanelIds(frame); + if (panelIds.empty()) { + return "(none)"; + } + + std::ostringstream stream = {}; + for (std::size_t index = 0; index < panelIds.size(); ++index) { + if (index > 0u) { + stream << ", "; + } + stream << panelIds[index]; + } + return stream.str(); +} + +std::string FormatEvents(const UIEditorWorkspaceComposeFrame& frame) { + if (frame.contentHostFrame.events.empty()) { + return "(none)"; + } + + std::ostringstream stream = {}; + for (std::size_t index = 0; index < frame.contentHostFrame.events.size(); ++index) { + if (index > 0u) { + stream << " | "; + } + stream << GetUIEditorPanelContentHostEventKindName(frame.contentHostFrame.events[index].kind) + << ":" << frame.contentHostFrame.events[index].panelId; + } + return stream.str(); +} + +std::string DescribeMountedState( + const UIEditorWorkspaceComposeFrame& frame, + std::string_view panelId) { + for (const UIEditorPanelContentHostPanelState& panelState : frame.contentHostFrame.panelStates) { + if (panelState.panelId != panelId) { + continue; + } + + if (!panelState.mounted) { + return std::string(panelId) + ": unmounted"; + } + + std::ostringstream stream = {}; + stream << panelId << ": mounted @ " + << static_cast(panelState.bounds.width) << "x" + << static_cast(panelState.bounds.height); + return stream.str(); + } + + return std::string(panelId) + ": missing"; +} + +void DrawCard( + UIDrawList& drawList, + const UIRect& rect, + std::string_view title, + std::string_view subtitle = {}) { + drawList.AddFilledRect(rect, kCardBg, 12.0f); + drawList.AddRectOutline(rect, kCardBorder, 1.0f, 12.0f); + drawList.AddText(UIPoint(rect.x + 18.0f, rect.y + 16.0f), std::string(title), kTextPrimary, 17.0f); + if (!subtitle.empty()) { + drawList.AddText(UIPoint(rect.x + 18.0f, rect.y + 42.0f), std::string(subtitle), kTextMuted, 12.0f); + } +} + +void DrawButton(UIDrawList& drawList, const ButtonState& button) { + drawList.AddFilledRect(button.rect, button.hovered ? kButtonHover : 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); +} + +class ScenarioApp { +public: + int 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); + } + +private: + static LRESULT CALLBACK 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->OnResize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + case WM_PAINT: + if (app != nullptr) { + PAINTSTRUCT paintStruct = {}; + BeginPaint(hwnd, &paintStruct); + app->RenderFrame(); + EndPaint(hwnd, &paintStruct); + return 0; + } + break; + case WM_MOUSEMOVE: + if (app != nullptr) { + app->HandleMouseMove( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandleClick( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr && wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + return 0; + } + break; + case WM_ERASEBKGND: + return 1; + case WM_DESTROY: + if (app != nullptr) { + app->m_hwnd = nullptr; + } + PostQuitMessage(0); + return 0; + default: + break; + } + + return DefWindowProcW(hwnd, message, wParam, lParam); + } + + bool Initialize(HINSTANCE hInstance, int nCmdShow) { + m_hInstance = hInstance; + + 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, + 1460, + 920, + nullptr, + nullptr, + hInstance, + this); + if (m_hwnd == nullptr) { + return false; + } + + ShowWindow(m_hwnd, nCmdShow); + UpdateWindow(m_hwnd); + + if (!m_renderer.Initialize(m_hwnd)) { + return false; + } + + m_captureRoot = + ResolveRepoRootPath() / + "tests/UI/Editor/integration/shell/panel_content_host_basic/captures"; + m_autoScreenshot.Initialize(m_captureRoot); + ResetScenario(); + return true; + } + + void Shutdown() { + m_autoScreenshot.Shutdown(); + m_renderer.Shutdown(); + + if (m_hwnd != nullptr) { + DestroyWindow(m_hwnd); + m_hwnd = nullptr; + } + + if (m_windowClassAtom != 0) { + UnregisterClassW(kWindowClassName, m_hInstance); + m_windowClassAtom = 0; + } + } + + void ResetScenario() { + m_controller = + BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + m_composeState = {}; + m_lastStatus = "Ready"; + m_lastMessage = + "当前先看 doc-a 与 inspector:两者都会 mount;切到 Console 后,tab body 会回退成 placeholder。"; + UpdateComposeFrame(); + } + + void UpdateLayout() { + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(clientRect.right - clientRect.left, 1L)); + const float height = static_cast((std::max)(clientRect.bottom - clientRect.top, 1L)); + constexpr float padding = 20.0f; + constexpr float leftWidth = 430.0f; + + m_introRect = UIRect(padding, padding, leftWidth, 206.0f); + m_controlsRect = UIRect(padding, 242.0f, leftWidth, 208.0f); + m_stateRect = UIRect(padding, 466.0f, leftWidth, height - 486.0f); + m_previewRect = UIRect(leftWidth + padding * 2.0f, padding, width - leftWidth - padding * 3.0f, height - padding * 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 buttonWidth = (m_controlsRect.width - 32.0f - 16.0f) * 0.5f; + const float buttonLeft = m_controlsRect.x + 16.0f; + const float buttonTop = m_controlsRect.y + 62.0f; + const float buttonHeight = 34.0f; + const float rowGap = 10.0f; + m_buttons = { + { ActionId::ActivateDocA, "Activate Doc A", UIRect(buttonLeft, buttonTop, buttonWidth, buttonHeight), false }, + { ActionId::ActivateDocB, "Activate Doc B", UIRect(buttonLeft + buttonWidth + 16.0f, buttonTop, buttonWidth, buttonHeight), false }, + { ActionId::ActivateConsole, "Activate Console", UIRect(buttonLeft, buttonTop + buttonHeight + rowGap, buttonWidth, buttonHeight), false }, + { ActionId::ActivateInspector, "Activate Inspector", UIRect(buttonLeft + buttonWidth + 16.0f, buttonTop + buttonHeight + rowGap, buttonWidth, buttonHeight), false }, + { ActionId::CloseInspector, "Close Inspector", UIRect(buttonLeft, buttonTop + (buttonHeight + rowGap) * 2.0f, buttonWidth, buttonHeight), false }, + { ActionId::OpenInspector, "Open Inspector", UIRect(buttonLeft + buttonWidth + 16.0f, buttonTop + (buttonHeight + rowGap) * 2.0f, buttonWidth, buttonHeight), false }, + { ActionId::Reset, "Reset", UIRect(buttonLeft, buttonTop + (buttonHeight + rowGap) * 3.0f, buttonWidth, buttonHeight), false }, + { ActionId::Capture, "Capture(F12)", UIRect(buttonLeft + buttonWidth + 16.0f, buttonTop + (buttonHeight + rowGap) * 3.0f, buttonWidth, buttonHeight), false } + }; + } + + void OnResize(UINT width, UINT height) { + m_renderer.Resize(width, height); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleMouseMove(float x, float y) { + UpdateLayout(); + for (ButtonState& button : m_buttons) { + button.hovered = ContainsPoint(button.rect, x, y); + } + } + + void HandleClick(float x, float y) { + UpdateLayout(); + for (const ButtonState& button : m_buttons) { + if (ContainsPoint(button.rect, x, y)) { + ExecuteAction(button.action); + return; + } + } + } + + void ExecuteAction(ActionId action) { + if (action == ActionId::Reset) { + ResetScenario(); + return; + } + + if (action == ActionId::Capture) { + m_autoScreenshot.RequestCapture("manual_button"); + m_lastStatus = "Ready"; + m_lastMessage = + "截图已排队,输出到 tests/UI/Editor/integration/shell/panel_content_host_basic/captures/。"; + return; + } + + UIEditorWorkspaceCommand command = {}; + switch (action) { + case ActionId::ActivateDocA: + command.kind = UIEditorWorkspaceCommandKind::ActivatePanel; + command.panelId = "doc-a"; + break; + case ActionId::ActivateDocB: + command.kind = UIEditorWorkspaceCommandKind::ActivatePanel; + command.panelId = "doc-b"; + break; + case ActionId::ActivateConsole: + command.kind = UIEditorWorkspaceCommandKind::ActivatePanel; + command.panelId = "console"; + break; + case ActionId::ActivateInspector: + command.kind = UIEditorWorkspaceCommandKind::ActivatePanel; + command.panelId = "inspector"; + break; + case ActionId::CloseInspector: + command.kind = UIEditorWorkspaceCommandKind::ClosePanel; + command.panelId = "inspector"; + break; + case ActionId::OpenInspector: + command.kind = UIEditorWorkspaceCommandKind::OpenPanel; + command.panelId = "inspector"; + break; + default: + return; + } + + const auto result = m_controller.Dispatch(command); + m_lastStatus = std::string(GetUIEditorWorkspaceCommandStatusName(result.status)); + m_lastMessage = result.message; + UpdateComposeFrame(); + } + + void UpdateComposeFrame() { + UpdateLayout(); + m_composeFrame = UpdateUIEditorWorkspaceCompose( + m_composeState, + m_workspaceRect, + m_controller.GetPanelRegistry(), + m_controller.GetWorkspace(), + m_controller.GetSession(), + BuildPresentationModels(), + {}); + } + + void RenderFrame() { + UpdateComposeFrame(); + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(clientRect.right - clientRect.left, 1L)); + const float height = static_cast((std::max)(clientRect.bottom - clientRect.top, 1L)); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("PanelContentHostBasic"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); + + DrawCard(drawList, m_introRect, "这个测试验证什么功能", "只验证 Editor panel content host contract,不做业务逻辑。"); + drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 72.0f), "1. 验证 HostedContent panel 会正式接管 DockHost body,而不是继续画 placeholder。", kTextPrimary, 12.0f); + drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 94.0f), "2. 验证 tab 切换时,旧 body unmount,新 body mount。", kTextPrimary, 12.0f); + drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 116.0f), "3. 验证切到 Console 这种 placeholder panel 后,external host 会退出。", kTextPrimary, 12.0f); + drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 138.0f), "4. 验证 standalone HostedContent close/open 时,会发生 unmount / remount。", kTextPrimary, 12.0f); + drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 164.0f), "建议操作:Doc A -> Doc B -> Console -> Open/Close Inspector,观察 Mounted Panels 和 Events。", kTextWeak, 11.0f); + + DrawCard(drawList, m_controlsRect, "操作", "只保留内容承载 contract 必要按钮。"); + for (const ButtonState& button : m_buttons) { + DrawButton(drawList, button); + } + + DrawCard(drawList, m_stateRect, "状态", "重点检查 mounted panel 集合和本帧 mount events。"); + float stateY = m_stateRect.y + 66.0f; + auto addStateLine = [&](std::string label, std::string value, const UIColor& color, float fontSize = 12.0f) { + drawList.AddText(UIPoint(m_stateRect.x + 18.0f, stateY), std::move(label) + ": " + std::move(value), color, fontSize); + stateY += 20.0f; + }; + + const UIColor resultColor = + m_lastStatus == "Rejected" ? kDanger : + (m_lastStatus == "Ready" ? kWarning : kSuccess); + addStateLine("Active Panel", m_controller.GetWorkspace().activePanelId, kTextPrimary, 11.0f); + addStateLine("Mounted Panels", JoinExternalBodyPanelIds(m_composeFrame), kSuccess, 11.0f); + addStateLine("Events", FormatEvents(m_composeFrame), kWarning, 11.0f); + addStateLine("Result", m_lastStatus, resultColor); + drawList.AddText(UIPoint(m_stateRect.x + 18.0f, stateY + 4.0f), m_lastMessage, kTextMuted, 11.0f); + stateY += 32.0f; + addStateLine("doc-a", DescribeMountedState(m_composeFrame, "doc-a"), kTextWeak, 11.0f); + addStateLine("doc-b", DescribeMountedState(m_composeFrame, "doc-b"), kTextWeak, 11.0f); + addStateLine("inspector", DescribeMountedState(m_composeFrame, "inspector"), kTextWeak, 11.0f); + addStateLine( + "Screenshot", + m_autoScreenshot.HasPendingCapture() + ? "截图排队中..." + : (m_autoScreenshot.GetLastCaptureSummary().empty() + ? std::string("F12 或 Capture -> captures/") + : m_autoScreenshot.GetLastCaptureSummary()), + kTextWeak, + 11.0f); + + DrawCard(drawList, m_previewRect, "Preview", "DockHost 画壳;蓝色内容块是 external content host 实际挂进去的 body。"); + AppendUIEditorWorkspaceCompose(drawList, m_composeFrame); + for (const UIEditorPanelContentHostPanelState& panelState : m_composeFrame.contentHostFrame.panelStates) { + if (!panelState.mounted || + panelState.kind != UIEditorPanelPresentationKind::HostedContent) { + continue; + } + + drawList.AddFilledRect(panelState.bounds, kMountedFill, 8.0f); + drawList.AddRectOutline(panelState.bounds, kMountedBorder, 2.0f, 8.0f); + drawList.AddText( + UIPoint(panelState.bounds.x + 16.0f, panelState.bounds.y + 16.0f), + panelState.panelId, + kTextPrimary, + 18.0f); + drawList.AddText( + UIPoint(panelState.bounds.x + 16.0f, panelState.bounds.y + 44.0f), + "External Content Host", + kTextMuted, + 12.0f); + drawList.AddText( + UIPoint(panelState.bounds.x + 16.0f, panelState.bounds.y + 66.0f), + "Mounted Body owned outside DockHost placeholder path.", + kTextWeak, + 11.0f); + } + + const bool framePresented = m_renderer.Render(drawData); + m_autoScreenshot.CaptureIfRequested( + m_renderer, + drawData, + static_cast(width), + static_cast(height), + framePresented); + } + + HINSTANCE m_hInstance = nullptr; + HWND m_hwnd = nullptr; + ATOM m_windowClassAtom = 0; + NativeRenderer m_renderer = {}; + AutoScreenshotController m_autoScreenshot = {}; + std::filesystem::path m_captureRoot = {}; + UIEditorWorkspaceController m_controller = {}; + UIEditorWorkspaceComposeState m_composeState = {}; + UIEditorWorkspaceComposeFrame m_composeFrame = {}; + std::string m_lastStatus = {}; + std::string m_lastMessage = {}; + UIRect m_introRect = {}; + UIRect m_controlsRect = {}; + UIRect m_stateRect = {}; + UIRect m_previewRect = {}; + UIRect m_workspaceRect = {}; + std::vector m_buttons = {}; +}; + +} // 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 eeb4288b..9c6ac080 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -9,6 +9,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES test_ui_editor_menu_session.cpp test_ui_editor_menu_bar.cpp test_ui_editor_menu_popup.cpp + test_ui_editor_panel_content_host.cpp test_ui_editor_panel_host_lifecycle.cpp test_ui_editor_panel_registry.cpp test_ui_editor_shell_compose.cpp diff --git a/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp b/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp new file mode 100644 index 00000000..0de44e08 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_panel_content_host.cpp @@ -0,0 +1,181 @@ +#include + +#include +#include +#include + +namespace { + +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; +using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; +using XCEngine::UI::Editor::CollectMountedUIEditorPanelContentHostPanelIds; +using XCEngine::UI::Editor::FindUIEditorPanelContentHostMountRequest; +using XCEngine::UI::Editor::FindUIEditorPanelContentHostPanelState; +using XCEngine::UI::Editor::GetUIEditorPanelContentHostEventKindName; +using XCEngine::UI::Editor::ResolveUIEditorPanelContentHostRequest; +using XCEngine::UI::Editor::UIEditorPanelContentHostBinding; +using XCEngine::UI::Editor::UIEditorPanelContentHostState; +using XCEngine::UI::Editor::UIEditorPanelPresentationKind; +using XCEngine::UI::Editor::UIEditorPanelRegistry; +using XCEngine::UI::Editor::UIEditorWorkspaceModel; +using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; +using XCEngine::UI::Editor::UpdateUIEditorPanelContentHost; +using XCEngine::UI::Editor::Widgets::BuildUIEditorDockHostLayout; + +UIEditorPanelRegistry BuildPanelRegistry() { + UIEditorPanelRegistry registry = {}; + registry.panels = { + { "doc-a", "Document A", UIEditorPanelPresentationKind::HostedContent, false, true, true }, + { "doc-b", "Document B", UIEditorPanelPresentationKind::HostedContent, false, true, true }, + { "console", "Console", UIEditorPanelPresentationKind::Placeholder, true, true, true }, + { "inspector", "Inspector", UIEditorPanelPresentationKind::HostedContent, false, true, true } + }; + return registry; +} + +UIEditorWorkspaceModel BuildWorkspace() { + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "root", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.68f, + BuildUIEditorWorkspaceTabStack( + "documents", + { + BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A"), + BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B"), + BuildUIEditorWorkspacePanel("console-node", "console", "Console", true) + }, + 1u), + BuildUIEditorWorkspacePanel("inspector-node", "inspector", "Inspector")); + workspace.activePanelId = "doc-b"; + return workspace; +} + +std::vector BuildBindings() { + return { + { "doc-a", UIEditorPanelPresentationKind::HostedContent }, + { "doc-b", UIEditorPanelPresentationKind::HostedContent }, + { "console", UIEditorPanelPresentationKind::Placeholder }, + { "inspector", UIEditorPanelPresentationKind::HostedContent } + }; +} + +std::vector FormatEvents( + const std::vector& events) { + std::vector formatted = {}; + formatted.reserve(events.size()); + for (const auto& event : events) { + formatted.push_back( + std::string(GetUIEditorPanelContentHostEventKindName(event.kind)) + ":" + event.panelId); + } + return formatted; +} + +} // namespace + +TEST(UIEditorPanelContentHostTest, ResolveRequestMountsSelectedHostedTabAndStandaloneHostedPanel) { + const UIEditorPanelRegistry registry = BuildPanelRegistry(); + const UIEditorWorkspaceModel workspace = BuildWorkspace(); + const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + const auto layout = BuildUIEditorDockHostLayout( + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + registry, + workspace, + session); + + const auto request = ResolveUIEditorPanelContentHostRequest( + layout, + registry, + BuildBindings()); + + ASSERT_EQ(request.mountRequests.size(), 2u); + EXPECT_NE(FindUIEditorPanelContentHostMountRequest(request, "doc-b"), nullptr); + EXPECT_NE(FindUIEditorPanelContentHostMountRequest(request, "inspector"), nullptr); + EXPECT_EQ(FindUIEditorPanelContentHostMountRequest(request, "doc-a"), nullptr); + EXPECT_EQ(FindUIEditorPanelContentHostMountRequest(request, "console"), nullptr); +} + +TEST(UIEditorPanelContentHostTest, UpdateEmitsMountedAndUnmountedWhenHostedTabSelectionChanges) { + const UIEditorPanelRegistry registry = BuildPanelRegistry(); + UIEditorWorkspaceModel workspace = BuildWorkspace(); + const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + + UIEditorPanelContentHostState state = {}; + auto layout = BuildUIEditorDockHostLayout( + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + registry, + workspace, + session); + auto request = ResolveUIEditorPanelContentHostRequest(layout, registry, BuildBindings()); + const auto initialFrame = UpdateUIEditorPanelContentHost( + state, + request, + registry, + BuildBindings()); + EXPECT_EQ( + FormatEvents(initialFrame.events), + std::vector({ "Mounted:doc-b", "Mounted:inspector" })); + EXPECT_EQ( + CollectMountedUIEditorPanelContentHostPanelIds(initialFrame), + std::vector({ "doc-b", "inspector" })); + + workspace.root.children[0].selectedTabIndex = 0u; + workspace.activePanelId = "doc-a"; + layout = BuildUIEditorDockHostLayout( + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + registry, + workspace, + session); + request = ResolveUIEditorPanelContentHostRequest(layout, registry, BuildBindings()); + const auto switchedFrame = UpdateUIEditorPanelContentHost( + state, + request, + registry, + BuildBindings()); + + EXPECT_EQ( + FormatEvents(switchedFrame.events), + std::vector({ "Mounted:doc-a", "Unmounted:doc-b" })); + + const auto* docAState = FindUIEditorPanelContentHostPanelState(switchedFrame, "doc-a"); + const auto* docBState = FindUIEditorPanelContentHostPanelState(switchedFrame, "doc-b"); + ASSERT_NE(docAState, nullptr); + ASSERT_NE(docBState, nullptr); + EXPECT_TRUE(docAState->mounted); + EXPECT_FALSE(docBState->mounted); +} + +TEST(UIEditorPanelContentHostTest, UpdateEmitsBoundsChangedForMountedHostedPanelsWhenLayoutChanges) { + const UIEditorPanelRegistry registry = BuildPanelRegistry(); + const UIEditorWorkspaceModel workspace = BuildWorkspace(); + const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + + UIEditorPanelContentHostState state = {}; + auto layout = BuildUIEditorDockHostLayout( + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + registry, + workspace, + session); + auto request = ResolveUIEditorPanelContentHostRequest(layout, registry, BuildBindings()); + UpdateUIEditorPanelContentHost(state, request, registry, BuildBindings()); + + layout = BuildUIEditorDockHostLayout( + UIRect(0.0f, 0.0f, 1440.0f, 800.0f), + registry, + workspace, + session); + request = ResolveUIEditorPanelContentHostRequest(layout, registry, BuildBindings()); + const auto resizedFrame = UpdateUIEditorPanelContentHost( + state, + request, + registry, + BuildBindings()); + + EXPECT_EQ( + FormatEvents(resizedFrame.events), + std::vector({ "BoundsChanged:doc-b", "BoundsChanged:inspector" })); +} diff --git a/tests/UI/Editor/unit/test_ui_editor_workspace_compose.cpp b/tests/UI/Editor/unit/test_ui_editor_workspace_compose.cpp index 9a579e1b..791e5b1a 100644 --- a/tests/UI/Editor/unit/test_ui_editor_workspace_compose.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_workspace_compose.cpp @@ -10,6 +10,7 @@ using XCEngine::UI::UIInputEventType; using XCEngine::UI::UIPoint; using XCEngine::UI::UIPointerButton; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; +using XCEngine::UI::Editor::CollectUIEditorWorkspaceComposeExternalBodyPanelIds; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; @@ -88,6 +89,42 @@ XCEngine::UI::Editor::UIEditorViewportShellModel BuildViewportShellModel(std::st return model; } +UIEditorPanelRegistry BuildRegistryWithHostedContentPanels() { + UIEditorPanelRegistry registry = {}; + registry.panels = { + { "doc-a", "Document A", UIEditorPanelPresentationKind::HostedContent, false, true, true }, + { "doc-b", "Document B", UIEditorPanelPresentationKind::HostedContent, false, true, true }, + { "inspector", "Inspector", UIEditorPanelPresentationKind::HostedContent, false, true, true } + }; + return registry; +} + +UIEditorWorkspaceModel BuildHostedContentWorkspace() { + UIEditorWorkspaceModel workspace = {}; + workspace.root = BuildUIEditorWorkspaceSplit( + "root", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.68f, + BuildUIEditorWorkspaceTabStack( + "documents", + { + BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A"), + BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B") + }, + 1u), + BuildUIEditorWorkspacePanel("inspector-node", "inspector", "Inspector")); + workspace.activePanelId = "doc-b"; + return workspace; +} + +UIEditorWorkspacePanelPresentationModel BuildHostedContentPresentationModel( + std::string panelId) { + UIEditorWorkspacePanelPresentationModel model = {}; + model.panelId = std::move(panelId); + model.kind = UIEditorPanelPresentationKind::HostedContent; + return model; +} + UIEditorWorkspacePanelPresentationModel BuildViewportPresentationModel( std::string panelId, std::string title) { @@ -267,3 +304,29 @@ TEST(UIEditorWorkspaceComposeTest, HiddenViewportTabResetsCapturedAndFocusedStat EXPECT_FALSE(viewportBStateAfterHide->viewportShellState.inputBridgeState.focused); EXPECT_FALSE(viewportBStateAfterHide->viewportShellState.inputBridgeState.captured); } + +TEST(UIEditorWorkspaceComposeTest, HostedContentPanelsFlowThroughContentHostAndSuppressDockPlaceholder) { + const auto registry = BuildRegistryWithHostedContentPanels(); + const UIEditorWorkspaceModel workspace = BuildHostedContentWorkspace(); + const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + const std::vector presentationModels = { + BuildHostedContentPresentationModel("doc-a"), + BuildHostedContentPresentationModel("doc-b"), + BuildHostedContentPresentationModel("inspector") + }; + + UIEditorWorkspaceComposeState state = {}; + const UIEditorWorkspaceComposeFrame frame = UpdateUIEditorWorkspaceCompose( + state, + UIRect(0.0f, 0.0f, 1200.0f, 760.0f), + registry, + workspace, + session, + presentationModels, + {}); + + EXPECT_TRUE(frame.viewportFrames.empty()); + EXPECT_EQ( + CollectUIEditorWorkspaceComposeExternalBodyPanelIds(frame), + std::vector({ "doc-b", "inspector" })); +}