Add core popup overlay primitive

This commit is contained in:
2026-04-06 22:32:40 +08:00
parent 9568cf0a16
commit 620717f8b4
16 changed files with 1261 additions and 4 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,10 @@
# Core Input | Popup Menu Overlay
这个场景只验证 `Core popup / menu overlay primitive`
- 根 popup 打开与关闭
- submenu 展开与分支关闭
- overlay 空白区点击 dismiss
- popup 打开时阻断底层按钮命中
不验证 Editor 菜单栏,不验证业务命令。

View 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>

View File

@@ -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");
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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 = {};

View File

@@ -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,

View File

@@ -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

View File

@@ -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