Files
XCEngine/tests/UI/Editor/integration/state/layout_persistence/main.cpp

859 lines
31 KiB
C++
Raw Normal View History

#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorWorkspaceController.h>
#include <XCEditor/Core/UIEditorWorkspaceLayoutPersistence.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <iomanip>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#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<int>(message.wParam);
}
private:
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCCREATE) {
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(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<float>(GET_X_LPARAM(lParam)),
static_cast<float>(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<UINT>(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<float>((std::max)(clientRect.right - clientRect.left, 1L));
const float height = static_cast<float>((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<unsigned int>(width),
static_cast<unsigned int>(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<std::pair<std::string, std::string>> 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-bvisible 应变成 `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<std::pair<ActionId, std::string>> 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<ButtonState> 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);
}