feat(xcui): advance core and editor validation flow

This commit is contained in:
2026-04-06 16:20:46 +08:00
parent 33bb84f650
commit 2d030a97da
128 changed files with 9961 additions and 773 deletions

View File

@@ -11,14 +11,11 @@
#include <unordered_set>
#include <vector>
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
namespace XCEngine::Tests::EditorUI {
namespace {
using ::XCEngine::Input::KeyCode;
using ::XCEngine::UI::UIColor;
using ::XCEngine::UI::UIDrawData;
using ::XCEngine::UI::UIDrawList;
@@ -28,10 +25,9 @@ using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIPointerButton;
using ::XCEngine::UI::UIRect;
using ::XCEngine::UI::Runtime::UIScreenFrameInput;
using ::XCEngine::Input::KeyCode;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorValidationHost";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor Validation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor 验证";
constexpr auto kReloadPollInterval = std::chrono::milliseconds(150);
constexpr UIColor kOverlayBgColor(0.10f, 0.10f, 0.10f, 0.95f);
@@ -45,14 +41,6 @@ Application* GetApplicationFromWindow(HWND hwnd) {
return reinterpret_cast<Application*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
}
std::filesystem::path GetRepoRootPath() {
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();
}
std::string TruncateText(const std::string& text, std::size_t maxLength) {
if (text.size() <= maxLength) {
return text;
@@ -90,14 +78,6 @@ std::string FormatPoint(const UIPoint& point) {
return "(" + FormatFloat(point.x) + ", " + FormatFloat(point.y) + ")";
}
std::string FormatRect(const UIRect& rect) {
return "(" + FormatFloat(rect.x) +
", " + FormatFloat(rect.y) +
", " + FormatFloat(rect.width) +
", " + FormatFloat(rect.height) +
")";
}
std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) {
switch (wParam) {
case 'A': return static_cast<std::int32_t>(KeyCode::A);
@@ -242,14 +222,14 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
return false;
}
m_startTime = std::chrono::steady_clock::now();
m_lastFrameTime = m_startTime;
m_lastFrameTime = std::chrono::steady_clock::now();
const EditorValidationScenario* initialScenario = m_requestedScenarioId.empty()
? &GetDefaultEditorValidationScenario()
: FindEditorValidationScenario(m_requestedScenarioId);
if (initialScenario == nullptr) {
initialScenario = &GetDefaultEditorValidationScenario();
}
m_autoScreenshot.Initialize(initialScenario->captureRootPath);
LoadStructuredScreen("startup");
return true;
@@ -315,12 +295,12 @@ void Application::RenderFrame() {
m_runtimeStatus = m_activeScenario != nullptr
? m_activeScenario->displayName
: "Editor UI Validation";
: "Editor UI 验证";
m_runtimeError = frame.errorMessage;
}
if (drawData.Empty()) {
m_runtimeStatus = "Editor UI Validation | Load Error";
m_runtimeStatus = "Editor UI 验证 | 加载失败";
if (m_runtimeError.empty() && !m_screenPlayer.IsLoaded()) {
m_runtimeError = m_screenPlayer.GetLastError();
}
@@ -352,7 +332,7 @@ void Application::QueuePointerEvent(UIInputEventType type, UIPointerButton butto
event.position = UIPoint(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<size_t>(wParam));
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
m_pendingInputEvents.push_back(event);
}
@@ -383,7 +363,7 @@ void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM
event.type = UIInputEventType::PointerWheel;
event.position = UIPoint(static_cast<float>(screenPoint.x), static_cast<float>(screenPoint.y));
event.wheelDelta = static_cast<float>(wheelDelta);
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<size_t>(wParam));
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
m_pendingInputEvents.push_back(event);
}
@@ -412,6 +392,7 @@ void Application::QueueWindowFocusEvent(UIInputEventType type) {
bool Application::LoadStructuredScreen(const char* triggerReason) {
(void)triggerReason;
std::string scenarioLoadWarning = {};
const EditorValidationScenario* scenario = m_requestedScenarioId.empty()
? &GetDefaultEditorValidationScenario()
@@ -429,7 +410,7 @@ bool Application::LoadStructuredScreen(const char* triggerReason) {
const bool loaded = m_screenPlayer.Load(m_screenAsset);
m_useStructuredScreen = loaded;
m_runtimeStatus = loaded ? scenario->displayName : "Editor UI Validation | Load Error";
m_runtimeStatus = loaded ? scenario->displayName : "Editor UI 验证 | 加载失败";
m_runtimeError = loaded
? scenarioLoadWarning
: (scenarioLoadWarning.empty()
@@ -518,19 +499,19 @@ bool Application::DetectTrackedFileChange() const {
void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float height) const {
const bool authoredMode = m_useStructuredScreen && m_screenPlayer.IsLoaded();
const float panelWidth = authoredMode ? 460.0f : 360.0f;
const float panelWidth = authoredMode ? 470.0f : 390.0f;
std::vector<std::string> detailLines = {};
detailLines.push_back(
authoredMode
? "Hot reload watches authored UI resources."
: "Authored validation scene failed to load.");
? "热重载正在监听当前 Editor 集成测试资源。"
: "当前 Editor 验证场景加载失败。");
if (m_activeScenario != nullptr) {
detailLines.push_back("Scenario: " + m_activeScenario->id);
detailLines.push_back("当前场景: " + m_activeScenario->id);
}
detailLines.push_back("验证范围: Splitter / TabStrip / Panel Frame / 占位内容");
if (authoredMode) {
const auto& inputDebug = m_documentHost.GetInputDebugSnapshot();
const auto& scrollDebug = m_documentHost.GetScrollDebugSnapshot();
detailLines.push_back(
"Hover | Focus: " +
ExtractStateKeyTail(inputDebug.hoveredStateKey) +
@@ -541,32 +522,6 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
ExtractStateKeyTail(inputDebug.activeStateKey) +
" | " +
ExtractStateKeyTail(inputDebug.captureStateKey));
detailLines.push_back(
"Scope W/P/Wg: " +
ExtractStateKeyTail(inputDebug.windowScopeStateKey) +
" | " +
ExtractStateKeyTail(inputDebug.panelScopeStateKey) +
" | " +
ExtractStateKeyTail(inputDebug.widgetScopeStateKey));
detailLines.push_back(
std::string("Text input: ") +
(inputDebug.textInputActive ? "active" : "idle"));
if (!inputDebug.recentShortcutCommandId.empty()) {
detailLines.push_back(
"Recent shortcut: " +
inputDebug.recentShortcutScope +
" -> " +
inputDebug.recentShortcutCommandId);
detailLines.push_back(
std::string("Recent shortcut state: ") +
(inputDebug.recentShortcutHandled
? "handled"
: (inputDebug.recentShortcutSuppressed ? "suppressed" : "observed")) +
" @ " +
ExtractStateKeyTail(inputDebug.recentShortcutOwnerStateKey));
} else {
detailLines.push_back("Recent shortcut: none");
}
if (!inputDebug.lastEventType.empty()) {
const std::string eventPosition = inputDebug.lastEventType == "KeyDown" ||
inputDebug.lastEventType == "KeyUp" ||
@@ -576,59 +531,26 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
? std::string()
: " at " + FormatPoint(inputDebug.pointerPosition);
detailLines.push_back(
"Last input: " +
"最近输入: " +
inputDebug.lastEventType +
eventPosition);
detailLines.push_back(
"Route: " +
"命中路径: " +
inputDebug.lastTargetKind +
" -> " +
ExtractStateKeyTail(inputDebug.lastTargetStateKey));
if (!inputDebug.lastShortcutCommandId.empty()) {
detailLines.push_back(
"Shortcut: " +
inputDebug.lastShortcutScope +
" -> " +
inputDebug.lastShortcutCommandId);
detailLines.push_back(
std::string("Shortcut state: ") +
(inputDebug.lastShortcutHandled
? "handled"
: (inputDebug.lastShortcutSuppressed ? "suppressed" : "observed")) +
" @ " +
ExtractStateKeyTail(inputDebug.lastShortcutOwnerStateKey));
}
detailLines.push_back(
"Last event result: " +
"Result: " +
(inputDebug.lastResult.empty() ? std::string("n/a") : inputDebug.lastResult));
}
detailLines.push_back(
"Scroll target | Primary: " +
ExtractStateKeyTail(scrollDebug.lastTargetStateKey) +
" | " +
ExtractStateKeyTail(scrollDebug.primaryTargetStateKey));
detailLines.push_back(
"Scroll offset B/A: " +
FormatFloat(scrollDebug.lastOffsetBefore) +
" -> " +
FormatFloat(scrollDebug.lastOffsetAfter) +
" | overflow " +
FormatFloat(scrollDebug.lastOverflow));
detailLines.push_back(
"Scroll H/T: " +
std::to_string(scrollDebug.handledWheelEventCount) +
"/" +
std::to_string(scrollDebug.totalWheelEventCount) +
" | " +
(scrollDebug.lastResult.empty() ? std::string("n/a") : scrollDebug.lastResult));
}
if (m_autoScreenshot.HasPendingCapture()) {
detailLines.push_back("Shot pending...");
detailLines.push_back("截图排队中...");
} else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) {
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u));
} else {
detailLines.push_back("Screenshots: F12 -> current scenario captures/");
detailLines.push_back("截图: F12 -> 当前场景 captures/");
}
if (!m_runtimeError.empty()) {
@@ -636,13 +558,13 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
} else if (!m_autoScreenshot.GetLastCaptureError().empty()) {
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureError(), 78u));
} else if (!authoredMode) {
detailLines.push_back("No fallback sandbox is rendered in this host.");
detailLines.push_back("当前宿主不会回退到 sandbox 画面。");
}
const float panelHeight = 38.0f + static_cast<float>(detailLines.size()) * 18.0f;
const UIRect panelRect(width - panelWidth - 16.0f, height - panelHeight - 42.0f, panelWidth, panelHeight);
UIDrawList& overlay = drawData.EmplaceDrawList("Editor UI Validation Overlay");
UIDrawList& overlay = drawData.EmplaceDrawList("Editor UI 验证浮层");
overlay.AddFilledRect(panelRect, kOverlayBgColor, 10.0f);
overlay.AddRectOutline(panelRect, kOverlayBorderColor, 1.0f, 10.0f);
overlay.AddFilledRect(
@@ -651,7 +573,7 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
4.0f);
overlay.AddText(
UIPoint(panelRect.x + 28.0f, panelRect.y + 10.0f),
m_runtimeStatus.empty() ? "Editor UI Validation" : m_runtimeStatus,
m_runtimeStatus.empty() ? "Editor UI 验证" : m_runtimeStatus,
kOverlayTextPrimary,
14.0f);
@@ -669,10 +591,6 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
}
}
std::filesystem::path Application::ResolveRepoRelativePath(const char* relativePath) {
return (GetRepoRootPath() / relativePath).lexically_normal();
}
LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCCREATE) {
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);

View File

@@ -5,6 +5,7 @@
#endif
#include "EditorValidationScenario.h"
#include <XCNewEditor/Host/AutoScreenshot.h>
#include <XCNewEditor/Host/InputModifierTracker.h>
#include <XCNewEditor/Host/NativeRenderer.h>
@@ -53,7 +54,6 @@ private:
void RebuildTrackedFileStates();
bool DetectTrackedFileChange() const;
void AppendRuntimeOverlay(::XCEngine::UI::UIDrawData& drawData, float width, float height) const;
static std::filesystem::path ResolveRepoRelativePath(const char* relativePath);
HWND m_hwnd = nullptr;
HINSTANCE m_hInstance = nullptr;
@@ -66,7 +66,6 @@ private:
const EditorValidationScenario* m_activeScenario = nullptr;
std::string m_requestedScenarioId = {};
std::vector<TrackedFileState> m_trackedFiles = {};
std::chrono::steady_clock::time_point m_startTime = {};
std::chrono::steady_clock::time_point m_lastFrameTime = {};
std::chrono::steady_clock::time_point m_lastReloadPollTime = {};
std::uint64_t m_frameIndex = 0;

View File

@@ -17,6 +17,7 @@ fs::path RepoRootPath() {
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
root = root.substr(1u, root.size() - 2u);
}
return fs::path(root).lexically_normal();
}
@@ -24,72 +25,19 @@ fs::path RepoRelative(const char* relativePath) {
return (RepoRootPath() / relativePath).lexically_normal();
}
const std::array<EditorValidationScenario, 7>& GetEditorValidationScenarios() {
static const std::array<EditorValidationScenario, 7> scenarios = { {
const std::array<EditorValidationScenario, 1>& GetEditorValidationScenarios() {
static const std::array<EditorValidationScenario, 1> scenarios = { {
{
"editor.input.keyboard_focus",
"editor.shell.workspace_compose",
UIValidationDomain::Editor,
"input",
"Editor Input | Keyboard Focus",
RepoRelative("tests/UI/Editor/integration/input/keyboard_focus/View.xcui"),
"shell",
"Editor 壳层 | 工作区组合",
RepoRelative("tests/UI/Editor/integration/workspace_shell_compose/View.xcui"),
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
RepoRelative("tests/UI/Editor/integration/input/keyboard_focus/captures")
},
{
"editor.input.pointer_states",
UIValidationDomain::Editor,
"input",
"Editor Input | Pointer States",
RepoRelative("tests/UI/Editor/integration/input/pointer_states/View.xcui"),
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
RepoRelative("tests/UI/Editor/integration/input/pointer_states/captures")
},
{
"editor.input.scroll_view",
UIValidationDomain::Editor,
"input",
"Editor Input | Scroll View",
RepoRelative("tests/UI/Editor/integration/input/scroll_view/View.xcui"),
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
RepoRelative("tests/UI/Editor/integration/input/scroll_view/captures")
},
{
"editor.input.shortcut_scope",
UIValidationDomain::Editor,
"input",
"Editor Input | Shortcut Scope",
RepoRelative("tests/UI/Editor/integration/input/shortcut_scope/View.xcui"),
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
RepoRelative("tests/UI/Editor/integration/input/shortcut_scope/captures")
},
{
"editor.layout.splitter_resize",
UIValidationDomain::Editor,
"layout",
"Editor Layout | Splitter Resize",
RepoRelative("tests/UI/Editor/integration/layout/splitter_resize/View.xcui"),
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
RepoRelative("tests/UI/Editor/integration/layout/splitter_resize/captures")
},
{
"editor.layout.tab_strip_selection",
UIValidationDomain::Editor,
"layout",
"Editor Layout | TabStrip Selection",
RepoRelative("tests/UI/Editor/integration/layout/tab_strip_selection/View.xcui"),
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
RepoRelative("tests/UI/Editor/integration/layout/tab_strip_selection/captures")
},
{
"editor.layout.workspace_compose",
UIValidationDomain::Editor,
"layout",
"Editor Layout | Workspace Compose",
RepoRelative("tests/UI/Editor/integration/layout/workspace_compose/View.xcui"),
RepoRelative("tests/UI/Editor/integration/shared/themes/editor_validation.xctheme"),
RepoRelative("tests/UI/Editor/integration/layout/workspace_compose/captures")
RepoRelative("tests/UI/Editor/integration/workspace_shell_compose/captures")
}
} };
return scenarios;
}

View File

@@ -7,8 +7,7 @@
namespace XCEngine::Tests::EditorUI {
enum class UIValidationDomain : unsigned char {
Editor = 0,
Runtime
Editor = 0
};
struct EditorValidationScenario {