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

859 lines
31 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
#include <XCEditor/Shell/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-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<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);
}