feat(xcui): close scroll view validation loop
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
add_subdirectory(keyboard_focus)
|
||||
add_subdirectory(pointer_states)
|
||||
add_subdirectory(scroll_view)
|
||||
add_subdirectory(shortcut_scope)
|
||||
|
||||
add_custom_target(editor_ui_input_integration_tests
|
||||
DEPENDS
|
||||
editor_ui_input_keyboard_focus_validation
|
||||
editor_ui_input_pointer_states_validation
|
||||
editor_ui_input_scroll_view_validation
|
||||
editor_ui_input_shortcut_scope_validation
|
||||
)
|
||||
|
||||
35
tests/UI/Editor/integration/input/scroll_view/CMakeLists.txt
Normal file
35
tests/UI/Editor/integration/input/scroll_view/CMakeLists.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
set(EDITOR_UI_INPUT_SCROLL_VIEW_RESOURCES
|
||||
View.xcui
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/themes/editor_validation.xctheme
|
||||
)
|
||||
|
||||
add_executable(editor_ui_input_scroll_view_validation WIN32
|
||||
main.cpp
|
||||
${EDITOR_UI_INPUT_SCROLL_VIEW_RESOURCES}
|
||||
)
|
||||
|
||||
target_include_directories(editor_ui_input_scroll_view_validation PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
target_compile_definitions(editor_ui_input_scroll_view_validation PRIVATE
|
||||
UNICODE
|
||||
_UNICODE
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(editor_ui_input_scroll_view_validation PRIVATE /utf-8 /FS)
|
||||
set_property(TARGET editor_ui_input_scroll_view_validation PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
target_link_libraries(editor_ui_input_scroll_view_validation PRIVATE
|
||||
editor_ui_integration_host
|
||||
)
|
||||
|
||||
set_target_properties(editor_ui_input_scroll_view_validation PROPERTIES
|
||||
OUTPUT_NAME "XCUIEditorInputScrollViewValidation"
|
||||
)
|
||||
|
||||
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES View.xcui)
|
||||
61
tests/UI/Editor/integration/input/scroll_view/View.xcui
Normal file
61
tests/UI/Editor/integration/input/scroll_view/View.xcui
Normal file
@@ -0,0 +1,61 @@
|
||||
<View
|
||||
name="EditorInputScrollView"
|
||||
theme="../../shared/themes/editor_validation.xctheme">
|
||||
<Column padding="20" gap="12">
|
||||
<Card
|
||||
title="功能:ScrollView 滚动 / clip / overflow"
|
||||
subtitle="只检查滚轮滚动、裁剪、overflow 与 target 路由,不检查业务面板"
|
||||
tone="accent"
|
||||
height="118">
|
||||
<Column gap="6">
|
||||
<Text text="1. 把鼠标放到下方日志区内滚动滚轮:内容应上下移动,右下角 Scroll target 应落到 validation-scroll。" />
|
||||
<Text text="2. 连续向下滚到末尾再继续滚:Offset 应被 clamp,Result 应显示 Scroll delta clamped to current offset。" />
|
||||
<Text text="3. 把鼠标移到日志区外再滚动:日志位置不应变化,Result 应显示 No hovered ScrollView。" />
|
||||
<Text text="4. 这个场景只验证 ScrollView 基础能力,不验证 editor 业务面板。" />
|
||||
</Column>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Scrollable Log"
|
||||
subtitle="wheel inside this viewport"
|
||||
height="fill">
|
||||
<ScrollView id="validation-scroll" height="fill">
|
||||
<Column gap="8">
|
||||
<Text text="Line 01 - Scroll validation log" />
|
||||
<Text text="Line 02 - Scroll validation log" />
|
||||
<Text text="Line 03 - Scroll validation log" />
|
||||
<Text text="Line 04 - Scroll validation log" />
|
||||
<Text text="Line 05 - Scroll validation log" />
|
||||
<Text text="Line 06 - Scroll validation log" />
|
||||
<Text text="Line 07 - Scroll validation log" />
|
||||
<Text text="Line 08 - Scroll validation log" />
|
||||
<Text text="Line 09 - Scroll validation log" />
|
||||
<Text text="Line 10 - Scroll validation log" />
|
||||
<Text text="Line 11 - Scroll validation log" />
|
||||
<Text text="Line 12 - Scroll validation log" />
|
||||
<Text text="Line 13 - Scroll validation log" />
|
||||
<Text text="Line 14 - Scroll validation log" />
|
||||
<Text text="Line 15 - Scroll validation log" />
|
||||
<Text text="Line 16 - Scroll validation log" />
|
||||
<Text text="Line 17 - Scroll validation log" />
|
||||
<Text text="Line 18 - Scroll validation log" />
|
||||
<Text text="Line 19 - Scroll validation log" />
|
||||
<Text text="Line 20 - Scroll validation log" />
|
||||
<Text text="Line 21 - Scroll validation log" />
|
||||
<Text text="Line 22 - Scroll validation log" />
|
||||
<Text text="Line 23 - Scroll validation log" />
|
||||
<Text text="Line 24 - Scroll validation log" />
|
||||
</Column>
|
||||
</ScrollView>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Outside Area"
|
||||
subtitle="wheel here should not move the log"
|
||||
height="84">
|
||||
<Column gap="8">
|
||||
<Text text="把鼠标移到这个区域再滚动,用来检查 No hovered ScrollView。" />
|
||||
</Column>
|
||||
</Card>
|
||||
</Column>
|
||||
</View>
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
8
tests/UI/Editor/integration/input/scroll_view/main.cpp
Normal file
8
tests/UI/Editor/integration/input/scroll_view/main.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "Application.h"
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||
return XCEngine::Tests::EditorUI::RunEditorUIValidationApp(
|
||||
hInstance,
|
||||
nCmdShow,
|
||||
"editor.input.scroll_view");
|
||||
}
|
||||
@@ -530,6 +530,7 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
|
||||
|
||||
if (authoredMode) {
|
||||
const auto& inputDebug = m_documentHost.GetInputDebugSnapshot();
|
||||
const auto& scrollDebug = m_documentHost.GetScrollDebugSnapshot();
|
||||
detailLines.push_back(
|
||||
"Hover | Focus: " +
|
||||
ExtractStateKeyTail(inputDebug.hoveredStateKey) +
|
||||
@@ -601,6 +602,25 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
|
||||
"Last event 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()) {
|
||||
|
||||
@@ -24,8 +24,8 @@ fs::path RepoRelative(const char* relativePath) {
|
||||
return (RepoRootPath() / relativePath).lexically_normal();
|
||||
}
|
||||
|
||||
const std::array<EditorValidationScenario, 6>& GetEditorValidationScenarios() {
|
||||
static const std::array<EditorValidationScenario, 6> scenarios = { {
|
||||
const std::array<EditorValidationScenario, 7>& GetEditorValidationScenarios() {
|
||||
static const std::array<EditorValidationScenario, 7> scenarios = { {
|
||||
{
|
||||
"editor.input.keyboard_focus",
|
||||
UIValidationDomain::Editor,
|
||||
@@ -44,6 +44,15 @@ const std::array<EditorValidationScenario, 6>& GetEditorValidationScenarios() {
|
||||
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,
|
||||
|
||||
@@ -15,6 +15,7 @@ using XCEngine::Tests::EditorUI::UIValidationDomain;
|
||||
TEST(EditorValidationRegistryTest, KnownEditorValidationScenariosResolveToExistingResources) {
|
||||
const auto* pointerScenario = FindEditorValidationScenario("editor.input.pointer_states");
|
||||
const auto* keyboardScenario = FindEditorValidationScenario("editor.input.keyboard_focus");
|
||||
const auto* scrollScenario = FindEditorValidationScenario("editor.input.scroll_view");
|
||||
const auto* shortcutScenario = FindEditorValidationScenario("editor.input.shortcut_scope");
|
||||
const auto* splitterScenario = FindEditorValidationScenario("editor.layout.splitter_resize");
|
||||
const auto* tabStripScenario = FindEditorValidationScenario("editor.layout.tab_strip_selection");
|
||||
@@ -22,18 +23,21 @@ TEST(EditorValidationRegistryTest, KnownEditorValidationScenariosResolveToExisti
|
||||
|
||||
ASSERT_NE(pointerScenario, nullptr);
|
||||
ASSERT_NE(keyboardScenario, nullptr);
|
||||
ASSERT_NE(scrollScenario, nullptr);
|
||||
ASSERT_NE(shortcutScenario, nullptr);
|
||||
ASSERT_NE(splitterScenario, nullptr);
|
||||
ASSERT_NE(tabStripScenario, nullptr);
|
||||
ASSERT_NE(workspaceScenario, nullptr);
|
||||
EXPECT_EQ(pointerScenario->domain, UIValidationDomain::Editor);
|
||||
EXPECT_EQ(keyboardScenario->domain, UIValidationDomain::Editor);
|
||||
EXPECT_EQ(scrollScenario->domain, UIValidationDomain::Editor);
|
||||
EXPECT_EQ(shortcutScenario->domain, UIValidationDomain::Editor);
|
||||
EXPECT_EQ(splitterScenario->domain, UIValidationDomain::Editor);
|
||||
EXPECT_EQ(tabStripScenario->domain, UIValidationDomain::Editor);
|
||||
EXPECT_EQ(workspaceScenario->domain, UIValidationDomain::Editor);
|
||||
EXPECT_EQ(pointerScenario->categoryId, "input");
|
||||
EXPECT_EQ(keyboardScenario->categoryId, "input");
|
||||
EXPECT_EQ(scrollScenario->categoryId, "input");
|
||||
EXPECT_EQ(shortcutScenario->categoryId, "input");
|
||||
EXPECT_EQ(splitterScenario->categoryId, "layout");
|
||||
EXPECT_EQ(tabStripScenario->categoryId, "layout");
|
||||
@@ -42,6 +46,8 @@ TEST(EditorValidationRegistryTest, KnownEditorValidationScenariosResolveToExisti
|
||||
EXPECT_TRUE(std::filesystem::exists(pointerScenario->themePath));
|
||||
EXPECT_TRUE(std::filesystem::exists(keyboardScenario->documentPath));
|
||||
EXPECT_TRUE(std::filesystem::exists(keyboardScenario->themePath));
|
||||
EXPECT_TRUE(std::filesystem::exists(scrollScenario->documentPath));
|
||||
EXPECT_TRUE(std::filesystem::exists(scrollScenario->themePath));
|
||||
EXPECT_TRUE(std::filesystem::exists(shortcutScenario->documentPath));
|
||||
EXPECT_TRUE(std::filesystem::exists(shortcutScenario->themePath));
|
||||
EXPECT_TRUE(std::filesystem::exists(splitterScenario->documentPath));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
set(RUNTIME_UI_TEST_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Runtime/unit/test_ui_runtime_scroll_view.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Runtime/unit/test_ui_runtime_shortcut_scope.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Runtime/unit/test_ui_runtime_splitter_validation.cpp
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Runtime/unit/test_ui_runtime_tab_strip.cpp
|
||||
|
||||
331
tests/UI/Runtime/unit/test_ui_runtime_scroll_view.cpp
Normal file
331
tests/UI/Runtime/unit/test_ui_runtime_scroll_view.cpp
Normal file
@@ -0,0 +1,331 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommand;
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawData;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Runtime::UIScreenAsset;
|
||||
using XCEngine::UI::Runtime::UIScreenFrameInput;
|
||||
using XCEngine::UI::Runtime::UIScreenPlayer;
|
||||
using XCEngine::UI::Runtime::UIDocumentScreenHost;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
class TempFileScope {
|
||||
public:
|
||||
TempFileScope(std::string stem, std::string extension, std::string contents) {
|
||||
const auto uniqueId = std::to_string(
|
||||
std::chrono::steady_clock::now().time_since_epoch().count());
|
||||
m_path = fs::temp_directory_path() / (std::move(stem) + "_" + uniqueId + std::move(extension));
|
||||
std::ofstream output(m_path, std::ios::binary | std::ios::trunc);
|
||||
output << contents;
|
||||
}
|
||||
|
||||
~TempFileScope() {
|
||||
std::error_code errorCode = {};
|
||||
fs::remove(m_path, errorCode);
|
||||
}
|
||||
|
||||
const fs::path& Path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
private:
|
||||
fs::path m_path = {};
|
||||
};
|
||||
|
||||
UIScreenAsset BuildScreenAsset(const fs::path& viewPath, const char* screenId) {
|
||||
UIScreenAsset screen = {};
|
||||
screen.screenId = screenId;
|
||||
screen.documentPath = viewPath.string();
|
||||
return screen;
|
||||
}
|
||||
|
||||
UIScreenFrameInput BuildInputState(std::uint64_t frameIndex = 1u) {
|
||||
UIScreenFrameInput input = {};
|
||||
input.viewportRect = UIRect(0.0f, 0.0f, 520.0f, 340.0f);
|
||||
input.frameIndex = frameIndex;
|
||||
input.focused = true;
|
||||
return input;
|
||||
}
|
||||
|
||||
const UIDrawCommand* FindTextCommand(
|
||||
const UIDrawData& drawData,
|
||||
const std::string& text) {
|
||||
for (const UIDrawList& drawList : drawData.GetDrawLists()) {
|
||||
for (const UIDrawCommand& command : drawList.GetCommands()) {
|
||||
if (command.type == UIDrawCommandType::Text &&
|
||||
command.text == text) {
|
||||
return &command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::size_t CountCommandsOfType(
|
||||
const UIDrawData& drawData,
|
||||
UIDrawCommandType type) {
|
||||
std::size_t count = 0u;
|
||||
for (const UIDrawList& drawList : drawData.GetDrawLists()) {
|
||||
for (const UIDrawCommand& command : drawList.GetCommands()) {
|
||||
if (command.type == type) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool RectContainsPoint(const UIRect& rect, const UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
point.x <= rect.x + rect.width &&
|
||||
point.y >= rect.y &&
|
||||
point.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
bool TryFindSmallestFilledRectContainingPoint(
|
||||
const UIDrawData& drawData,
|
||||
const UIPoint& point,
|
||||
UIRect& outRect) {
|
||||
bool found = false;
|
||||
float bestArea = (std::numeric_limits<float>::max)();
|
||||
|
||||
for (const UIDrawList& drawList : drawData.GetDrawLists()) {
|
||||
for (const UIDrawCommand& command : drawList.GetCommands()) {
|
||||
if (command.type != UIDrawCommandType::FilledRect ||
|
||||
!RectContainsPoint(command.rect, point)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float area = command.rect.width * command.rect.height;
|
||||
if (!found || area < bestArea) {
|
||||
outRect = command.rect;
|
||||
bestArea = area;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
bool TryFindFilledRectForText(
|
||||
const UIDrawData& drawData,
|
||||
const std::string& text,
|
||||
UIRect& outRect) {
|
||||
const UIDrawCommand* textCommand = FindTextCommand(drawData, text);
|
||||
return textCommand != nullptr &&
|
||||
TryFindSmallestFilledRectContainingPoint(drawData, textCommand->position, outRect);
|
||||
}
|
||||
|
||||
UIPoint GetRectCenter(const UIRect& rect) {
|
||||
return UIPoint(rect.x + rect.width * 0.5f, rect.y + rect.height * 0.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIRuntimeScrollViewTest, ScrollViewClipsOverflowingChildrenAndPersistsWheelOffset) {
|
||||
TempFileScope viewFile(
|
||||
"xcui_runtime_scroll_view",
|
||||
".xcui",
|
||||
"<View name=\"Scroll View Test\">\n"
|
||||
" <Column padding=\"18\" gap=\"10\">\n"
|
||||
" <Card title=\"Console\" subtitle=\"Scroll smoke\" height=\"200\">\n"
|
||||
" <ScrollView id=\"log-scroll\" height=\"fill\">\n"
|
||||
" <Column gap=\"8\">\n"
|
||||
" <Text text=\"Line 01\" />\n"
|
||||
" <Text text=\"Line 02\" />\n"
|
||||
" <Text text=\"Line 03\" />\n"
|
||||
" <Text text=\"Line 04\" />\n"
|
||||
" <Text text=\"Line 05\" />\n"
|
||||
" <Text text=\"Line 06\" />\n"
|
||||
" <Text text=\"Line 07\" />\n"
|
||||
" <Text text=\"Line 08\" />\n"
|
||||
" <Text text=\"Line 09\" />\n"
|
||||
" <Text text=\"Line 10\" />\n"
|
||||
" <Text text=\"Line 11\" />\n"
|
||||
" <Text text=\"Line 12\" />\n"
|
||||
" </Column>\n"
|
||||
" </ScrollView>\n"
|
||||
" </Card>\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
|
||||
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.scroll.view")));
|
||||
|
||||
UIScreenFrameInput firstInput = BuildInputState(1u);
|
||||
const auto& firstFrame = player.Update(firstInput);
|
||||
const auto* line01Before = FindTextCommand(firstFrame.drawData, "Line 01");
|
||||
ASSERT_NE(line01Before, nullptr);
|
||||
EXPECT_GT(CountCommandsOfType(firstFrame.drawData, UIDrawCommandType::PushClipRect), 0u);
|
||||
EXPECT_GT(CountCommandsOfType(firstFrame.drawData, UIDrawCommandType::PopClipRect), 0u);
|
||||
|
||||
UIScreenFrameInput secondInput = BuildInputState(2u);
|
||||
UIInputEvent wheelEvent = {};
|
||||
wheelEvent.type = UIInputEventType::PointerWheel;
|
||||
wheelEvent.position = UIPoint(90.0f, 130.0f);
|
||||
wheelEvent.wheelDelta = -120.0f;
|
||||
secondInput.events.push_back(wheelEvent);
|
||||
|
||||
const auto& secondFrame = player.Update(secondInput);
|
||||
const auto* line01After = FindTextCommand(secondFrame.drawData, "Line 01");
|
||||
ASSERT_NE(line01After, nullptr);
|
||||
EXPECT_LT(line01After->position.y, line01Before->position.y);
|
||||
|
||||
const auto& scrollDebug = host.GetScrollDebugSnapshot();
|
||||
EXPECT_NE(scrollDebug.lastTargetStateKey.find("/log-scroll"), std::string::npos);
|
||||
EXPECT_EQ(scrollDebug.lastResult, "Handled");
|
||||
EXPECT_EQ(scrollDebug.handledWheelEventCount, 1u);
|
||||
EXPECT_LT(scrollDebug.lastOffsetBefore, scrollDebug.lastOffsetAfter);
|
||||
|
||||
UIScreenFrameInput thirdInput = BuildInputState(3u);
|
||||
const auto& thirdFrame = player.Update(thirdInput);
|
||||
const auto* line01Persisted = FindTextCommand(thirdFrame.drawData, "Line 01");
|
||||
ASSERT_NE(line01Persisted, nullptr);
|
||||
EXPECT_FLOAT_EQ(line01Persisted->position.y, line01After->position.y);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeScrollViewTest, WheelOutsideScrollViewportDoesNotChangeContentOffset) {
|
||||
TempFileScope viewFile(
|
||||
"xcui_runtime_scroll_view_outside",
|
||||
".xcui",
|
||||
"<View name=\"Scroll Outside Test\">\n"
|
||||
" <Column padding=\"18\" gap=\"10\">\n"
|
||||
" <Card title=\"Console\" subtitle=\"Outside wheel should do nothing\" height=\"200\">\n"
|
||||
" <ScrollView id=\"log-scroll\" height=\"fill\">\n"
|
||||
" <Column gap=\"8\">\n"
|
||||
" <Text text=\"Line 01\" />\n"
|
||||
" <Text text=\"Line 02\" />\n"
|
||||
" <Text text=\"Line 03\" />\n"
|
||||
" <Text text=\"Line 04\" />\n"
|
||||
" <Text text=\"Line 05\" />\n"
|
||||
" <Text text=\"Line 06\" />\n"
|
||||
" <Text text=\"Line 07\" />\n"
|
||||
" <Text text=\"Line 08\" />\n"
|
||||
" <Text text=\"Line 09\" />\n"
|
||||
" <Text text=\"Line 10\" />\n"
|
||||
" <Text text=\"Line 11\" />\n"
|
||||
" <Text text=\"Line 12\" />\n"
|
||||
" </Column>\n"
|
||||
" </ScrollView>\n"
|
||||
" </Card>\n"
|
||||
" <Card title=\"Outside Area\" subtitle=\"not scrollable\" height=\"72\" />\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
|
||||
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.scroll.outside")));
|
||||
|
||||
UIScreenFrameInput firstInput = BuildInputState(1u);
|
||||
const auto& firstFrame = player.Update(firstInput);
|
||||
const auto* line01Before = FindTextCommand(firstFrame.drawData, "Line 01");
|
||||
ASSERT_NE(line01Before, nullptr);
|
||||
|
||||
UIScreenFrameInput secondInput = BuildInputState(2u);
|
||||
UIInputEvent wheelEvent = {};
|
||||
wheelEvent.type = UIInputEventType::PointerWheel;
|
||||
wheelEvent.position = UIPoint(110.0f, 300.0f);
|
||||
wheelEvent.wheelDelta = -120.0f;
|
||||
secondInput.events.push_back(wheelEvent);
|
||||
|
||||
const auto& secondFrame = player.Update(secondInput);
|
||||
const auto* line01After = FindTextCommand(secondFrame.drawData, "Line 01");
|
||||
ASSERT_NE(line01After, nullptr);
|
||||
EXPECT_FLOAT_EQ(line01After->position.y, line01Before->position.y);
|
||||
|
||||
const auto& scrollDebug = host.GetScrollDebugSnapshot();
|
||||
EXPECT_EQ(scrollDebug.lastResult, "No hovered ScrollView");
|
||||
EXPECT_EQ(scrollDebug.handledWheelEventCount, 0u);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeScrollViewTest, NestedScrollViewRoutesWheelToDeepestHoveredTarget) {
|
||||
TempFileScope viewFile(
|
||||
"xcui_runtime_nested_scroll_view",
|
||||
".xcui",
|
||||
"<View name=\"Nested Scroll View Test\">\n"
|
||||
" <Column padding=\"18\" gap=\"10\">\n"
|
||||
" <ScrollView id=\"outer-scroll\" height=\"260\">\n"
|
||||
" <Column gap=\"8\">\n"
|
||||
" <Text text=\"Outer 01\" />\n"
|
||||
" <Text text=\"Outer 02\" />\n"
|
||||
" <Text text=\"Outer 03\" />\n"
|
||||
" <Card title=\"Nested Panel\" subtitle=\"inner scroll host\" height=\"180\">\n"
|
||||
" <ScrollView id=\"inner-scroll\" height=\"fill\">\n"
|
||||
" <Column gap=\"8\">\n"
|
||||
" <Text text=\"Inner 01\" />\n"
|
||||
" <Text text=\"Inner 02\" />\n"
|
||||
" <Text text=\"Inner 03\" />\n"
|
||||
" <Text text=\"Inner 04\" />\n"
|
||||
" <Text text=\"Inner 05\" />\n"
|
||||
" <Text text=\"Inner 06\" />\n"
|
||||
" <Text text=\"Inner 07\" />\n"
|
||||
" <Text text=\"Inner 08\" />\n"
|
||||
" <Text text=\"Inner 09\" />\n"
|
||||
" <Text text=\"Inner 10\" />\n"
|
||||
" <Text text=\"Inner 11\" />\n"
|
||||
" <Text text=\"Inner 12\" />\n"
|
||||
" </Column>\n"
|
||||
" </ScrollView>\n"
|
||||
" </Card>\n"
|
||||
" <Text text=\"Outer 04\" />\n"
|
||||
" <Text text=\"Outer 05\" />\n"
|
||||
" <Text text=\"Outer 06\" />\n"
|
||||
" <Text text=\"Outer 07\" />\n"
|
||||
" <Text text=\"Outer 08\" />\n"
|
||||
" <Text text=\"Outer 09\" />\n"
|
||||
" </Column>\n"
|
||||
" </ScrollView>\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
|
||||
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.scroll.nested")));
|
||||
|
||||
UIScreenFrameInput firstInput = BuildInputState(1u);
|
||||
const auto& firstFrame = player.Update(firstInput);
|
||||
const auto* innerBefore = FindTextCommand(firstFrame.drawData, "Inner 01");
|
||||
ASSERT_NE(innerBefore, nullptr);
|
||||
|
||||
UIRect innerRect = {};
|
||||
ASSERT_TRUE(TryFindFilledRectForText(firstFrame.drawData, "Inner 01", innerRect));
|
||||
const UIPoint wheelPoint = GetRectCenter(innerRect);
|
||||
|
||||
UIScreenFrameInput secondInput = BuildInputState(2u);
|
||||
UIInputEvent wheelEvent = {};
|
||||
wheelEvent.type = UIInputEventType::PointerWheel;
|
||||
wheelEvent.position = wheelPoint;
|
||||
wheelEvent.wheelDelta = -120.0f;
|
||||
secondInput.events.push_back(wheelEvent);
|
||||
|
||||
const auto& secondFrame = player.Update(secondInput);
|
||||
const auto* innerAfter = FindTextCommand(secondFrame.drawData, "Inner 01");
|
||||
ASSERT_NE(innerAfter, nullptr);
|
||||
EXPECT_LT(innerAfter->position.y, innerBefore->position.y);
|
||||
|
||||
const auto& scrollDebug = host.GetScrollDebugSnapshot();
|
||||
EXPECT_NE(scrollDebug.lastTargetStateKey.find("/inner-scroll"), std::string::npos);
|
||||
EXPECT_NE(scrollDebug.primaryTargetStateKey.find("/outer-scroll"), std::string::npos);
|
||||
EXPECT_EQ(scrollDebug.lastResult, "Handled");
|
||||
EXPECT_GT(scrollDebug.lastOverflow, 0.0f);
|
||||
}
|
||||
@@ -119,6 +119,7 @@ Runtime 的集成测试结构与 Editor 保持同一规范,但宿主职责必
|
||||
|
||||
- `editor.input.keyboard_focus`
|
||||
- `editor.input.pointer_states`
|
||||
- `editor.input.scroll_view`
|
||||
- `editor.input.shortcut_scope`
|
||||
- `editor.layout.splitter_resize`
|
||||
- `editor.layout.tab_strip_selection`
|
||||
|
||||
Reference in New Issue
Block a user