Add core popup overlay primitive
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
add_subdirectory(keyboard_focus)
|
||||
add_subdirectory(popup_menu_overlay)
|
||||
add_subdirectory(pointer_states)
|
||||
add_subdirectory(scroll_view)
|
||||
add_subdirectory(shortcut_scope)
|
||||
@@ -6,6 +7,7 @@ add_subdirectory(shortcut_scope)
|
||||
add_custom_target(core_ui_input_integration_tests
|
||||
DEPENDS
|
||||
core_ui_input_keyboard_focus_validation
|
||||
core_ui_input_popup_menu_overlay_validation
|
||||
core_ui_input_pointer_states_validation
|
||||
core_ui_input_scroll_view_validation
|
||||
core_ui_input_shortcut_scope_validation
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
set(CORE_UI_INPUT_POPUP_MENU_OVERLAY_RESOURCES
|
||||
View.xcui
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/integration/shared/themes/core_validation.xctheme
|
||||
)
|
||||
|
||||
add_executable(core_ui_input_popup_menu_overlay_validation WIN32
|
||||
main.cpp
|
||||
${CORE_UI_INPUT_POPUP_MENU_OVERLAY_RESOURCES}
|
||||
)
|
||||
|
||||
target_include_directories(core_ui_input_popup_menu_overlay_validation PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Core/integration/shared/src
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
target_compile_definitions(core_ui_input_popup_menu_overlay_validation PRIVATE
|
||||
UNICODE
|
||||
_UNICODE
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(core_ui_input_popup_menu_overlay_validation PRIVATE /utf-8 /FS)
|
||||
set_property(TARGET core_ui_input_popup_menu_overlay_validation PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
target_link_libraries(core_ui_input_popup_menu_overlay_validation PRIVATE
|
||||
core_ui_integration_host
|
||||
)
|
||||
|
||||
set_target_properties(core_ui_input_popup_menu_overlay_validation PROPERTIES
|
||||
OUTPUT_NAME "XCUICoreInputPopupMenuOverlayValidation"
|
||||
)
|
||||
|
||||
source_group(TREE "${CMAKE_CURRENT_SOURCE_DIR}" FILES View.xcui)
|
||||
10
tests/UI/Core/integration/input/popup_menu_overlay/README.md
Normal file
10
tests/UI/Core/integration/input/popup_menu_overlay/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Core Input | Popup Menu Overlay
|
||||
|
||||
这个场景只验证 `Core popup / menu overlay primitive`:
|
||||
|
||||
- 根 popup 打开与关闭
|
||||
- submenu 展开与分支关闭
|
||||
- overlay 空白区点击 dismiss
|
||||
- popup 打开时阻断底层按钮命中
|
||||
|
||||
不验证 Editor 菜单栏,不验证业务命令。
|
||||
18
tests/UI/Core/integration/input/popup_menu_overlay/View.xcui
Normal file
18
tests/UI/Core/integration/input/popup_menu_overlay/View.xcui
Normal file
@@ -0,0 +1,18 @@
|
||||
<View
|
||||
name="CoreInputPopupMenuOverlay"
|
||||
theme="../../shared/themes/core_validation.xctheme">
|
||||
<Column padding="24" gap="16">
|
||||
<Card
|
||||
title="测试内容:Core Popup / Menu Overlay Primitive"
|
||||
subtitle="只验证 popup 打开关闭、overlay 遮挡、outside click dismiss 与 hover 展开子菜单"
|
||||
tone="accent"
|
||||
height="160">
|
||||
<Column gap="8">
|
||||
<Text text="1. 点击下方交互区里的 Open Menu,根 popup 应在 overlay 层弹出。" />
|
||||
<Text text="2. hover 到 Open Submenu 后,右侧应直接弹出 child popup;再点根 popup 空白区,应该只关闭 child popup。" />
|
||||
<Text text="3. popup 打开时,Background Button 不应在同一次点击里被命中;先 dismiss,再点它才应生效。" />
|
||||
<Text text="4. 按 Esc 应关闭当前最上层 popup;点击 overlay 空白区应关闭整条 popup 链。" />
|
||||
</Column>
|
||||
</Card>
|
||||
</Column>
|
||||
</View>
|
||||
@@ -0,0 +1,8 @@
|
||||
#include "Application.h"
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||
return XCEngine::Tests::CoreUI::RunCoreUIValidationApp(
|
||||
hInstance,
|
||||
nCmdShow,
|
||||
"core.input.popup_menu_overlay");
|
||||
}
|
||||
@@ -30,6 +30,7 @@ add_library(core_ui_integration_host STATIC
|
||||
src/AutoScreenshot.cpp
|
||||
src/Application.cpp
|
||||
src/NativeRenderer.cpp
|
||||
src/PopupMenuOverlayValidationScene.cpp
|
||||
)
|
||||
|
||||
target_include_directories(core_ui_integration_host
|
||||
|
||||
@@ -300,13 +300,20 @@ void Application::RenderFrame() {
|
||||
m_pendingInputEvents.clear();
|
||||
|
||||
UIDrawData drawData = {};
|
||||
const UIRect viewportRect(0.0f, 0.0f, width, height);
|
||||
const bool windowFocused = GetForegroundWindow() == m_hwnd;
|
||||
if (m_activeScenario != nullptr &&
|
||||
m_activeScenario->id == PopupMenuOverlayValidationScene::ScenarioId) {
|
||||
m_popupMenuOverlayScene.Update(frameEvents, viewportRect, windowFocused);
|
||||
}
|
||||
|
||||
if (m_useStructuredScreen && m_screenPlayer.IsLoaded()) {
|
||||
UIScreenFrameInput input = {};
|
||||
input.viewportRect = UIRect(0.0f, 0.0f, width, height);
|
||||
input.viewportRect = viewportRect;
|
||||
input.events = std::move(frameEvents);
|
||||
input.deltaTimeSeconds = deltaTimeSeconds;
|
||||
input.frameIndex = ++m_frameIndex;
|
||||
input.focused = GetForegroundWindow() == m_hwnd;
|
||||
input.focused = windowFocused;
|
||||
|
||||
const auto& frame = m_screenPlayer.Update(input);
|
||||
for (const auto& drawList : frame.drawData.GetDrawLists()) {
|
||||
@@ -319,6 +326,11 @@ void Application::RenderFrame() {
|
||||
m_runtimeError = frame.errorMessage;
|
||||
}
|
||||
|
||||
if (m_activeScenario != nullptr &&
|
||||
m_activeScenario->id == PopupMenuOverlayValidationScene::ScenarioId) {
|
||||
m_popupMenuOverlayScene.AppendDrawData(drawData, viewportRect);
|
||||
}
|
||||
|
||||
if (drawData.Empty()) {
|
||||
m_runtimeStatus = "Core UI Validation | Load Error";
|
||||
if (m_runtimeError.empty() && !m_screenPlayer.IsLoaded()) {
|
||||
@@ -435,6 +447,7 @@ bool Application::LoadStructuredScreen(const char* triggerReason) {
|
||||
: (scenarioLoadWarning.empty()
|
||||
? m_screenPlayer.GetLastError()
|
||||
: scenarioLoadWarning + " | " + m_screenPlayer.GetLastError());
|
||||
m_popupMenuOverlayScene.Reset();
|
||||
RebuildTrackedFileStates();
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "CoreValidationScenario.h"
|
||||
#include "InputModifierTracker.h"
|
||||
#include "NativeRenderer.h"
|
||||
#include "PopupMenuOverlayValidationScene.h"
|
||||
|
||||
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||
@@ -72,6 +73,7 @@ private:
|
||||
std::uint64_t m_frameIndex = 0;
|
||||
std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {};
|
||||
Host::InputModifierTracker m_inputModifierTracker = {};
|
||||
PopupMenuOverlayValidationScene m_popupMenuOverlayScene = {};
|
||||
bool m_trackingMouseLeave = false;
|
||||
bool m_useStructuredScreen = false;
|
||||
std::string m_runtimeStatus = {};
|
||||
|
||||
@@ -24,8 +24,8 @@ fs::path RepoRelative(const char* relativePath) {
|
||||
return (RepoRootPath() / relativePath).lexically_normal();
|
||||
}
|
||||
|
||||
const std::array<CoreValidationScenario, 9>& GetCoreValidationScenarios() {
|
||||
static const std::array<CoreValidationScenario, 9> scenarios = { {
|
||||
const std::array<CoreValidationScenario, 10>& GetCoreValidationScenarios() {
|
||||
static const std::array<CoreValidationScenario, 10> scenarios = { {
|
||||
{
|
||||
"core.input.keyboard_focus",
|
||||
UIValidationDomain::Core,
|
||||
@@ -35,6 +35,15 @@ const std::array<CoreValidationScenario, 9>& GetCoreValidationScenarios() {
|
||||
RepoRelative("tests/UI/Core/integration/shared/themes/core_validation.xctheme"),
|
||||
RepoRelative("tests/UI/Core/integration/input/keyboard_focus/captures")
|
||||
},
|
||||
{
|
||||
"core.input.popup_menu_overlay",
|
||||
UIValidationDomain::Core,
|
||||
"input",
|
||||
"Core Input | Popup Menu Overlay",
|
||||
RepoRelative("tests/UI/Core/integration/input/popup_menu_overlay/View.xcui"),
|
||||
RepoRelative("tests/UI/Core/integration/shared/themes/core_validation.xctheme"),
|
||||
RepoRelative("tests/UI/Core/integration/input/popup_menu_overlay/captures")
|
||||
},
|
||||
{
|
||||
"core.input.pointer_states",
|
||||
UIValidationDomain::Core,
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
#include "PopupMenuOverlayValidationScene.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::Tests::CoreUI {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::Input::KeyCode;
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawData;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIInputPath;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::UISize;
|
||||
using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect;
|
||||
using ::XCEngine::UI::Widgets::UIPopupDismissReason;
|
||||
using ::XCEngine::UI::Widgets::UIPopupOverlayEntry;
|
||||
using ::XCEngine::UI::Widgets::UIPopupPlacement;
|
||||
|
||||
constexpr UIColor kLabPanelBg(0.12f, 0.12f, 0.12f, 1.0f);
|
||||
constexpr UIColor kLabPanelBorder(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
constexpr UIColor kStatusBg(0.16f, 0.16f, 0.16f, 1.0f);
|
||||
constexpr UIColor kStatusBorder(0.28f, 0.28f, 0.28f, 1.0f);
|
||||
constexpr UIColor kControlBg(0.22f, 0.22f, 0.22f, 1.0f);
|
||||
constexpr UIColor kControlHover(0.31f, 0.31f, 0.31f, 1.0f);
|
||||
constexpr UIColor kControlDisabled(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
constexpr UIColor kPopupBg(0.17f, 0.17f, 0.17f, 1.0f);
|
||||
constexpr UIColor kPopupHover(0.30f, 0.30f, 0.30f, 1.0f);
|
||||
constexpr UIColor kPopupBorder(0.38f, 0.38f, 0.38f, 1.0f);
|
||||
constexpr UIColor kScrim(0.05f, 0.05f, 0.05f, 0.48f);
|
||||
constexpr UIColor kTextPrimary(0.93f, 0.93f, 0.93f, 1.0f);
|
||||
constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f);
|
||||
|
||||
const UIInputPath kTriggerPath = { 10u, 11u };
|
||||
const UIInputPath kBackgroundButtonPath = { 20u, 21u };
|
||||
const UIInputPath kRootSurfacePath = { 30u, 31u };
|
||||
const UIInputPath kRootActionPath = { 30u, 31u, 32u };
|
||||
const UIInputPath kRootSubmenuPath = { 30u, 31u, 33u };
|
||||
const UIInputPath kChildSurfacePath = { 40u, 41u };
|
||||
const UIInputPath kChildActionPath = { 40u, 41u, 42u };
|
||||
|
||||
std::string DescribePath(const UIInputPath& path) {
|
||||
if (path == kTriggerPath) {
|
||||
return "Open Menu";
|
||||
}
|
||||
if (path == kBackgroundButtonPath) {
|
||||
return "Background Button";
|
||||
}
|
||||
if (path == kRootActionPath) {
|
||||
return "Root Action";
|
||||
}
|
||||
if (path == kRootSubmenuPath) {
|
||||
return "Open Submenu";
|
||||
}
|
||||
if (path == kRootSurfacePath) {
|
||||
return "Root Popup Surface";
|
||||
}
|
||||
if (path == kChildActionPath) {
|
||||
return "Leaf Action";
|
||||
}
|
||||
if (path == kChildSurfacePath) {
|
||||
return "Child Popup Surface";
|
||||
}
|
||||
return "Overlay Blank";
|
||||
}
|
||||
|
||||
void DrawPanel(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
const UIColor& fillColor,
|
||||
const UIColor& borderColor,
|
||||
float rounding) {
|
||||
drawList.AddFilledRect(rect, fillColor, rounding);
|
||||
drawList.AddRectOutline(rect, borderColor, 1.0f, rounding);
|
||||
}
|
||||
|
||||
void DrawLabel(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
std::string text,
|
||||
const UIColor& textColor,
|
||||
float fontSize = 13.0f) {
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 12.0f, rect.y + 10.0f),
|
||||
std::move(text),
|
||||
textColor,
|
||||
fontSize);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PopupMenuOverlayValidationScene::Reset() {
|
||||
m_popupModel = {};
|
||||
m_pointerPosition = {};
|
||||
m_hasPointer = false;
|
||||
m_backgroundClickCount = 0;
|
||||
m_resultText = "Result: Ready";
|
||||
}
|
||||
|
||||
void PopupMenuOverlayValidationScene::Update(
|
||||
const std::vector<UIInputEvent>& events,
|
||||
const UIRect& viewportRect,
|
||||
bool windowFocused) {
|
||||
(void)windowFocused;
|
||||
const Geometry geometry = BuildGeometry(viewportRect);
|
||||
|
||||
for (const UIInputEvent& event : events) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove:
|
||||
m_pointerPosition = event.position;
|
||||
m_hasPointer = true;
|
||||
HandlePointerHover(geometry, event.position);
|
||||
break;
|
||||
case UIInputEventType::PointerLeave:
|
||||
m_hasPointer = false;
|
||||
break;
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Left) {
|
||||
m_pointerPosition = event.position;
|
||||
m_hasPointer = true;
|
||||
HandlePointerDown(geometry, event.position);
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::KeyDown:
|
||||
if (event.keyCode == static_cast<std::int32_t>(KeyCode::Escape)) {
|
||||
HandleEscapeKey();
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::FocusLost: {
|
||||
const auto dismissResult = m_popupModel.DismissFromFocusLoss({});
|
||||
if (dismissResult.changed) {
|
||||
SetResult("Result: window focus lost, popup chain closed");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PopupMenuOverlayValidationScene::AppendDrawData(
|
||||
UIDrawData& drawData,
|
||||
const UIRect& viewportRect) const {
|
||||
const Geometry geometry = BuildGeometry(viewportRect);
|
||||
const UIInputPath hoverPath =
|
||||
m_hasPointer ? HitTest(geometry, m_pointerPosition) : UIInputPath();
|
||||
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("Core Popup Menu Overlay Lab");
|
||||
DrawPanel(drawList, geometry.labRect, kLabPanelBg, kLabPanelBorder, 12.0f);
|
||||
DrawPanel(drawList, geometry.backgroundButtonRect, kControlBg, kStatusBorder, 8.0f);
|
||||
DrawLabel(
|
||||
drawList,
|
||||
geometry.backgroundButtonRect,
|
||||
"Background Button",
|
||||
!HasOpenPopups() && hoverPath == kBackgroundButtonPath ? kTextPrimary : kTextMuted);
|
||||
|
||||
if (HasOpenPopups()) {
|
||||
drawList.AddFilledRect(geometry.labRect, kScrim, 12.0f);
|
||||
}
|
||||
|
||||
DrawPanel(drawList, geometry.statusRect, kStatusBg, kStatusBorder, 10.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 12.0f, geometry.statusRect.y + 10.0f),
|
||||
"测试内容:Core Popup / Menu Overlay Primitive",
|
||||
kTextPrimary,
|
||||
14.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 12.0f, geometry.statusRect.y + 32.0f),
|
||||
m_resultText,
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 12.0f, geometry.statusRect.y + 50.0f),
|
||||
"Popup Chain: " + FormatPopupChain(),
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 320.0f, geometry.statusRect.y + 32.0f),
|
||||
"Hover: " + DescribeHoverTarget(geometry),
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 320.0f, geometry.statusRect.y + 50.0f),
|
||||
"Background Hits: " + std::to_string(m_backgroundClickCount),
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
|
||||
const UIColor triggerColor =
|
||||
hoverPath == kTriggerPath ? kControlHover : kControlBg;
|
||||
DrawPanel(drawList, geometry.triggerRect, triggerColor, kStatusBorder, 8.0f);
|
||||
DrawLabel(drawList, geometry.triggerRect, "Open Menu", kTextPrimary);
|
||||
|
||||
if (IsRootOpen()) {
|
||||
DrawPanel(drawList, geometry.rootPopupRect, kPopupBg, kPopupBorder, 10.0f);
|
||||
|
||||
const UIColor rootActionColor =
|
||||
hoverPath == kRootActionPath ? kPopupHover : kPopupBg;
|
||||
DrawPanel(drawList, geometry.rootActionRect, rootActionColor, kStatusBorder, 8.0f);
|
||||
DrawLabel(drawList, geometry.rootActionRect, "Root Action", kTextPrimary);
|
||||
|
||||
const UIColor submenuColor =
|
||||
hoverPath == kRootSubmenuPath || hoverPath == kChildSurfacePath || hoverPath == kChildActionPath
|
||||
? kPopupHover
|
||||
: kPopupBg;
|
||||
DrawPanel(drawList, geometry.rootSubmenuRect, submenuColor, kStatusBorder, 8.0f);
|
||||
DrawLabel(drawList, geometry.rootSubmenuRect, "Open Submenu >", kTextPrimary);
|
||||
}
|
||||
|
||||
if (IsSubmenuOpen()) {
|
||||
DrawPanel(drawList, geometry.childPopupRect, kPopupBg, kPopupBorder, 10.0f);
|
||||
const UIColor childActionColor =
|
||||
hoverPath == kChildActionPath ? kPopupHover : kPopupBg;
|
||||
DrawPanel(drawList, geometry.childActionRect, childActionColor, kStatusBorder, 8.0f);
|
||||
DrawLabel(drawList, geometry.childActionRect, "Leaf Action", kTextPrimary);
|
||||
}
|
||||
}
|
||||
|
||||
PopupMenuOverlayValidationScene::Geometry PopupMenuOverlayValidationScene::BuildGeometry(
|
||||
const UIRect& viewportRect) const {
|
||||
Geometry geometry = {};
|
||||
|
||||
const float availableWidth = (std::max)(280.0f, viewportRect.width - 48.0f);
|
||||
const float availableHeight = (std::max)(280.0f, viewportRect.height - 244.0f);
|
||||
geometry.labRect = UIRect(
|
||||
24.0f,
|
||||
220.0f,
|
||||
(std::min)(760.0f, availableWidth),
|
||||
(std::min)(380.0f, availableHeight));
|
||||
|
||||
geometry.statusRect = UIRect(
|
||||
geometry.labRect.x + 20.0f,
|
||||
geometry.labRect.y + 20.0f,
|
||||
geometry.labRect.width - 40.0f,
|
||||
78.0f);
|
||||
geometry.triggerRect = UIRect(
|
||||
geometry.labRect.x + 20.0f,
|
||||
geometry.statusRect.y + geometry.statusRect.height + 18.0f,
|
||||
126.0f,
|
||||
40.0f);
|
||||
geometry.backgroundButtonRect = UIRect(
|
||||
geometry.labRect.x + geometry.labRect.width - 220.0f,
|
||||
geometry.labRect.y + geometry.labRect.height - 64.0f,
|
||||
180.0f,
|
||||
40.0f);
|
||||
|
||||
const auto rootPlacement = ResolvePopupPlacementRect(
|
||||
geometry.triggerRect,
|
||||
UISize(220.0f, 112.0f),
|
||||
geometry.labRect,
|
||||
UIPopupPlacement::BottomStart);
|
||||
geometry.rootPopupRect = rootPlacement.rect;
|
||||
geometry.rootActionRect = UIRect(
|
||||
geometry.rootPopupRect.x + 10.0f,
|
||||
geometry.rootPopupRect.y + 10.0f,
|
||||
geometry.rootPopupRect.width - 20.0f,
|
||||
40.0f);
|
||||
geometry.rootSubmenuRect = UIRect(
|
||||
geometry.rootPopupRect.x + 10.0f,
|
||||
geometry.rootPopupRect.y + 58.0f,
|
||||
geometry.rootPopupRect.width - 20.0f,
|
||||
40.0f);
|
||||
|
||||
const auto childPlacement = ResolvePopupPlacementRect(
|
||||
geometry.rootSubmenuRect,
|
||||
UISize(180.0f, 60.0f),
|
||||
geometry.labRect,
|
||||
UIPopupPlacement::RightStart);
|
||||
geometry.childPopupRect = childPlacement.rect;
|
||||
geometry.childActionRect = UIRect(
|
||||
geometry.childPopupRect.x + 10.0f,
|
||||
geometry.childPopupRect.y + 10.0f,
|
||||
geometry.childPopupRect.width - 20.0f,
|
||||
40.0f);
|
||||
return geometry;
|
||||
}
|
||||
|
||||
UIInputPath PopupMenuOverlayValidationScene::HitTest(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) const {
|
||||
if (IsSubmenuOpen()) {
|
||||
if (RectContains(geometry.childActionRect, position)) {
|
||||
return kChildActionPath;
|
||||
}
|
||||
if (RectContains(geometry.childPopupRect, position)) {
|
||||
return kChildSurfacePath;
|
||||
}
|
||||
}
|
||||
|
||||
if (IsRootOpen()) {
|
||||
if (RectContains(geometry.rootActionRect, position)) {
|
||||
return kRootActionPath;
|
||||
}
|
||||
if (RectContains(geometry.rootSubmenuRect, position)) {
|
||||
return kRootSubmenuPath;
|
||||
}
|
||||
if (RectContains(geometry.rootPopupRect, position)) {
|
||||
return kRootSurfacePath;
|
||||
}
|
||||
}
|
||||
|
||||
if (RectContains(geometry.triggerRect, position)) {
|
||||
return kTriggerPath;
|
||||
}
|
||||
|
||||
if (!HasOpenPopups() && RectContains(geometry.backgroundButtonRect, position)) {
|
||||
return kBackgroundButtonPath;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void PopupMenuOverlayValidationScene::HandlePointerDown(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) {
|
||||
const UIInputPath hitPath = HitTest(geometry, position);
|
||||
if (hitPath == kTriggerPath) {
|
||||
if (IsRootOpen()) {
|
||||
m_popupModel.CloseAll(UIPopupDismissReason::Programmatic);
|
||||
SetResult("Result: root popup closed");
|
||||
} else {
|
||||
OpenRootPopup(geometry);
|
||||
SetResult("Result: root popup opened");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitPath == kRootActionPath) {
|
||||
m_popupModel.CloseAll(UIPopupDismissReason::Programmatic);
|
||||
SetResult("Result: Root Action dispatched");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitPath == kRootSubmenuPath) {
|
||||
if (!IsSubmenuOpen()) {
|
||||
OpenSubmenuPopup(geometry);
|
||||
SetResult("Result: submenu opened");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitPath == kChildActionPath) {
|
||||
m_popupModel.CloseAll(UIPopupDismissReason::Programmatic);
|
||||
SetResult("Result: Leaf Action dispatched");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto dismissResult = m_popupModel.DismissFromPointerDown(hitPath);
|
||||
if (dismissResult.changed) {
|
||||
if (dismissResult.closedPopupIds.size() == 1u &&
|
||||
dismissResult.closedPopupIds.front() == "submenu") {
|
||||
SetResult("Result: click stayed in root popup, submenu closed");
|
||||
} else {
|
||||
SetResult("Result: click hit overlay blank, popup chain closed");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitPath == kBackgroundButtonPath) {
|
||||
++m_backgroundClickCount;
|
||||
SetResult("Result: Background Button dispatched #" + std::to_string(m_backgroundClickCount));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void PopupMenuOverlayValidationScene::HandlePointerHover(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) {
|
||||
const UIInputPath hitPath = HitTest(geometry, position);
|
||||
if (!IsRootOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitPath == kRootSubmenuPath && !IsSubmenuOpen()) {
|
||||
OpenSubmenuPopup(geometry);
|
||||
SetResult("Result: submenu opened by hover");
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitPath == kRootActionPath && IsSubmenuOpen()) {
|
||||
m_popupModel.ClosePopup("submenu", UIPopupDismissReason::Programmatic);
|
||||
SetResult("Result: submenu collapsed by hover");
|
||||
}
|
||||
}
|
||||
|
||||
void PopupMenuOverlayValidationScene::HandleEscapeKey() {
|
||||
const auto dismissResult = m_popupModel.DismissFromEscape();
|
||||
if (dismissResult.changed) {
|
||||
SetResult("Result: Escape closed topmost popup");
|
||||
}
|
||||
}
|
||||
|
||||
void PopupMenuOverlayValidationScene::OpenRootPopup(const Geometry& geometry) {
|
||||
UIPopupOverlayEntry entry = {};
|
||||
entry.popupId = "root";
|
||||
entry.anchorRect = geometry.triggerRect;
|
||||
entry.anchorPath = kTriggerPath;
|
||||
entry.surfacePath = kRootSurfacePath;
|
||||
entry.placement = UIPopupPlacement::BottomStart;
|
||||
m_popupModel.OpenPopup(std::move(entry));
|
||||
}
|
||||
|
||||
void PopupMenuOverlayValidationScene::OpenSubmenuPopup(const Geometry& geometry) {
|
||||
UIPopupOverlayEntry entry = {};
|
||||
entry.popupId = "submenu";
|
||||
entry.parentPopupId = "root";
|
||||
entry.anchorRect = geometry.rootSubmenuRect;
|
||||
entry.anchorPath = kRootSubmenuPath;
|
||||
entry.surfacePath = kChildSurfacePath;
|
||||
entry.placement = UIPopupPlacement::RightStart;
|
||||
m_popupModel.OpenPopup(std::move(entry));
|
||||
}
|
||||
|
||||
void PopupMenuOverlayValidationScene::SetResult(std::string text) {
|
||||
m_resultText = std::move(text);
|
||||
}
|
||||
|
||||
std::string PopupMenuOverlayValidationScene::FormatPopupChain() const {
|
||||
if (!HasOpenPopups()) {
|
||||
return "(empty)";
|
||||
}
|
||||
|
||||
std::string chain = {};
|
||||
const auto& popups = m_popupModel.GetPopupChain();
|
||||
for (std::size_t index = 0; index < popups.size(); ++index) {
|
||||
if (!chain.empty()) {
|
||||
chain += " > ";
|
||||
}
|
||||
chain += popups[index].popupId;
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
std::string PopupMenuOverlayValidationScene::DescribeHoverTarget(const Geometry& geometry) const {
|
||||
if (!m_hasPointer) {
|
||||
return "(none)";
|
||||
}
|
||||
|
||||
return DescribePath(HitTest(geometry, m_pointerPosition));
|
||||
}
|
||||
|
||||
bool PopupMenuOverlayValidationScene::HasOpenPopups() const {
|
||||
return m_popupModel.HasOpenPopups();
|
||||
}
|
||||
|
||||
bool PopupMenuOverlayValidationScene::IsRootOpen() const {
|
||||
return m_popupModel.FindPopup("root") != nullptr;
|
||||
}
|
||||
|
||||
bool PopupMenuOverlayValidationScene::IsSubmenuOpen() const {
|
||||
return m_popupModel.FindPopup("submenu") != nullptr;
|
||||
}
|
||||
|
||||
bool PopupMenuOverlayValidationScene::RectContains(
|
||||
const UIRect& rect,
|
||||
const UIPoint& position) {
|
||||
return position.x >= rect.x &&
|
||||
position.x <= rect.x + rect.width &&
|
||||
position.y >= rect.y &&
|
||||
position.y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::Tests::CoreUI
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::Tests::CoreUI {
|
||||
|
||||
class PopupMenuOverlayValidationScene {
|
||||
public:
|
||||
static constexpr const char* ScenarioId = "core.input.popup_menu_overlay";
|
||||
|
||||
void Reset();
|
||||
void Update(
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& events,
|
||||
const ::XCEngine::UI::UIRect& viewportRect,
|
||||
bool windowFocused);
|
||||
void AppendDrawData(
|
||||
::XCEngine::UI::UIDrawData& drawData,
|
||||
const ::XCEngine::UI::UIRect& viewportRect) const;
|
||||
|
||||
private:
|
||||
struct Geometry {
|
||||
::XCEngine::UI::UIRect labRect = {};
|
||||
::XCEngine::UI::UIRect statusRect = {};
|
||||
::XCEngine::UI::UIRect triggerRect = {};
|
||||
::XCEngine::UI::UIRect backgroundButtonRect = {};
|
||||
::XCEngine::UI::UIRect rootPopupRect = {};
|
||||
::XCEngine::UI::UIRect rootActionRect = {};
|
||||
::XCEngine::UI::UIRect rootSubmenuRect = {};
|
||||
::XCEngine::UI::UIRect childPopupRect = {};
|
||||
::XCEngine::UI::UIRect childActionRect = {};
|
||||
};
|
||||
|
||||
Geometry BuildGeometry(const ::XCEngine::UI::UIRect& viewportRect) const;
|
||||
::XCEngine::UI::UIInputPath HitTest(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position) const;
|
||||
void HandlePointerDown(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
void HandlePointerHover(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
void HandleEscapeKey();
|
||||
void OpenRootPopup(const Geometry& geometry);
|
||||
void OpenSubmenuPopup(const Geometry& geometry);
|
||||
void SetResult(std::string text);
|
||||
std::string FormatPopupChain() const;
|
||||
std::string DescribeHoverTarget(const Geometry& geometry) const;
|
||||
bool HasOpenPopups() const;
|
||||
bool IsRootOpen() const;
|
||||
bool IsSubmenuOpen() const;
|
||||
|
||||
static bool RectContains(
|
||||
const ::XCEngine::UI::UIRect& rect,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
|
||||
::XCEngine::UI::Widgets::UIPopupOverlayModel m_popupModel = {};
|
||||
::XCEngine::UI::UIPoint m_pointerPosition = {};
|
||||
bool m_hasPointer = false;
|
||||
std::uint32_t m_backgroundClickCount = 0;
|
||||
std::string m_resultText = "Result: Ready";
|
||||
};
|
||||
|
||||
} // namespace XCEngine::Tests::CoreUI
|
||||
Reference in New Issue
Block a user