Add XCUI input state validation sandbox batch

This commit is contained in:
2026-04-05 22:35:24 +08:00
parent 1908e81e5c
commit 5342e447af
7 changed files with 376 additions and 133 deletions

View File

@@ -27,6 +27,7 @@ using ::XCEngine::UI::UIInputEvent;
using ::XCEngine::UI::UIInputEventType;
using ::XCEngine::UI::UIInputModifiers;
using ::XCEngine::UI::UIPoint;
using ::XCEngine::UI::UIPointerButton;
using ::XCEngine::UI::UIRect;
using ::XCEngine::UI::Runtime::UIScreenFrameInput;
@@ -57,6 +58,19 @@ std::string TruncateText(const std::string& text, std::size_t maxLength) {
return text.substr(0, maxLength - 3u) + "...";
}
std::string ExtractStateKeyTail(const std::string& stateKey) {
if (stateKey.empty()) {
return "-";
}
const std::size_t separator = stateKey.find_last_of('/');
if (separator == std::string::npos || separator + 1u >= stateKey.size()) {
return stateKey;
}
return stateKey.substr(separator + 1u);
}
std::string FormatFloat(float value) {
std::ostringstream stream;
stream.setf(std::ios::fixed, std::ios::floatfield);
@@ -255,6 +269,29 @@ void Application::OnResize(UINT width, UINT height) {
m_renderer.Resize(width, height);
}
void Application::QueuePointerEvent(UIInputEventType type, UIPointerButton button, WPARAM wParam, LPARAM lParam) {
UIInputEvent event = {};
event.type = type;
event.pointerButton = button;
event.position = UIPoint(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
event.modifiers = BuildInputModifiers(static_cast<size_t>(wParam));
m_pendingInputEvents.push_back(event);
}
void Application::QueuePointerLeaveEvent() {
UIInputEvent event = {};
event.type = UIInputEventType::PointerLeave;
if (m_hwnd != nullptr) {
POINT clientPoint = {};
GetCursorPos(&clientPoint);
ScreenToClient(m_hwnd, &clientPoint);
event.position = UIPoint(static_cast<float>(clientPoint.x), static_cast<float>(clientPoint.y));
}
m_pendingInputEvents.push_back(event);
}
void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) {
if (m_hwnd == nullptr) {
return;
@@ -272,7 +309,6 @@ void Application::QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM
event.wheelDelta = static_cast<float>(wheelDelta);
event.modifiers = BuildInputModifiers(static_cast<size_t>(wParam));
m_pendingInputEvents.push_back(event);
m_autoScreenshot.RequestCapture("wheel");
}
bool Application::LoadStructuredScreen(const char* triggerReason) {
@@ -286,10 +322,6 @@ bool Application::LoadStructuredScreen(const char* triggerReason) {
m_runtimeStatus = loaded ? "Authored XCUI" : "Fallback Sandbox";
m_runtimeError = loaded ? std::string() : m_screenPlayer.GetLastError();
RebuildTrackedFileStates();
std::string screenshotReason = triggerReason != nullptr ? triggerReason : "update";
screenshotReason += loaded ? "_authored" : "_fallback";
m_autoScreenshot.RequestCapture(std::move(screenshotReason));
return loaded;
}
@@ -380,47 +412,31 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
: "Using native fallback while authored UI is invalid.");
if (authoredMode) {
const auto& scrollDebug = m_documentHost.GetScrollDebugSnapshot();
if (!scrollDebug.primaryTargetStateKey.empty()) {
detailLines.push_back(
"Primary: " +
TruncateText(scrollDebug.primaryTargetStateKey, 52u));
detailLines.push_back(
"Primary viewport: " +
FormatRect(scrollDebug.primaryViewportRect));
detailLines.push_back(
"Primary overflow: " +
FormatFloat(scrollDebug.primaryOverflow));
}
const auto& inputDebug = m_documentHost.GetInputDebugSnapshot();
detailLines.push_back(
"Wheel total/handled: " +
std::to_string(scrollDebug.totalWheelEventCount) +
" / " +
std::to_string(scrollDebug.handledWheelEventCount));
if (scrollDebug.totalWheelEventCount > 0u) {
"Hover | Focus: " +
ExtractStateKeyTail(inputDebug.hoveredStateKey) +
" | " +
ExtractStateKeyTail(inputDebug.focusedStateKey));
detailLines.push_back(
"Active | Capture: " +
ExtractStateKeyTail(inputDebug.activeStateKey) +
" | " +
ExtractStateKeyTail(inputDebug.captureStateKey));
if (!inputDebug.lastEventType.empty()) {
detailLines.push_back(
"Last wheel " +
FormatFloat(scrollDebug.lastWheelDelta) +
"Last input: " +
inputDebug.lastEventType +
" at " +
FormatPoint(scrollDebug.lastPointerPosition));
FormatPoint(inputDebug.pointerPosition));
detailLines.push_back(
"Route: " +
inputDebug.lastTargetKind +
" -> " +
ExtractStateKeyTail(inputDebug.lastTargetStateKey));
detailLines.push_back(
"Result: " +
(scrollDebug.lastResult.empty() ? std::string("n/a") : scrollDebug.lastResult));
if (!scrollDebug.lastTargetStateKey.empty()) {
detailLines.push_back(
"Target: " +
TruncateText(scrollDebug.lastTargetStateKey, 52u));
detailLines.push_back(
"Viewport: " +
FormatRect(scrollDebug.lastViewportRect));
detailLines.push_back(
"Overflow/offset: " +
FormatFloat(scrollDebug.lastOverflow) +
" | " +
FormatFloat(scrollDebug.lastOffsetBefore) +
" -> " +
FormatFloat(scrollDebug.lastOffsetAfter));
}
(inputDebug.lastResult.empty() ? std::string("n/a") : inputDebug.lastResult));
}
}
@@ -428,6 +444,8 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
detailLines.push_back("Shot pending...");
} else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) {
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u));
} else {
detailLines.push_back("Screenshots: manual only (F12)");
}
if (!m_runtimeError.empty()) {
@@ -494,12 +512,57 @@ LRESULT CALLBACK Application::WndProc(HWND hwnd, UINT message, WPARAM wParam, LP
return 0;
}
break;
case WM_MOUSEMOVE:
if (application != nullptr) {
if (!application->m_trackingMouseLeave) {
TRACKMOUSEEVENT trackMouseEvent = {};
trackMouseEvent.cbSize = sizeof(trackMouseEvent);
trackMouseEvent.dwFlags = TME_LEAVE;
trackMouseEvent.hwndTrack = hwnd;
if (TrackMouseEvent(&trackMouseEvent)) {
application->m_trackingMouseLeave = true;
}
}
application->QueuePointerEvent(UIInputEventType::PointerMove, UIPointerButton::None, wParam, lParam);
return 0;
}
break;
case WM_MOUSELEAVE:
if (application != nullptr) {
application->m_trackingMouseLeave = false;
application->QueuePointerLeaveEvent();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (application != nullptr) {
SetFocus(hwnd);
SetCapture(hwnd);
application->QueuePointerEvent(UIInputEventType::PointerButtonDown, UIPointerButton::Left, wParam, lParam);
return 0;
}
break;
case WM_LBUTTONUP:
if (application != nullptr) {
if (GetCapture() == hwnd) {
ReleaseCapture();
}
application->QueuePointerEvent(UIInputEventType::PointerButtonUp, UIPointerButton::Left, wParam, lParam);
return 0;
}
break;
case WM_MOUSEWHEEL:
if (application != nullptr) {
application->QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam);
return 0;
}
break;
case WM_KEYDOWN:
if (application != nullptr && wParam == VK_F12) {
application->m_autoScreenshot.RequestCapture("manual_f12");
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:

View File

@@ -41,6 +41,8 @@ private:
void Shutdown();
void RenderFrame();
void OnResize(UINT width, UINT height);
void QueuePointerEvent(::XCEngine::UI::UIInputEventType type, ::XCEngine::UI::UIPointerButton button, WPARAM wParam, LPARAM lParam);
void QueuePointerLeaveEvent();
void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam);
bool LoadStructuredScreen(const char* triggerReason);
void RefreshStructuredScreen();
@@ -63,6 +65,7 @@ private:
std::chrono::steady_clock::time_point m_lastReloadPollTime = {};
std::uint64_t m_frameIndex = 0;
std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {};
bool m_trackingMouseLeave = false;
bool m_useStructuredScreen = false;
std::string m_runtimeStatus = {};
std::string m_runtimeError = {};

View File

@@ -1,92 +1,30 @@
<View
name="NewEditorShell"
theme="../themes/editor_shell.xctheme">
<Column padding="18" gap="14">
<Column padding="24" gap="16">
<Card
title="XCUI Editor Sandbox"
subtitle="Editor-layer proving ground | direct native renderer | hot reload"
title="XCUI Core Validation"
subtitle="Current batch: input state routing | minimal sandbox | native renderer"
tone="accent"
height="86">
<Row gap="10">
<Button text="Shell" />
<Button text="Panels" />
<Button text="Schema First" />
</Row>
height="90">
<Column gap="8">
<Text text="这个试验面板只保留当前批次需要检查的控件。" />
<Text text="无关的 editor 壳层面板暂时不放进来,避免干扰检查。" />
</Column>
</Card>
<Row gap="14" height="fill">
<Card title="Hierarchy" subtitle="scene graph" width="280">
<Column gap="8">
<Text text="Sandbox_City.xcscene" />
<Button text="World" />
<Button text="Main Camera" />
<Button text="Directional Light" />
<Button text="Player" tone="accent" />
<Button text="WeaponSocket" />
<Button text="FX_Trail" />
</Column>
</Card>
<Column width="fill" gap="14">
<Card
title="Scene"
subtitle="ViewportSlot comes later | shell composition first"
tone="accent-alt"
height="fill">
<Column gap="10">
<Row gap="10">
<Button text="Translate" />
<Button text="Rotate" />
<Button text="Scale" />
</Row>
<Text text="This sandbox stays native-rendered and does not host ImGui." />
<Text text="Use it to iterate shell chrome, panel composition, and authored XCUI." />
<Button text="Selection: Player" tone="accent" />
<Button text="Camera: Perspective" />
</Column>
</Card>
<Card title="Console" subtitle="wheel-scroll core validation" height="220">
<ScrollView id="console-scroll" height="fill">
<Column gap="8">
<Text text="Scroll here with the mouse wheel." />
<Text text="Check that content clips cleanly inside this card." />
<Text text="Info XCUI authored screen loaded." />
<Text text="Info Theme + schema resources are tracked for reload." />
<Text text="Warn Viewport host stays out of scope in this phase." />
<Text text="Todo Replace shell placeholders with Editor widgets." />
<Text text="Trace ScrollView should retain offset across frames." />
<Text text="Trace Wheel input should only affect the hovered view." />
<Text text="Trace Hidden rows must stay clipped below the footer." />
<Text text="Trace Resize should not corrupt the scroll offset clamp." />
<Text text="Trace Hot reload should rebuild layout without tearing." />
<Text text="Trace This panel exists only to validate XCUI core scroll." />
</Column>
</ScrollView>
</Card>
<Card title="Input Core" subtitle="hover focus active capture" height="196">
<Column gap="12">
<Text text="这一轮只需要检查下面这三个按钮。" />
<Row gap="12">
<Button id="input-hover" text="Hover / Focus" />
<Button id="input-capture" text="Pointer Capture" capturePointer="true" />
<Button id="input-route" text="Route Target" />
</Row>
<Text text="1. 鼠标移到左侧按钮hover 应该变化focus 应该保持空。" />
<Text text="2. 按住中间按钮不放focus、active、capture 都应该停在中间按钮。" />
<Text text="3. 按住中间按钮拖到右侧再松开hover 移到右侧capture 清空focus 仍留在中间。" />
</Column>
<Card
title="Inspector"
subtitle="schema-first scaffold"
width="336"
schema="../schemas/editor_inspector_shell.xcschema">
<Column gap="8">
<Text text="Transform" />
<Button text="Position 12.0, 1.8, -4.0" />
<Button text="Rotation 0.0, 36.5, 0.0" />
<Button text="Scale 1.0, 1.0, 1.0" />
<Text text="Character" />
<Button text="State Locomotion" />
<Button text="MoveSpeed 6.4" />
<Button text="JumpHeight 1.3" />
</Column>
</Card>
</Row>
<Card
title="Status"
subtitle="No ImGui host | authored XCUI drives the shell | fallback stays for resilience"
height="58" />
</Card>
</Column>
</View>