#ifndef NOMINMAX #define NOMINMAX #endif #include #include #include "Host/AutoScreenshot.h" #include "Host/NativeRenderer.h" #include #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::Editor::BuildDefaultUIEditorWorkspaceController; using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession; using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel; using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit; using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack; using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels; using XCEngine::UI::Editor::FindUIEditorPanelSessionState; using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName; using XCEngine::UI::Editor::GetUIEditorWorkspaceLayoutOperationStatusName; using XCEngine::UI::Editor::SerializeUIEditorWorkspaceLayoutSnapshot; using XCEngine::UI::Editor::UIEditorPanelRegistry; using XCEngine::UI::Editor::UIEditorWorkspaceCommand; using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind; using XCEngine::UI::Editor::UIEditorWorkspaceCommandResult; using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus; using XCEngine::UI::Editor::UIEditorWorkspaceController; using XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationResult; using XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationStatus; using XCEngine::UI::Editor::UIEditorWorkspaceLayoutSnapshot; using XCEngine::UI::Editor::UIEditorWorkspaceModel; using XCEngine::UI::Editor::UIEditorWorkspaceSession; using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis; using XCEngine::UI::UIColor; using XCEngine::UI::UIDrawData; using XCEngine::UI::UIDrawList; using XCEngine::UI::UIPoint; using XCEngine::UI::UIRect; using XCEngine::UI::Editor::Host::AutoScreenshotController; using XCEngine::UI::Editor::Host::NativeRenderer; constexpr const wchar_t* kWindowClassName = L"XCUIEditorLayoutPersistenceValidation"; constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Layout Persistence"; constexpr UIColor kWindowBg(0.14f, 0.14f, 0.14f, 1.0f); constexpr UIColor kCardBg(0.19f, 0.19f, 0.19f, 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.70f, 0.70f, 0.70f, 1.0f); constexpr UIColor kAccent(0.82f, 0.82f, 0.82f, 1.0f); constexpr UIColor kSuccess(0.43f, 0.71f, 0.47f, 1.0f); constexpr UIColor kWarning(0.78f, 0.60f, 0.30f, 1.0f); constexpr UIColor kDanger(0.78f, 0.34f, 0.34f, 1.0f); constexpr UIColor kButtonEnabled(0.31f, 0.31f, 0.31f, 1.0f); constexpr UIColor kButtonDisabled(0.24f, 0.24f, 0.24f, 1.0f); constexpr UIColor kButtonBorder(0.42f, 0.42f, 0.42f, 1.0f); constexpr UIColor kPanelRowBg(0.17f, 0.17f, 0.17f, 1.0f); enum class ActionId : unsigned char { HideActive = 0, SaveLayout, CloseDocB, LoadLayout, ActivateDetails, LoadInvalid, Reset }; struct ButtonState { ActionId action = ActionId::HideActive; std::string label = {}; UIRect rect = {}; bool enabled = 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(); } UIEditorPanelRegistry BuildPanelRegistry() { UIEditorPanelRegistry registry = {}; registry.panels = { { "doc-a", "Document A", {}, true, true, true }, { "doc-b", "Document B", {}, true, true, true }, { "details", "Details", {}, true, true, true } }; return registry; } UIEditorWorkspaceModel BuildWorkspace() { UIEditorWorkspaceModel workspace = {}; workspace.root = BuildUIEditorWorkspaceSplit( "root-split", UIEditorWorkspaceSplitAxis::Horizontal, 0.66f, BuildUIEditorWorkspaceTabStack( "document-tabs", { BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true), BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true) }, 0u), BuildUIEditorWorkspacePanel("details-node", "details", "Details", true)); workspace.activePanelId = "doc-a"; return workspace; } 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 JoinVisiblePanelIds( const UIEditorWorkspaceModel& workspace, const UIEditorWorkspaceSession& session) { const auto panels = CollectUIEditorWorkspaceVisiblePanels(workspace, session); if (panels.empty()) { return "(none)"; } std::ostringstream stream; for (std::size_t index = 0; index < panels.size(); ++index) { if (index > 0u) { stream << ", "; } stream << panels[index].panelId; } return stream.str(); } std::string DescribePanelState( const UIEditorWorkspaceSession& session, std::string_view panelId, std::string_view displayName) { const auto* state = FindUIEditorPanelSessionState(session, panelId); if (state == nullptr) { return std::string(displayName) + ": missing"; } std::string visibility = {}; if (!state->open) { visibility = "closed"; } else if (!state->visible) { visibility = "hidden"; } else { visibility = "visible"; } return std::string(displayName) + ": " + visibility; } UIColor ResolvePanelStateColor( const UIEditorWorkspaceSession& session, std::string_view panelId) { const auto* state = FindUIEditorPanelSessionState(session, panelId); if (state == nullptr) { return kDanger; } if (!state->open) { return kDanger; } if (!state->visible) { return kWarning; } return kSuccess; } 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); } } std::string BuildSerializedPreview(std::string_view serializedLayout) { if (serializedLayout.empty()) { return "尚未保存布局"; } std::istringstream stream{ std::string(serializedLayout) }; std::ostringstream preview = {}; std::string line = {}; int lineCount = 0; while (std::getline(stream, line) && lineCount < 4) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } if (line.empty()) { continue; } if (lineCount > 0) { preview << " | "; } preview << line; ++lineCount; } return preview.str(); } std::string ReplaceActiveRecord( std::string serializedLayout, std::string_view replacementPanelId) { const std::size_t activeStart = serializedLayout.find("active "); if (activeStart == std::string::npos) { return {}; } const std::size_t lineEnd = serializedLayout.find('\n', activeStart); std::ostringstream replacement = {}; replacement << "active " << std::quoted(std::string(replacementPanelId)); serializedLayout.replace( activeStart, lineEnd == std::string::npos ? serializedLayout.size() - activeStart : lineEnd - activeStart, replacement.str()); return serializedLayout; } UIColor ResolveCommandStatusColor(UIEditorWorkspaceCommandStatus status) { switch (status) { case UIEditorWorkspaceCommandStatus::Changed: return kSuccess; case UIEditorWorkspaceCommandStatus::NoOp: return kWarning; case UIEditorWorkspaceCommandStatus::Rejected: return kDanger; } return kTextMuted; } UIColor ResolveLayoutStatusColor(UIEditorWorkspaceLayoutOperationStatus status) { switch (status) { case UIEditorWorkspaceLayoutOperationStatus::Changed: return kSuccess; case UIEditorWorkspaceLayoutOperationStatus::NoOp: return kWarning; case UIEditorWorkspaceLayoutOperationStatus::Rejected: return kDanger; } return kTextMuted; } 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_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) { if (wParam == VK_F12) { app->m_autoScreenshot.RequestCapture("manual_f12"); } else { app->HandleShortcut(static_cast(wParam)); } 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; ResetScenario(); 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, 1360, 900, 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_autoScreenshot.Initialize( (ResolveRepoRootPath() / "tests/UI/Editor/integration/state/layout_persistence/captures") .lexically_normal()); return true; } void Shutdown() { m_autoScreenshot.Shutdown(); m_renderer.Shutdown(); if (m_hwnd != nullptr && IsWindow(m_hwnd)) { DestroyWindow(m_hwnd); } m_hwnd = nullptr; if (m_windowClassAtom != 0 && m_hInstance != nullptr) { UnregisterClassW(kWindowClassName, m_hInstance); m_windowClassAtom = 0; } } void ResetScenario() { m_controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace()); m_savedSnapshot = {}; m_savedSerializedLayout.clear(); m_hasSavedLayout = false; SetCustomResult( "等待操作", "Pending", "先点 `1 Hide Active`,再点 `2 Save Layout`;保存后继续改状态,再用 `4 Load Layout` 检查恢复。"); } void OnResize(UINT width, UINT height) { if (width == 0 || height == 0) { return; } m_renderer.Resize(width, height); } void RenderFrame() { if (m_hwnd == nullptr) { return; } 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 = {}; BuildDrawData(drawData, width, height); const bool framePresented = m_renderer.Render(drawData); m_autoScreenshot.CaptureIfRequested( m_renderer, drawData, static_cast(width), static_cast(height), framePresented); } void HandleClick(float x, float y) { for (const ButtonState& button : m_buttons) { if (button.enabled && ContainsPoint(button.rect, x, y)) { DispatchAction(button.action); InvalidateRect(m_hwnd, nullptr, FALSE); return; } } } void HandleShortcut(UINT keyCode) { switch (keyCode) { case '1': DispatchAction(ActionId::HideActive); break; case '2': DispatchAction(ActionId::SaveLayout); break; case '3': DispatchAction(ActionId::CloseDocB); break; case '4': DispatchAction(ActionId::LoadLayout); break; case '5': DispatchAction(ActionId::ActivateDetails); break; case '6': DispatchAction(ActionId::LoadInvalid); break; case 'R': DispatchAction(ActionId::Reset); break; default: return; } InvalidateRect(m_hwnd, nullptr, FALSE); } void DispatchAction(ActionId action) { switch (action) { case ActionId::HideActive: { UIEditorWorkspaceCommand command = {}; command.kind = UIEditorWorkspaceCommandKind::HidePanel; command.panelId = m_controller.GetWorkspace().activePanelId; SetCommandResult("Hide Active", m_controller.Dispatch(command)); return; } case ActionId::SaveLayout: SaveLayout(); return; case ActionId::CloseDocB: SetCommandResult( "Close Doc B", m_controller.Dispatch({ UIEditorWorkspaceCommandKind::ClosePanel, "doc-b" })); return; case ActionId::LoadLayout: LoadLayout(); return; case ActionId::ActivateDetails: SetCommandResult( "Activate Details", m_controller.Dispatch({ UIEditorWorkspaceCommandKind::ActivatePanel, "details" })); return; case ActionId::LoadInvalid: LoadInvalidLayout(); return; case ActionId::Reset: SetCommandResult( "Reset", m_controller.Dispatch({ UIEditorWorkspaceCommandKind::ResetWorkspace, {} })); return; } } void SaveLayout() { const auto validation = m_controller.ValidateState(); if (!validation.IsValid()) { SetCustomResult( "Save Layout", "Rejected", "当前 controller 状态非法,不能保存布局:" + validation.message); return; } const UIEditorWorkspaceLayoutSnapshot snapshot = m_controller.CaptureLayoutSnapshot(); const std::string serialized = SerializeUIEditorWorkspaceLayoutSnapshot(snapshot); const bool changed = !m_hasSavedLayout || serialized != m_savedSerializedLayout; m_savedSnapshot = snapshot; m_savedSerializedLayout = serialized; m_hasSavedLayout = true; SetCustomResult( "Save Layout", changed ? "Saved" : "NoOp", changed ? "已更新保存的布局快照。接下来继续改状态,再点 `Load Layout` 检查恢复。" : "保存内容与当前已保存布局一致。"); } void LoadLayout() { if (!m_hasSavedLayout) { SetCustomResult("Load Layout", "Rejected", "当前还没有保存过布局。"); return; } SetLayoutResult("Load Layout", m_controller.RestoreSerializedLayout(m_savedSerializedLayout)); } void LoadInvalidLayout() { const auto validation = m_controller.ValidateState(); if (!validation.IsValid()) { SetCustomResult( "Load Invalid", "Rejected", "当前 controller 状态非法,无法构造 invalid payload:" + validation.message); return; } const std::string source = m_hasSavedLayout ? m_savedSerializedLayout : SerializeUIEditorWorkspaceLayoutSnapshot(m_controller.CaptureLayoutSnapshot()); const std::string invalidSerialized = ReplaceActiveRecord(source, "missing-panel"); if (invalidSerialized.empty()) { SetCustomResult("Load Invalid", "Rejected", "构造 invalid payload 失败。"); return; } SetLayoutResult("Load Invalid", m_controller.RestoreSerializedLayout(invalidSerialized)); } void SetCommandResult( std::string actionName, const UIEditorWorkspaceCommandResult& result) { m_lastActionName = std::move(actionName); m_lastStatusLabel = std::string(GetUIEditorWorkspaceCommandStatusName(result.status)); m_lastMessage = result.message; m_lastStatusColor = ResolveCommandStatusColor(result.status); } void SetLayoutResult( std::string actionName, const UIEditorWorkspaceLayoutOperationResult& result) { m_lastActionName = std::move(actionName); m_lastStatusLabel = std::string(GetUIEditorWorkspaceLayoutOperationStatusName(result.status)); m_lastMessage = result.message; m_lastStatusColor = ResolveLayoutStatusColor(result.status); } void SetCustomResult( std::string actionName, std::string statusLabel, std::string message) { m_lastActionName = std::move(actionName); m_lastStatusLabel = std::move(statusLabel); m_lastMessage = std::move(message); if (m_lastStatusLabel == "Rejected") { m_lastStatusColor = kDanger; } else if (m_lastStatusLabel == "NoOp") { m_lastStatusColor = kWarning; } else if (m_lastStatusLabel == "Pending") { m_lastStatusColor = kTextMuted; } else { m_lastStatusColor = kSuccess; } } bool IsButtonEnabled(ActionId action) const { const UIEditorWorkspaceModel& workspace = m_controller.GetWorkspace(); const UIEditorWorkspaceSession& session = m_controller.GetSession(); switch (action) { case ActionId::HideActive: { if (workspace.activePanelId.empty()) { return false; } const auto* state = FindUIEditorPanelSessionState(session, workspace.activePanelId); return state != nullptr && state->open && state->visible; } case ActionId::SaveLayout: return m_controller.ValidateState().IsValid(); case ActionId::CloseDocB: { const auto* state = FindUIEditorPanelSessionState(session, "doc-b"); return state != nullptr && state->open; } case ActionId::LoadLayout: return m_hasSavedLayout; case ActionId::ActivateDetails: { const auto* state = FindUIEditorPanelSessionState(session, "details"); return state != nullptr && state->open && state->visible && workspace.activePanelId != "details"; } case ActionId::LoadInvalid: return m_controller.ValidateState().IsValid(); case ActionId::Reset: return true; } return false; } void DrawPanelStateRows( UIDrawList& drawList, float startX, float startY, float width, const UIEditorWorkspaceSession& session, std::string_view activePanelId) { const std::vector> panelDefs = { { "doc-a", "Document A" }, { "doc-b", "Document B" }, { "details", "Details" } }; float rowY = startY; for (const auto& [panelId, label] : panelDefs) { const UIRect rowRect(startX, rowY, width, 54.0f); drawList.AddFilledRect(rowRect, kPanelRowBg, 8.0f); drawList.AddRectOutline(rowRect, kCardBorder, 1.0f, 8.0f); drawList.AddFilledRect( UIRect(rowRect.x + 12.0f, rowRect.y + 15.0f, 10.0f, 10.0f), ResolvePanelStateColor(session, panelId), 5.0f); drawList.AddText( UIPoint(rowRect.x + 32.0f, rowRect.y + 11.0f), DescribePanelState(session, panelId, label), kTextPrimary, 14.0f); const bool active = activePanelId == panelId; drawList.AddText( UIPoint(rowRect.x + 32.0f, rowRect.y + 31.0f), active ? "active = true" : "active = false", active ? kAccent : kTextMuted, 12.0f); rowY += 64.0f; } } void BuildDrawData(UIDrawData& drawData, float width, float height) { const UIEditorWorkspaceModel& workspace = m_controller.GetWorkspace(); const UIEditorWorkspaceSession& session = m_controller.GetSession(); const auto validation = m_controller.ValidateState(); UIDrawList& drawList = drawData.EmplaceDrawList("Editor Layout Persistence"); drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); const float margin = 20.0f; const UIRect headerRect(margin, margin, width - margin * 2.0f, 178.0f); const UIRect actionRect(margin, headerRect.y + headerRect.height + 16.0f, 320.0f, height - 254.0f); const UIRect stateRect(actionRect.x + actionRect.width + 16.0f, actionRect.y, width - actionRect.width - margin * 2.0f - 16.0f, height - 254.0f); const UIRect footerRect(margin, height - 100.0f, width - margin * 2.0f, 80.0f); DrawCard(drawList, headerRect, "测试功能:Editor Layout Persistence", "只验证 Save / Load / Load Invalid / Reset 的布局恢复链路;不验证业务面板。"); drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 70.0f), "1. 点 `1 Hide Active`,把 Document A 隐藏;active 应切到 `doc-b`,visible 应变成 `doc-b, details`。", kTextPrimary, 13.0f); drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 92.0f), "2. 点 `2 Save Layout` 保存当前布局;右侧 Saved 摘要必须记录刚才的 active/visible。", kTextPrimary, 13.0f); drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 114.0f), "3. 再点 `3 Close Doc B` 或 `5 Activate Details` 改状态,然后点 `4 Load Layout`;当前状态必须恢复到保存时。", kTextPrimary, 13.0f); drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 136.0f), "4. 点 `6 Load Invalid`,Result 必须是 Rejected,且当前 active/visible 不得被污染。", kTextPrimary, 13.0f); drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 158.0f), "5. `R Reset` 必须回到基线 active=doc-a;按 `F12` 保存当前窗口截图。", kTextPrimary, 13.0f); DrawCard(drawList, actionRect, "操作区", "这里只保留这一步需要检查的布局保存/恢复动作。"); DrawCard(drawList, stateRect, "状态摘要", "左侧看 Current,右侧看 Saved;重点检查 active、visible 和坏数据恢复。"); DrawCard(drawList, footerRect, "最近结果", "显示最近一次操作、状态和当前 validation。"); m_buttons.clear(); const std::vector> buttonDefs = { { ActionId::HideActive, "1 Hide Active" }, { ActionId::SaveLayout, "2 Save Layout" }, { ActionId::CloseDocB, "3 Close Doc B" }, { ActionId::LoadLayout, "4 Load Layout" }, { ActionId::ActivateDetails, "5 Activate Details" }, { ActionId::LoadInvalid, "6 Load Invalid" }, { ActionId::Reset, "R Reset" } }; float buttonY = actionRect.y + 72.0f; for (const auto& [action, label] : buttonDefs) { ButtonState button = {}; button.action = action; button.label = label; button.rect = UIRect(actionRect.x + 18.0f, buttonY, actionRect.width - 36.0f, 46.0f); button.enabled = IsButtonEnabled(action); m_buttons.push_back(button); drawList.AddFilledRect( button.rect, button.enabled ? kButtonEnabled : kButtonDisabled, 8.0f); drawList.AddRectOutline(button.rect, kButtonBorder, 1.0f, 8.0f); drawList.AddText( UIPoint(button.rect.x + 14.0f, button.rect.y + 13.0f), button.label, button.enabled ? kTextPrimary : kTextMuted, 13.0f); buttonY += 58.0f; } const float columnGap = 20.0f; const float contentLeft = stateRect.x + 18.0f; const float columnWidth = (stateRect.width - 36.0f - columnGap) * 0.5f; const float rightColumnX = contentLeft + columnWidth + columnGap; drawList.AddText(UIPoint(contentLeft, stateRect.y + 70.0f), "Current", kAccent, 15.0f); drawList.AddText(UIPoint(contentLeft, stateRect.y + 96.0f), "active panel", kTextMuted, 12.0f); drawList.AddText(UIPoint(contentLeft, stateRect.y + 116.0f), workspace.activePanelId.empty() ? "(none)" : workspace.activePanelId, kTextPrimary, 14.0f); drawList.AddText(UIPoint(contentLeft, stateRect.y + 148.0f), "visible panels", kTextMuted, 12.0f); drawList.AddText(UIPoint(contentLeft, stateRect.y + 168.0f), JoinVisiblePanelIds(workspace, session), kTextPrimary, 14.0f); drawList.AddText( UIPoint(contentLeft, stateRect.y + 198.0f), validation.IsValid() ? "validation = OK" : "validation = " + validation.message, validation.IsValid() ? kSuccess : kDanger, 12.0f); DrawPanelStateRows(drawList, contentLeft, stateRect.y + 236.0f, columnWidth, session, workspace.activePanelId); drawList.AddText(UIPoint(rightColumnX, stateRect.y + 70.0f), "Saved", kAccent, 15.0f); if (!m_hasSavedLayout) { drawList.AddText(UIPoint(rightColumnX, stateRect.y + 104.0f), "尚未执行 Save Layout。", kTextMuted, 13.0f); drawList.AddText(UIPoint(rightColumnX, stateRect.y + 128.0f), "先把状态改掉,再保存,这样 Load 才有恢复意义。", kTextMuted, 12.0f); } else { drawList.AddText(UIPoint(rightColumnX, stateRect.y + 96.0f), "active panel", kTextMuted, 12.0f); drawList.AddText(UIPoint(rightColumnX, stateRect.y + 116.0f), m_savedSnapshot.workspace.activePanelId.empty() ? "(none)" : m_savedSnapshot.workspace.activePanelId, kTextPrimary, 14.0f); drawList.AddText(UIPoint(rightColumnX, stateRect.y + 148.0f), "visible panels", kTextMuted, 12.0f); drawList.AddText( UIPoint(rightColumnX, stateRect.y + 168.0f), JoinVisiblePanelIds(m_savedSnapshot.workspace, m_savedSnapshot.session), kTextPrimary, 14.0f); drawList.AddText(UIPoint(rightColumnX, stateRect.y + 198.0f), "payload preview", kTextMuted, 12.0f); drawList.AddText( UIPoint(rightColumnX, stateRect.y + 220.0f), BuildSerializedPreview(m_savedSerializedLayout), kTextPrimary, 12.0f); } drawList.AddText( UIPoint(footerRect.x + 18.0f, footerRect.y + 28.0f), "Last operation: " + m_lastActionName + " | Result: " + m_lastStatusLabel, m_lastStatusColor, 13.0f); drawList.AddText(UIPoint(footerRect.x + 18.0f, footerRect.y + 48.0f), m_lastMessage, kTextPrimary, 12.0f); drawList.AddText( UIPoint(footerRect.x + 18.0f, footerRect.y + 66.0f), validation.IsValid() ? "Validation: OK" : "Validation: " + validation.message, validation.IsValid() ? kSuccess : kDanger, 12.0f); const std::string captureSummary = m_autoScreenshot.HasPendingCapture() ? "截图排队中..." : (m_autoScreenshot.GetLastCaptureSummary().empty() ? std::string("F12 -> tests/UI/Editor/integration/state/layout_persistence/captures/") : m_autoScreenshot.GetLastCaptureSummary()); drawList.AddText(UIPoint(footerRect.x + 760.0f, footerRect.y + 66.0f), captureSummary, kTextMuted, 12.0f); } HWND m_hwnd = nullptr; HINSTANCE m_hInstance = nullptr; ATOM m_windowClassAtom = 0; NativeRenderer m_renderer = {}; AutoScreenshotController m_autoScreenshot = {}; UIEditorWorkspaceController m_controller = {}; UIEditorWorkspaceLayoutSnapshot m_savedSnapshot = {}; std::string m_savedSerializedLayout = {}; bool m_hasSavedLayout = false; std::vector m_buttons = {}; std::string m_lastActionName = {}; std::string m_lastStatusLabel = {}; std::string m_lastMessage = {}; UIColor m_lastStatusColor = kTextMuted; }; } // namespace int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { ScenarioApp app; return app.Run(hInstance, nCmdShow); }