Add XCUI input state validation sandbox batch
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -8,6 +8,7 @@ set(UI_TEST_SOURCES
|
||||
test_ui_editor_panel_chrome.cpp
|
||||
test_ui_expansion_model.cpp
|
||||
test_ui_flat_hierarchy_helpers.cpp
|
||||
test_ui_input_dispatcher.cpp
|
||||
test_ui_keyboard_navigation_model.cpp
|
||||
test_ui_property_edit_model.cpp
|
||||
test_layout_engine.cpp
|
||||
|
||||
110
tests/Core/UI/test_ui_input_dispatcher.cpp
Normal file
110
tests/Core/UI/test_ui_input_dispatcher.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputDispatchRequest;
|
||||
using XCEngine::UI::UIInputDispatcher;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIInputPath;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIInputDispatcherTest, PointerDownTransfersFocusAndStartsActivePath) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath hoveredPath = { 10u, 20u, 30u };
|
||||
std::vector<UIInputDispatchRequest> routedRequests = {};
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, UIPointerButton::Left),
|
||||
hoveredPath,
|
||||
[&](const UIInputDispatchRequest& request) {
|
||||
routedRequests.push_back(request);
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_TRUE(summary.focusChange.Changed());
|
||||
EXPECT_EQ(summary.focusChange.previousPath, UIInputPath());
|
||||
EXPECT_EQ(summary.focusChange.currentPath, hoveredPath);
|
||||
EXPECT_EQ(dispatcher.GetFocusController().GetFocusedPath(), hoveredPath);
|
||||
EXPECT_EQ(dispatcher.GetFocusController().GetActivePath(), hoveredPath);
|
||||
ASSERT_FALSE(routedRequests.empty());
|
||||
EXPECT_EQ(summary.routing.plan.targetPath, hoveredPath);
|
||||
EXPECT_EQ(summary.routing.plan.targetKind, XCEngine::UI::UIInputTargetKind::Hovered);
|
||||
const auto targetIt = std::find_if(
|
||||
routedRequests.begin(),
|
||||
routedRequests.end(),
|
||||
[](const UIInputDispatchRequest& request) {
|
||||
return request.isTargetElement;
|
||||
});
|
||||
ASSERT_NE(targetIt, routedRequests.end());
|
||||
EXPECT_EQ(targetIt->elementId, hoveredPath.Target());
|
||||
}
|
||||
|
||||
TEST(UIInputDispatcherTest, PointerCaptureOverridesHoveredRouteForPointerEvents) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath hoveredPath = { 41u, 42u };
|
||||
const UIInputPath capturePath = { 7u, 8u, 9u };
|
||||
dispatcher.GetFocusController().SetPointerCapturePath(capturePath);
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakePointerEvent(UIInputEventType::PointerMove),
|
||||
hoveredPath,
|
||||
[](const UIInputDispatchRequest&) {
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_EQ(summary.routing.plan.targetKind, XCEngine::UI::UIInputTargetKind::Captured);
|
||||
EXPECT_EQ(summary.routing.plan.targetPath, capturePath);
|
||||
}
|
||||
|
||||
TEST(UIInputDispatcherTest, PointerButtonUpClearsActivePathAfterDispatch) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath activePath = { 2u, 4u, 6u };
|
||||
dispatcher.GetFocusController().SetActivePath(activePath);
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakePointerEvent(UIInputEventType::PointerButtonUp, UIPointerButton::Left),
|
||||
{},
|
||||
[](const UIInputDispatchRequest&) {
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_FALSE(summary.routing.handled);
|
||||
EXPECT_FALSE(dispatcher.GetFocusController().HasActivePath());
|
||||
}
|
||||
|
||||
TEST(UIInputDispatcherTest, KeyboardEventsRouteToFocusedPath) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath focusedPath = { 101u, 202u };
|
||||
dispatcher.GetFocusController().SetFocusedPath(focusedPath);
|
||||
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = 'F';
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
event,
|
||||
{},
|
||||
[](const UIInputDispatchRequest&) {
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_EQ(summary.routing.plan.targetKind, XCEngine::UI::UIInputTargetKind::Focused);
|
||||
EXPECT_EQ(summary.routing.plan.targetPath, focusedPath);
|
||||
}
|
||||
@@ -153,6 +153,21 @@ bool TryFindSmallestFilledRectContainingPoint(
|
||||
return found;
|
||||
}
|
||||
|
||||
bool TryFindFilledRectForText(
|
||||
const XCEngine::UI::UIDrawData& drawData,
|
||||
const std::string& text,
|
||||
XCEngine::UI::UIRect& outRect) {
|
||||
const auto* textCommand = FindTextCommand(drawData, text);
|
||||
return textCommand != nullptr &&
|
||||
TryFindSmallestFilledRectContainingPoint(drawData, textCommand->position, outRect);
|
||||
}
|
||||
|
||||
XCEngine::UI::UIPoint GetRectCenter(const XCEngine::UI::UIRect& rect) {
|
||||
return XCEngine::UI::UIPoint(
|
||||
rect.x + rect.width * 0.5f,
|
||||
rect.y + rect.height * 0.5f);
|
||||
}
|
||||
|
||||
std::size_t CountCommandsOfType(
|
||||
const XCEngine::UI::UIDrawData& drawData,
|
||||
XCEngine::UI::UIDrawCommandType type) {
|
||||
@@ -384,6 +399,122 @@ TEST(UIRuntimeTest, DocumentHostScrollViewClipsOverflowingChildrenAndRespondsToW
|
||||
EXPECT_FLOAT_EQ(line01Persisted->position.y, line01AfterY);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, DocumentHostTracksHoverFocusAndPointerCaptureAcrossFrames) {
|
||||
TempFileScope viewFile(
|
||||
"xcui_runtime_input_states",
|
||||
".xcui",
|
||||
"<View name=\"Input State Test\">\n"
|
||||
" <Column padding=\"18\" gap=\"10\">\n"
|
||||
" <Button id=\"input-hover\" text=\"Hover / Focus\" />\n"
|
||||
" <Button id=\"input-capture\" text=\"Pointer Capture\" capturePointer=\"true\" />\n"
|
||||
" <Button id=\"input-route\" text=\"Route Target\" />\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
|
||||
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.input.states")));
|
||||
|
||||
UIScreenFrameInput firstInput = BuildInputState(1u);
|
||||
firstInput.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 520.0f, 260.0f);
|
||||
const auto& firstFrame = player.Update(firstInput);
|
||||
const auto* hoverButtonText = FindTextCommand(firstFrame.drawData, "Hover / Focus");
|
||||
const auto* captureButtonText = FindTextCommand(firstFrame.drawData, "Pointer Capture");
|
||||
const auto* routeButtonText = FindTextCommand(firstFrame.drawData, "Route Target");
|
||||
ASSERT_NE(hoverButtonText, nullptr);
|
||||
ASSERT_NE(captureButtonText, nullptr);
|
||||
ASSERT_NE(routeButtonText, nullptr);
|
||||
|
||||
XCEngine::UI::UIRect hoverButtonRect = {};
|
||||
XCEngine::UI::UIRect captureButtonRect = {};
|
||||
XCEngine::UI::UIRect routeButtonRect = {};
|
||||
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Hover / Focus", hoverButtonRect));
|
||||
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Pointer Capture", captureButtonRect));
|
||||
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Route Target", routeButtonRect));
|
||||
|
||||
const XCEngine::UI::UIPoint hoverButtonPoint = GetRectCenter(hoverButtonRect);
|
||||
const XCEngine::UI::UIPoint captureButtonPoint = GetRectCenter(captureButtonRect);
|
||||
const XCEngine::UI::UIPoint routeButtonPoint = GetRectCenter(routeButtonRect);
|
||||
|
||||
SCOPED_TRACE(
|
||||
std::string("hoverPos=") + std::to_string(hoverButtonText->position.x) + "," + std::to_string(hoverButtonText->position.y) +
|
||||
" hoverRect=" + std::to_string(hoverButtonRect.x) + "," + std::to_string(hoverButtonRect.y) + "," +
|
||||
std::to_string(hoverButtonRect.width) + "," + std::to_string(hoverButtonRect.height) +
|
||||
" capturePos=" + std::to_string(captureButtonText->position.x) + "," + std::to_string(captureButtonText->position.y) +
|
||||
" captureRect=" + std::to_string(captureButtonRect.x) + "," + std::to_string(captureButtonRect.y) + "," +
|
||||
std::to_string(captureButtonRect.width) + "," + std::to_string(captureButtonRect.height) +
|
||||
" routePos=" + std::to_string(routeButtonText->position.x) + "," + std::to_string(routeButtonText->position.y) +
|
||||
" routeRect=" + std::to_string(routeButtonRect.x) + "," + std::to_string(routeButtonRect.y) + "," +
|
||||
std::to_string(routeButtonRect.width) + "," + std::to_string(routeButtonRect.height));
|
||||
|
||||
UIScreenFrameInput hoverInput = BuildInputState(2u);
|
||||
hoverInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent hoverEvent = {};
|
||||
hoverEvent.type = XCEngine::UI::UIInputEventType::PointerMove;
|
||||
hoverEvent.position = hoverButtonPoint;
|
||||
hoverInput.events.push_back(hoverEvent);
|
||||
player.Update(hoverInput);
|
||||
const auto& afterHover = host.GetInputDebugSnapshot();
|
||||
EXPECT_NE(afterHover.hoveredStateKey.find("/input-hover"), std::string::npos);
|
||||
EXPECT_TRUE(afterHover.focusedStateKey.empty());
|
||||
|
||||
UIScreenFrameInput captureDownInput = BuildInputState(3u);
|
||||
captureDownInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent captureDownEvent = {};
|
||||
captureDownEvent.type = XCEngine::UI::UIInputEventType::PointerButtonDown;
|
||||
captureDownEvent.pointerButton = XCEngine::UI::UIPointerButton::Left;
|
||||
captureDownEvent.position = captureButtonPoint;
|
||||
captureDownInput.events.push_back(captureDownEvent);
|
||||
player.Update(captureDownInput);
|
||||
const auto& afterCaptureDown = host.GetInputDebugSnapshot();
|
||||
SCOPED_TRACE(
|
||||
std::string("afterCaptureDown hovered=") + afterCaptureDown.hoveredStateKey +
|
||||
" focused=" + afterCaptureDown.focusedStateKey +
|
||||
" active=" + afterCaptureDown.activeStateKey +
|
||||
" capture=" + afterCaptureDown.captureStateKey +
|
||||
" lastKind=" + afterCaptureDown.lastTargetKind +
|
||||
" lastTarget=" + afterCaptureDown.lastTargetStateKey +
|
||||
" result=" + afterCaptureDown.lastResult);
|
||||
EXPECT_NE(afterCaptureDown.focusedStateKey.find("/input-capture"), std::string::npos);
|
||||
EXPECT_NE(afterCaptureDown.activeStateKey.find("/input-capture"), std::string::npos);
|
||||
EXPECT_NE(afterCaptureDown.captureStateKey.find("/input-capture"), std::string::npos);
|
||||
|
||||
UIScreenFrameInput dragInput = BuildInputState(4u);
|
||||
dragInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent dragEvent = {};
|
||||
dragEvent.type = XCEngine::UI::UIInputEventType::PointerMove;
|
||||
dragEvent.position = routeButtonPoint;
|
||||
dragInput.events.push_back(dragEvent);
|
||||
player.Update(dragInput);
|
||||
const auto& afterDrag = host.GetInputDebugSnapshot();
|
||||
SCOPED_TRACE(
|
||||
std::string("afterDrag hovered=") + afterDrag.hoveredStateKey +
|
||||
" focused=" + afterDrag.focusedStateKey +
|
||||
" active=" + afterDrag.activeStateKey +
|
||||
" capture=" + afterDrag.captureStateKey +
|
||||
" lastKind=" + afterDrag.lastTargetKind +
|
||||
" lastTarget=" + afterDrag.lastTargetStateKey +
|
||||
" result=" + afterDrag.lastResult);
|
||||
EXPECT_NE(afterDrag.hoveredStateKey.find("/input-route"), std::string::npos);
|
||||
EXPECT_NE(afterDrag.captureStateKey.find("/input-capture"), std::string::npos);
|
||||
EXPECT_EQ(afterDrag.lastTargetKind, "Captured");
|
||||
EXPECT_NE(afterDrag.lastTargetStateKey.find("/input-capture"), std::string::npos);
|
||||
|
||||
UIScreenFrameInput releaseInput = BuildInputState(5u);
|
||||
releaseInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent releaseEvent = {};
|
||||
releaseEvent.type = XCEngine::UI::UIInputEventType::PointerButtonUp;
|
||||
releaseEvent.pointerButton = XCEngine::UI::UIPointerButton::Left;
|
||||
releaseEvent.position = routeButtonPoint;
|
||||
releaseInput.events.push_back(releaseEvent);
|
||||
player.Update(releaseInput);
|
||||
const auto& afterRelease = host.GetInputDebugSnapshot();
|
||||
EXPECT_TRUE(afterRelease.activeStateKey.empty());
|
||||
EXPECT_TRUE(afterRelease.captureStateKey.empty());
|
||||
EXPECT_NE(afterRelease.focusedStateKey.find("/input-capture"), std::string::npos);
|
||||
EXPECT_NE(afterRelease.hoveredStateKey.find("/input-route"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, ScreenPlayerConsumeLastFrameReturnsDetachedPacketAndClearsBorrowedState) {
|
||||
TempFileScope viewFile("xcui_runtime_consume_player", ".xcui", BuildViewMarkup("Runtime Consume"));
|
||||
UIDocumentScreenHost host = {};
|
||||
|
||||
@@ -54,11 +54,9 @@ bool ContainsPathWithFilename(
|
||||
TEST(NewEditorStructuredShellTest, AuthoredEditorShellLoadsFromRepositoryResources) {
|
||||
const std::filesystem::path viewPath = RepoRelative("new_editor/ui/views/editor_shell.xcui");
|
||||
const std::filesystem::path themePath = RepoRelative("new_editor/ui/themes/editor_shell.xctheme");
|
||||
const std::filesystem::path schemaPath = RepoRelative("new_editor/ui/schemas/editor_inspector_shell.xcschema");
|
||||
|
||||
ASSERT_TRUE(std::filesystem::exists(viewPath));
|
||||
ASSERT_TRUE(std::filesystem::exists(themePath));
|
||||
ASSERT_TRUE(std::filesystem::exists(schemaPath));
|
||||
|
||||
UIScreenAsset asset = {};
|
||||
asset.screenId = "new_editor.editor_shell";
|
||||
@@ -72,7 +70,6 @@ TEST(NewEditorStructuredShellTest, AuthoredEditorShellLoadsFromRepositoryResourc
|
||||
ASSERT_NE(player.GetDocument(), nullptr);
|
||||
EXPECT_TRUE(player.GetDocument()->hasThemeDocument);
|
||||
EXPECT_TRUE(ContainsPathWithFilename(player.GetDocument()->dependencies, "editor_shell.xctheme"));
|
||||
EXPECT_TRUE(ContainsPathWithFilename(player.GetDocument()->dependencies, "editor_inspector_shell.xcschema"));
|
||||
|
||||
UIScreenFrameInput input = {};
|
||||
input.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 1440.0f, 900.0f);
|
||||
@@ -81,11 +78,11 @@ TEST(NewEditorStructuredShellTest, AuthoredEditorShellLoadsFromRepositoryResourc
|
||||
|
||||
const auto& frame = player.Update(input);
|
||||
EXPECT_TRUE(frame.stats.documentLoaded);
|
||||
EXPECT_GT(frame.stats.nodeCount, 12u);
|
||||
EXPECT_GT(frame.stats.commandCount, 20u);
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "XCUI Editor Sandbox"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Hierarchy"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Scene"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Inspector"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Status"));
|
||||
EXPECT_GT(frame.stats.nodeCount, 6u);
|
||||
EXPECT_GT(frame.stats.commandCount, 12u);
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "XCUI Core Validation"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Input Core"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Hover / Focus"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Pointer Capture"));
|
||||
EXPECT_TRUE(DrawDataContainsText(frame.drawData, "Route Target"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user