From 945420f3bd1e8ecfab40f07efc18b215da532e74 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Tue, 7 Apr 2026 06:30:34 +0800 Subject: [PATCH] Build XCEditor editor shell compose foundation --- new_editor/CMakeLists.txt | 1 + .../XCEditor/Core/UIEditorShellCompose.h | 95 +++ new_editor/src/Core/UIEditorShellCompose.cpp | 214 +++++++ tests/UI/Editor/integration/CMakeLists.txt | 1 + tests/UI/Editor/integration/README.md | 9 + .../Editor/integration/shell/CMakeLists.txt | 3 + .../shell/editor_shell_compose/CMakeLists.txt | 30 + .../editor_shell_compose/captures/.gitkeep | 1 + .../shell/editor_shell_compose/main.cpp | 590 ++++++++++++++++++ tests/UI/Editor/unit/CMakeLists.txt | 1 + .../unit/test_ui_editor_shell_compose.cpp | 164 +++++ 11 files changed, 1109 insertions(+) create mode 100644 new_editor/include/XCEditor/Core/UIEditorShellCompose.h create mode 100644 new_editor/src/Core/UIEditorShellCompose.cpp create mode 100644 tests/UI/Editor/integration/shell/editor_shell_compose/CMakeLists.txt create mode 100644 tests/UI/Editor/integration/shell/editor_shell_compose/captures/.gitkeep create mode 100644 tests/UI/Editor/integration/shell/editor_shell_compose/main.cpp create mode 100644 tests/UI/Editor/unit/test_ui_editor_shell_compose.cpp diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 0ec179cb..fe923c5e 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(XCUIEditorLib STATIC src/Core/UIEditorMenuModel.cpp src/Core/UIEditorMenuSession.cpp src/Core/UIEditorPanelRegistry.cpp + src/Core/UIEditorShellCompose.cpp src/Core/UIEditorShortcutManager.cpp src/Core/UIEditorViewportInputBridge.cpp src/Core/UIEditorViewportShell.cpp diff --git a/new_editor/include/XCEditor/Core/UIEditorShellCompose.h b/new_editor/include/XCEditor/Core/UIEditorShellCompose.h new file mode 100644 index 00000000..9f66e433 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorShellCompose.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include + +namespace XCEngine::UI::Editor { + +struct UIEditorShellComposeModel { + std::vector menuBarItems = {}; + std::vector statusSegments = {}; + std::vector workspacePresentations = {}; +}; + +struct UIEditorShellComposeState { + Widgets::UIEditorMenuBarState menuBarState = {}; + UIEditorWorkspaceComposeState workspaceState = {}; + Widgets::UIEditorStatusBarState statusBarState = {}; +}; + +struct UIEditorShellComposeMetrics { + float outerPadding = 12.0f; + float sectionGap = 8.0f; + float surfaceCornerRounding = 10.0f; + Widgets::UIEditorMenuBarMetrics menuBarMetrics = {}; + Widgets::UIEditorDockHostMetrics dockHostMetrics = {}; + Widgets::UIEditorStatusBarMetrics statusBarMetrics = {}; +}; + +struct UIEditorShellComposePalette { + ::XCEngine::UI::UIColor surfaceColor = + ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); + ::XCEngine::UI::UIColor surfaceBorderColor = + ::XCEngine::UI::UIColor(0.27f, 0.27f, 0.27f, 1.0f); + Widgets::UIEditorMenuBarPalette menuBarPalette = {}; + Widgets::UIEditorDockHostPalette dockHostPalette = {}; + Widgets::UIEditorStatusBarPalette statusBarPalette = {}; +}; + +struct UIEditorShellComposeLayout { + ::XCEngine::UI::UIRect bounds = {}; + ::XCEngine::UI::UIRect contentRect = {}; + ::XCEngine::UI::UIRect menuBarRect = {}; + ::XCEngine::UI::UIRect workspaceRect = {}; + ::XCEngine::UI::UIRect statusBarRect = {}; + Widgets::UIEditorMenuBarLayout menuBarLayout = {}; + Widgets::UIEditorStatusBarLayout statusBarLayout = {}; +}; + +struct UIEditorShellComposeRequest { + UIEditorShellComposeLayout layout = {}; + UIEditorWorkspaceComposeRequest workspaceRequest = {}; +}; + +struct UIEditorShellComposeFrame { + UIEditorShellComposeLayout layout = {}; + UIEditorWorkspaceComposeFrame workspaceFrame = {}; +}; + +UIEditorShellComposeLayout BuildUIEditorShellComposeLayout( + const ::XCEngine::UI::UIRect& bounds, + const std::vector& menuBarItems, + const std::vector& statusSegments, + const UIEditorShellComposeMetrics& metrics = {}); + +UIEditorShellComposeRequest ResolveUIEditorShellComposeRequest( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const UIEditorShellComposeModel& model, + const Widgets::UIEditorDockHostState& dockHostState = {}, + const UIEditorShellComposeState& state = {}, + const UIEditorShellComposeMetrics& metrics = {}); + +UIEditorShellComposeFrame UpdateUIEditorShellCompose( + UIEditorShellComposeState& state, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const UIEditorShellComposeModel& model, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorDockHostState& dockHostState = {}, + const UIEditorShellComposeMetrics& metrics = {}); + +void AppendUIEditorShellCompose( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorShellComposeFrame& frame, + const UIEditorShellComposeModel& model, + const UIEditorShellComposeState& state, + const UIEditorShellComposePalette& palette = {}, + const UIEditorShellComposeMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Core/UIEditorShellCompose.cpp b/new_editor/src/Core/UIEditorShellCompose.cpp new file mode 100644 index 00000000..c6cacac8 --- /dev/null +++ b/new_editor/src/Core/UIEditorShellCompose.cpp @@ -0,0 +1,214 @@ +#include + +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using ::XCEngine::UI::UIRect; +using Widgets::AppendUIEditorMenuBarBackground; +using Widgets::AppendUIEditorMenuBarForeground; +using Widgets::AppendUIEditorStatusBarBackground; +using Widgets::AppendUIEditorStatusBarForeground; +using Widgets::BuildUIEditorMenuBarLayout; +using Widgets::BuildUIEditorStatusBarLayout; + +float ClampNonNegative(float value) { + return (std::max)(value, 0.0f); +} + +UIRect InsetRect(const UIRect& rect, float inset) { + const float clampedInset = ClampNonNegative(inset); + const float insetX = (std::min)(clampedInset, rect.width * 0.5f); + const float insetY = (std::min)(clampedInset, rect.height * 0.5f); + return UIRect( + rect.x + insetX, + rect.y + insetY, + (std::max)(0.0f, rect.width - insetX * 2.0f), + (std::max)(0.0f, rect.height - insetY * 2.0f)); +} + +} // namespace + +UIEditorShellComposeLayout BuildUIEditorShellComposeLayout( + const UIRect& bounds, + const std::vector& menuBarItems, + const std::vector& statusSegments, + const UIEditorShellComposeMetrics& metrics) { + UIEditorShellComposeLayout layout = {}; + layout.bounds = UIRect( + bounds.x, + bounds.y, + ClampNonNegative(bounds.width), + ClampNonNegative(bounds.height)); + layout.contentRect = InsetRect(layout.bounds, metrics.outerPadding); + + const float menuBarHeight = ClampNonNegative(metrics.menuBarMetrics.barHeight); + const float statusBarHeight = ClampNonNegative(metrics.statusBarMetrics.barHeight); + const float sectionGap = ClampNonNegative(metrics.sectionGap); + const float availableHeight = layout.contentRect.height; + const float combinedBars = menuBarHeight + statusBarHeight; + const float gapBudget = combinedBars > 0.0f ? sectionGap * 2.0f : 0.0f; + const float clampedGapBudget = (std::min)(gapBudget, availableHeight); + const float workspaceHeight = + (std::max)(0.0f, availableHeight - combinedBars - clampedGapBudget); + + float cursorY = layout.contentRect.y; + if (menuBarHeight > 0.0f) { + layout.menuBarRect = UIRect( + layout.contentRect.x, + cursorY, + layout.contentRect.width, + menuBarHeight); + cursorY += menuBarHeight + sectionGap; + } + + layout.workspaceRect = UIRect( + layout.contentRect.x, + cursorY, + layout.contentRect.width, + workspaceHeight); + + if (statusBarHeight > 0.0f) { + layout.statusBarRect = UIRect( + layout.contentRect.x, + layout.contentRect.y + layout.contentRect.height - statusBarHeight, + layout.contentRect.width, + statusBarHeight); + } + + layout.menuBarLayout = + BuildUIEditorMenuBarLayout(layout.menuBarRect, menuBarItems, metrics.menuBarMetrics); + layout.statusBarLayout = + BuildUIEditorStatusBarLayout(layout.statusBarRect, statusSegments, metrics.statusBarMetrics); + return layout; +} + +UIEditorShellComposeRequest ResolveUIEditorShellComposeRequest( + const UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const UIEditorShellComposeModel& model, + const Widgets::UIEditorDockHostState& dockHostState, + const UIEditorShellComposeState& state, + const UIEditorShellComposeMetrics& metrics) { + UIEditorShellComposeRequest request = {}; + request.layout = BuildUIEditorShellComposeLayout( + bounds, + model.menuBarItems, + model.statusSegments, + metrics); + request.workspaceRequest = ResolveUIEditorWorkspaceComposeRequest( + request.layout.workspaceRect, + panelRegistry, + workspace, + session, + model.workspacePresentations, + dockHostState, + metrics.dockHostMetrics); + request.layout.menuBarLayout = BuildUIEditorMenuBarLayout( + request.layout.menuBarRect, + model.menuBarItems, + metrics.menuBarMetrics); + request.layout.statusBarLayout = BuildUIEditorStatusBarLayout( + request.layout.statusBarRect, + model.statusSegments, + metrics.statusBarMetrics); + (void)state; + return request; +} + +UIEditorShellComposeFrame UpdateUIEditorShellCompose( + UIEditorShellComposeState& state, + const UIRect& bounds, + const UIEditorPanelRegistry& panelRegistry, + const UIEditorWorkspaceModel& workspace, + const UIEditorWorkspaceSession& session, + const UIEditorShellComposeModel& model, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorDockHostState& dockHostState, + const UIEditorShellComposeMetrics& metrics) { + UIEditorShellComposeFrame frame = {}; + frame.layout = BuildUIEditorShellComposeLayout( + bounds, + model.menuBarItems, + model.statusSegments, + metrics); + frame.workspaceFrame = UpdateUIEditorWorkspaceCompose( + state.workspaceState, + frame.layout.workspaceRect, + panelRegistry, + workspace, + session, + model.workspacePresentations, + inputEvents, + dockHostState, + metrics.dockHostMetrics); + frame.layout.menuBarLayout = BuildUIEditorMenuBarLayout( + frame.layout.menuBarRect, + model.menuBarItems, + metrics.menuBarMetrics); + frame.layout.statusBarLayout = BuildUIEditorStatusBarLayout( + frame.layout.statusBarRect, + model.statusSegments, + metrics.statusBarMetrics); + return frame; +} + +void AppendUIEditorShellCompose( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorShellComposeFrame& frame, + const UIEditorShellComposeModel& model, + const UIEditorShellComposeState& state, + const UIEditorShellComposePalette& palette, + const UIEditorShellComposeMetrics& metrics) { + drawList.AddFilledRect( + frame.layout.bounds, + palette.surfaceColor, + metrics.surfaceCornerRounding); + drawList.AddRectOutline( + frame.layout.bounds, + palette.surfaceBorderColor, + 1.0f, + metrics.surfaceCornerRounding); + + AppendUIEditorMenuBarBackground( + drawList, + frame.layout.menuBarLayout, + model.menuBarItems, + state.menuBarState, + palette.menuBarPalette, + metrics.menuBarMetrics); + AppendUIEditorMenuBarForeground( + drawList, + frame.layout.menuBarLayout, + model.menuBarItems, + state.menuBarState, + palette.menuBarPalette, + metrics.menuBarMetrics); + + AppendUIEditorWorkspaceCompose( + drawList, + frame.workspaceFrame, + palette.dockHostPalette, + metrics.dockHostMetrics); + + AppendUIEditorStatusBarBackground( + drawList, + frame.layout.statusBarLayout, + model.statusSegments, + state.statusBarState, + palette.statusBarPalette, + metrics.statusBarMetrics); + AppendUIEditorStatusBarForeground( + drawList, + frame.layout.statusBarLayout, + model.statusSegments, + state.statusBarState, + palette.statusBarPalette, + metrics.statusBarMetrics); +} + +} // namespace XCEngine::UI::Editor diff --git a/tests/UI/Editor/integration/CMakeLists.txt b/tests/UI/Editor/integration/CMakeLists.txt index 963a2084..20b85a9e 100644 --- a/tests/UI/Editor/integration/CMakeLists.txt +++ b/tests/UI/Editor/integration/CMakeLists.txt @@ -6,6 +6,7 @@ add_subdirectory(state) set(EDITOR_UI_INTEGRATION_TARGETS editor_ui_workspace_shell_compose_validation + editor_ui_editor_shell_compose_validation editor_ui_menu_bar_basic_validation editor_ui_panel_frame_basic_validation editor_ui_tab_strip_basic_validation diff --git a/tests/UI/Editor/integration/README.md b/tests/UI/Editor/integration/README.md index 38a3f698..4d8cb856 100644 --- a/tests/UI/Editor/integration/README.md +++ b/tests/UI/Editor/integration/README.md @@ -15,6 +15,7 @@ Layout: - `menu`, `workspace`, and `viewport` in scenario names describe contract families, not extra directory levels. - `shared/`: shared host wrapper, scenario registry, shared theme - `shell/workspace_shell_compose/`: split/tab/panel shell compose only +- `shell/editor_shell_compose/`: editor root shell compose 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 @@ -35,6 +36,11 @@ Scenarios: Executable: `XCUIEditorWorkspaceShellComposeValidation.exe` Scope: DockHost compose, splitter drag, tab host, panel frame placeholders, workspace active-panel sync +- `editor.shell.editor_shell_compose` + Build target: `editor_ui_editor_shell_compose_validation` + Executable: `XCUIEditorShellComposeValidation.exe` + Scope: root shell compose only; MenuBar / WorkspaceCompose / StatusBar three-band layout and workspace body embedding + - `editor.shell.menu_bar_basic` Build target: `editor_ui_menu_bar_basic_validation` Executable: `XCUIEditorMenuBarBasicValidation.exe` @@ -111,6 +117,9 @@ Selected controls: - `shell/workspace_shell_compose/` Drag splitters, switch `Document A/B/C`, close tabs or side panels, press `Reset`, press `F12`. +- `shell/editor_shell_compose/` + Click `切到 Scene / 切到 Document`, toggle `TopBar / BottomBar / Texture`, inspect `MenuBar Rect / Workspace Rect / StatusBar Rect / Selected Presentation / Request Size`, press `Reset`, press `截图` or `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 870fa604..2d54b929 100644 --- a/tests/UI/Editor/integration/shell/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/CMakeLists.txt @@ -1,6 +1,9 @@ add_subdirectory(workspace_shell_compose) add_subdirectory(panel_frame_basic) add_subdirectory(tab_strip_basic) +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/editor_shell_compose/CMakeLists.txt") + add_subdirectory(editor_shell_compose) +endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/menu_bar_basic/CMakeLists.txt") add_subdirectory(menu_bar_basic) endif() diff --git a/tests/UI/Editor/integration/shell/editor_shell_compose/CMakeLists.txt b/tests/UI/Editor/integration/shell/editor_shell_compose/CMakeLists.txt new file mode 100644 index 00000000..ad2694dc --- /dev/null +++ b/tests/UI/Editor/integration/shell/editor_shell_compose/CMakeLists.txt @@ -0,0 +1,30 @@ +add_executable(editor_ui_editor_shell_compose_validation WIN32 + main.cpp +) + +target_include_directories(editor_ui_editor_shell_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_editor_shell_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_editor_shell_compose_validation PRIVATE /utf-8 /FS) + set_property(TARGET editor_ui_editor_shell_compose_validation PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +target_link_libraries(editor_ui_editor_shell_compose_validation PRIVATE + XCUIEditorLib + XCUIEditorHost +) + +set_target_properties(editor_ui_editor_shell_compose_validation PROPERTIES + OUTPUT_NAME "XCUIEditorShellComposeValidation" +) diff --git a/tests/UI/Editor/integration/shell/editor_shell_compose/captures/.gitkeep b/tests/UI/Editor/integration/shell/editor_shell_compose/captures/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/UI/Editor/integration/shell/editor_shell_compose/captures/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/UI/Editor/integration/shell/editor_shell_compose/main.cpp b/tests/UI/Editor/integration/shell/editor_shell_compose/main.cpp new file mode 100644 index 00000000..a485b834 --- /dev/null +++ b/tests/UI/Editor/integration/shell/editor_shell_compose/main.cpp @@ -0,0 +1,590 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#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::UISize; +using XCEngine::UI::Editor::AppendUIEditorShellCompose; +using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController; +using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; +using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; +using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationRequest; +using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName; +using XCEngine::UI::Editor::ResolveUIEditorShellComposeRequest; +using XCEngine::UI::Editor::UIEditorPanelPresentationKind; +using XCEngine::UI::Editor::UIEditorPanelRegistry; +using XCEngine::UI::Editor::UIEditorShellComposeFrame; +using XCEngine::UI::Editor::UIEditorShellComposeModel; +using XCEngine::UI::Editor::UIEditorShellComposeRequest; +using XCEngine::UI::Editor::UIEditorShellComposeState; +using XCEngine::UI::Editor::UIEditorWorkspaceCommand; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind; +using XCEngine::UI::Editor::UIEditorWorkspaceCommandResult; +using XCEngine::UI::Editor::UIEditorWorkspaceController; +using XCEngine::UI::Editor::UIEditorWorkspaceModel; +using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel; +using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; +using XCEngine::UI::Editor::UpdateUIEditorShellCompose; +using XCEngine::UI::Editor::Host::AutoScreenshotController; +using XCEngine::UI::Editor::Host::NativeRenderer; +using XCEngine::UI::Editor::Widgets::UIEditorMenuBarItem; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot; + +constexpr const wchar_t* kWindowClassName = L"XCUIEditorShellComposeValidation"; +constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Shell Compose"; + +constexpr UIColor kWindowBg(0.10f, 0.10f, 0.10f, 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 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() { + 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 FormatFloat(float value) { + std::ostringstream stream = {}; + stream.setf(std::ios::fixed, std::ios::floatfield); + stream.precision(1); + stream << value; + return stream.str(); +} + +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 FormatSize(const UISize& size) { + return FormatFloat(size.width) + " x " + FormatFloat(size.height); +} + +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; +} + +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->m_renderer.Resize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + 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"); + 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 Initialize(HINSTANCE hInstance, int nCmdShow) { + m_captureRoot = + ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/editor_shell_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; + } + + ShowWindow(m_hwnd, nCmdShow); + if (!m_renderer.Initialize(m_hwnd)) { + return false; + } + + ResetScenario(); + return true; + } + + void Shutdown() { + m_autoScreenshot.Shutdown(); + m_renderer.Shutdown(); + if (m_hwnd != nullptr && IsWindow(m_hwnd)) { + DestroyWindow(m_hwnd); + } + if (m_windowClassAtom != 0) { + UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr)); + } + } + + void ResetScenario() { + m_controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); + m_shellState = {}; + m_showTopBar = true; + m_showBottomBar = true; + m_textureEnabled = true; + m_lastResult = "Ready"; + } + + void UpdateLayoutForCurrentWindow() { + 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, 188.0f); + m_controlsRect = UIRect(outerPadding, 224.0f, leftColumnWidth, 340.0f); + m_stateRect = UIRect(outerPadding, 580.0f, leftColumnWidth, height - 600.0f); + m_previewRect = UIRect( + leftColumnWidth + outerPadding * 2.0f, + outerPadding, + width - leftColumnWidth - outerPadding * 3.0f, + height - outerPadding * 2.0f); + m_shellRect = 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 = GetSelectedTabId() == "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 } + }; + } + + UIEditorShellComposeModel BuildShellModel() const { + UIEditorShellComposeModel model = {}; + model.menuBarItems = { + UIEditorMenuBarItem{ "file", "File", true, 0.0f }, + UIEditorMenuBarItem{ "window", "Window", true, 0.0f }, + UIEditorMenuBarItem{ "layout", "Layout", true, 0.0f } + }; + model.statusSegments = { + UIEditorStatusBarSegment{ "mode", GetSelectedTabId() == "scene" ? "Scene" : "Document", UIEditorStatusBarSlot::Leading, {}, true, true, 86.0f }, + UIEditorStatusBarSegment{ "chrome", std::string("Top ") + (m_showTopBar ? "On" : "Off"), UIEditorStatusBarSlot::Leading, {}, true, false, 82.0f }, + UIEditorStatusBarSegment{ "branch", m_textureEnabled ? "Texture" : "Fallback", UIEditorStatusBarSlot::Trailing, {}, true, true, 96.0f } + }; + + UIEditorWorkspacePanelPresentationModel presentation = {}; + presentation.panelId = "scene"; + presentation.kind = UIEditorPanelPresentationKind::ViewportShell; + presentation.viewportShellModel.spec.chrome.title = "Scene"; + presentation.viewportShellModel.spec.chrome.subtitle = "Editor Shell 基础层"; + presentation.viewportShellModel.spec.chrome.showTopBar = m_showTopBar; + presentation.viewportShellModel.spec.chrome.showBottomBar = m_showBottomBar; + presentation.viewportShellModel.spec.chrome.topBarHeight = 40.0f; + presentation.viewportShellModel.spec.chrome.bottomBarHeight = 28.0f; + presentation.viewportShellModel.spec.toolItems = { + { "mode", "Perspective", XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot::Leading, true, true, 98.0f } + }; + presentation.viewportShellModel.spec.statusSegments = { + { "view", "Shell", UIEditorStatusBarSlot::Leading, {}, true, true, 64.0f }, + { "branch", m_textureEnabled ? "Texture" : "Fallback", UIEditorStatusBarSlot::Trailing, {}, true, false, 96.0f } + }; + if (m_textureEnabled) { + presentation.viewportShellModel.frame.hasTexture = true; + presentation.viewportShellModel.frame.texture = { 1u, 1280u, 720u }; + presentation.viewportShellModel.frame.presentedSize = UISize(1280.0f, 720.0f); + presentation.viewportShellModel.frame.statusText = "Fake viewport frame"; + } else { + presentation.viewportShellModel.frame.hasTexture = false; + presentation.viewportShellModel.frame.statusText = "这里只验证 Editor shell compose,不接 Scene/Game 业务。"; + } + model.workspacePresentations = { presentation }; + return model; + } + + std::string GetSelectedTabId() const { + if (!m_shellFrame.workspaceFrame.dockHostLayout.tabStacks.empty()) { + return m_shellFrame.workspaceFrame.dockHostLayout.tabStacks.front().selectedPanelId; + } + return m_controller.GetWorkspace().activePanelId; + } + + void UpdateShellFrame() { + const UIEditorShellComposeModel model = BuildShellModel(); + m_shellRequest = ResolveUIEditorShellComposeRequest( + m_shellRect, + m_controller.GetPanelRegistry(), + m_controller.GetWorkspace(), + m_controller.GetSession(), + model, + {}, + m_shellState); + m_shellFrame = UpdateUIEditorShellCompose( + m_shellState, + m_shellRect, + m_controller.GetPanelRegistry(), + m_controller.GetWorkspace(), + m_controller.GetSession(), + model, + {}); + m_cachedModel = model; + } + + void HandleClick(float x, float y) { + for (const ButtonState& button : m_buttons) { + if (!ContainsPoint(button.rect, x, y)) { + continue; + } + + ExecuteAction(button.action); + InvalidateRect(m_hwnd, nullptr, FALSE); + return; + } + } + + void 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 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; + } + + void RenderFrame() { + UpdateLayoutForCurrentWindow(); + UpdateShellFrame(); + UpdateLayoutForCurrentWindow(); + + 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)); + + const auto* viewportRequest = + FindUIEditorWorkspaceViewportPresentationRequest(m_shellRequest.workspaceRequest, "scene"); + const std::string selectedPresentation = + GetSelectedTabId() == "scene" && viewportRequest != nullptr ? "ViewportShell" : "Placeholder"; + const auto validation = m_controller.ValidateState(); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("EditorShellCompose"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); + + DrawCard(drawList, m_introRect, "测试功能:Editor 根壳层 compose", "只验证 MenuBar / Workspace / StatusBar 三段根壳组合。"); + drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 66.0f), "检查 1:顶部 MenuBar、中间 WorkspaceCompose、底部 StatusBar 必须各自占带,不能互相挤压或重叠。", kTextMuted, 11.0f); + drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 88.0f), "检查 2:切换 Scene / Document 时,只影响中间 Workspace body presentation,不应破坏顶栏和底栏。", kTextMuted, 11.0f); + drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 110.0f), "检查 3:切换 TopBar / BottomBar / Texture 时,Request Size 必须同步,但 MenuBar / StatusBar 位置保持稳定。", kTextMuted, 11.0f); + drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 132.0f), "这一层暂时不验证 menu popup 或 shortcut,只验证 Editor 根框架 compose。", kTextWeak, 11.0f); + + DrawCard(drawList, m_controlsRect, "操作", "这里只保留当前这个根壳层需要检查的 7 个控件。"); + for (const ButtonState& button : m_buttons) { + DrawButton(drawList, button); + } + + DrawCard(drawList, m_stateRect, "状态", "左侧直接回显根壳层三段布局与中间 workspace request。"); + 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: " + GetSelectedTabId(), kTextPrimary); + addStateLine("Selected Presentation: " + selectedPresentation, kTextPrimary); + addStateLine("MenuBar Rect: " + FormatRect(m_shellFrame.layout.menuBarRect), kTextMuted, 11.0f); + addStateLine("Workspace Rect: " + FormatRect(m_shellFrame.layout.workspaceRect), kTextMuted, 11.0f); + addStateLine("StatusBar Rect: " + FormatRect(m_shellFrame.layout.statusBarRect), kTextMuted, 11.0f); + addStateLine( + "Request Size: " + + (viewportRequest != nullptr + ? FormatSize(viewportRequest->viewportShellRequest.requestedViewportSize) + : 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 或按钮 -> editor_shell_compose/captures/") + : m_autoScreenshot.GetLastCaptureSummary()); + addStateLine(captureSummary, kTextWeak, 11.0f); + + DrawCard(drawList, m_previewRect, "Preview", "这里只有 Editor 根壳层 compose,不混入业务面板。"); + AppendUIEditorShellCompose(drawList, m_shellFrame, m_cachedModel, m_shellState); + + const bool framePresented = m_renderer.Render(drawData); + m_autoScreenshot.CaptureIfRequested( + m_renderer, + drawData, + static_cast(width), + static_cast(height), + framePresented); + } + + HWND m_hwnd = nullptr; + ATOM m_windowClassAtom = 0; + NativeRenderer m_renderer = {}; + AutoScreenshotController m_autoScreenshot = {}; + std::filesystem::path m_captureRoot = {}; + UIEditorWorkspaceController m_controller = {}; + UIEditorShellComposeState m_shellState = {}; + UIEditorShellComposeRequest m_shellRequest = {}; + UIEditorShellComposeFrame m_shellFrame = {}; + UIEditorShellComposeModel m_cachedModel = {}; + std::vector m_buttons = {}; + UIRect m_introRect = {}; + UIRect m_controlsRect = {}; + UIRect m_stateRect = {}; + UIRect m_previewRect = {}; + UIRect m_shellRect = {}; + bool m_showTopBar = true; + bool m_showBottomBar = true; + bool m_textureEnabled = true; + std::string m_lastResult = {}; +}; + +} // 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 f6c07fed..f97e89a7 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_bar.cpp test_ui_editor_menu_popup.cpp test_ui_editor_panel_registry.cpp + test_ui_editor_shell_compose.cpp test_ui_editor_collection_primitives.cpp test_ui_editor_dock_host.cpp test_ui_editor_panel_chrome.cpp diff --git a/tests/UI/Editor/unit/test_ui_editor_shell_compose.cpp b/tests/UI/Editor/unit/test_ui_editor_shell_compose.cpp new file mode 100644 index 00000000..05e90f1f --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_shell_compose.cpp @@ -0,0 +1,164 @@ +#include + +#include +#include +#include +#include + +namespace { + +using XCEngine::UI::UIDrawCommandType; +using XCEngine::UI::UIDrawList; +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::BuildUIEditorShellComposeLayout; +using XCEngine::UI::Editor::ResolveUIEditorShellComposeRequest; +using XCEngine::UI::Editor::UIEditorPanelPresentationKind; +using XCEngine::UI::Editor::UIEditorPanelRegistry; +using XCEngine::UI::Editor::UIEditorShellComposeFrame; +using XCEngine::UI::Editor::UIEditorShellComposeModel; +using XCEngine::UI::Editor::UIEditorShellComposeState; +using XCEngine::UI::Editor::UIEditorWorkspaceModel; +using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel; +using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; +using XCEngine::UI::Editor::UpdateUIEditorShellCompose; +using XCEngine::UI::Editor::AppendUIEditorShellCompose; +using XCEngine::UI::Editor::Widgets::UIEditorMenuBarItem; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment; +using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot; + +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", + UIEditorWorkspaceSplitAxis::Horizontal, + 0.24f, + BuildUIEditorWorkspacePanel("hierarchy-node", "hierarchy", "Hierarchy", true), + BuildUIEditorWorkspaceSplit( + "main", + 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; +} + +UIEditorShellComposeModel BuildShellModel() { + UIEditorShellComposeModel model = {}; + model.menuBarItems = { + UIEditorMenuBarItem{ "file", "File", true, 0.0f }, + UIEditorMenuBarItem{ "window", "Window", true, 0.0f }, + UIEditorMenuBarItem{ "layout", "Layout", true, 0.0f } + }; + model.statusSegments = { + UIEditorStatusBarSegment{ "scene", "Scene", UIEditorStatusBarSlot::Leading, {}, true, true, 72.0f }, + UIEditorStatusBarSegment{ "frame", "16.7 ms", UIEditorStatusBarSlot::Trailing, {}, true, false, 72.0f } + }; + + UIEditorWorkspacePanelPresentationModel presentation = {}; + presentation.panelId = "scene"; + presentation.kind = UIEditorPanelPresentationKind::ViewportShell; + presentation.viewportShellModel.spec.chrome.title = "Scene"; + presentation.viewportShellModel.spec.chrome.showTopBar = true; + presentation.viewportShellModel.spec.chrome.showBottomBar = true; + presentation.viewportShellModel.frame.hasTexture = false; + presentation.viewportShellModel.frame.statusText = "Viewport frame"; + model.workspacePresentations = { presentation }; + return model; +} + +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(UIEditorShellComposeTest, LayoutAllocatesMenuWorkspaceAndStatusBandsWithoutOverlap) { + const UIEditorShellComposeModel model = BuildShellModel(); + + const auto layout = BuildUIEditorShellComposeLayout( + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + model.menuBarItems, + model.statusSegments); + + EXPECT_GT(layout.menuBarRect.height, 0.0f); + EXPECT_GT(layout.workspaceRect.height, 0.0f); + EXPECT_GT(layout.statusBarRect.height, 0.0f); + EXPECT_LE(layout.menuBarRect.y + layout.menuBarRect.height, layout.workspaceRect.y); + EXPECT_LE(layout.workspaceRect.y + layout.workspaceRect.height, layout.statusBarRect.y); + EXPECT_FLOAT_EQ(layout.contentRect.x, 12.0f); + EXPECT_FLOAT_EQ(layout.contentRect.y, 12.0f); +} + +TEST(UIEditorShellComposeTest, ResolveRequestRoutesWorkspaceComposeThroughWorkspaceBand) { + const auto registry = BuildPanelRegistry(); + const auto workspace = BuildWorkspace(); + const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + const UIEditorShellComposeModel model = BuildShellModel(); + + const auto request = ResolveUIEditorShellComposeRequest( + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + registry, + workspace, + session, + model); + + EXPECT_FLOAT_EQ(request.workspaceRequest.dockHostLayout.bounds.x, request.layout.workspaceRect.x); + EXPECT_FLOAT_EQ(request.workspaceRequest.dockHostLayout.bounds.y, request.layout.workspaceRect.y); + EXPECT_FLOAT_EQ(request.workspaceRequest.dockHostLayout.bounds.width, request.layout.workspaceRect.width); + EXPECT_FLOAT_EQ(request.workspaceRequest.dockHostLayout.bounds.height, request.layout.workspaceRect.height); + ASSERT_EQ(request.workspaceRequest.viewportRequests.size(), 1u); + EXPECT_EQ(request.workspaceRequest.viewportRequests.front().panelId, "scene"); +} + +TEST(UIEditorShellComposeTest, AppendComposeEmitsMenuStatusAndWorkspaceCommandsTogether) { + const auto registry = BuildPanelRegistry(); + const auto workspace = BuildWorkspace(); + const auto session = BuildDefaultUIEditorWorkspaceSession(registry, workspace); + const UIEditorShellComposeModel model = BuildShellModel(); + + UIEditorShellComposeState state = {}; + const UIEditorShellComposeFrame frame = UpdateUIEditorShellCompose( + state, + UIRect(0.0f, 0.0f, 1280.0f, 720.0f), + registry, + workspace, + session, + model, + {}); + + UIDrawList drawList("EditorShellCompose"); + AppendUIEditorShellCompose(drawList, frame, model, state); + + EXPECT_GT(drawList.GetCommandCount(), 20u); + EXPECT_TRUE(ContainsTextCommand(drawList, "File")); + EXPECT_TRUE(ContainsTextCommand(drawList, "Scene")); + EXPECT_TRUE(ContainsTextCommand(drawList, "16.7 ms")); +}