chore: checkpoint current workspace changes
This commit is contained in:
@@ -15,6 +15,10 @@
|
||||
#define XCENGINE_CORE_UI_TESTS_REPO_ROOT "."
|
||||
#endif
|
||||
|
||||
#ifndef XCENGINE_CORE_UI_TESTS_BUILD_ROOT
|
||||
#define XCENGINE_CORE_UI_TESTS_BUILD_ROOT "."
|
||||
#endif
|
||||
|
||||
namespace XCEngine::Tests::CoreUI {
|
||||
|
||||
namespace {
|
||||
@@ -53,6 +57,47 @@ std::filesystem::path GetRepoRootPath() {
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path GetBuildRootPath() {
|
||||
std::string root = XCENGINE_CORE_UI_TESTS_BUILD_ROOT;
|
||||
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||
root = root.substr(1u, root.size() - 2u);
|
||||
}
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
bool TryMakeRepoRelativePath(
|
||||
const std::filesystem::path& absolutePath,
|
||||
std::filesystem::path& outRelativePath) {
|
||||
std::error_code errorCode = {};
|
||||
outRelativePath = std::filesystem::relative(
|
||||
absolutePath,
|
||||
GetRepoRootPath(),
|
||||
errorCode);
|
||||
if (errorCode || outRelativePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& part : outRelativePath) {
|
||||
if (part == "..") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveCaptureOutputRoot(
|
||||
const std::filesystem::path& sourceCaptureRoot) {
|
||||
const std::filesystem::path normalizedSourcePath =
|
||||
sourceCaptureRoot.lexically_normal();
|
||||
std::filesystem::path relativePath = {};
|
||||
if (TryMakeRepoRelativePath(normalizedSourcePath, relativePath)) {
|
||||
return (GetBuildRootPath() / relativePath).lexically_normal();
|
||||
}
|
||||
|
||||
return (GetBuildRootPath() / "ui_test_captures" / normalizedSourcePath.filename())
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
std::string TruncateText(const std::string& text, std::size_t maxLength) {
|
||||
if (text.size() <= maxLength) {
|
||||
return text;
|
||||
@@ -250,7 +295,8 @@ bool Application::Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
if (initialScenario == nullptr) {
|
||||
initialScenario = &GetDefaultCoreValidationScenario();
|
||||
}
|
||||
m_autoScreenshot.Initialize(initialScenario->captureRootPath);
|
||||
m_autoScreenshot.Initialize(
|
||||
ResolveCaptureOutputRoot(initialScenario->captureRootPath));
|
||||
LoadStructuredScreen("startup");
|
||||
return true;
|
||||
}
|
||||
@@ -306,6 +352,10 @@ void Application::RenderFrame() {
|
||||
m_activeScenario->id == PopupMenuOverlayValidationScene::ScenarioId) {
|
||||
m_popupMenuOverlayScene.Update(frameEvents, viewportRect, windowFocused);
|
||||
}
|
||||
if (m_activeScenario != nullptr &&
|
||||
m_activeScenario->id == DragDropValidationScene::ScenarioId) {
|
||||
m_dragDropValidationScene.Update(frameEvents, viewportRect, windowFocused);
|
||||
}
|
||||
|
||||
if (m_useStructuredScreen && m_screenPlayer.IsLoaded()) {
|
||||
UIScreenFrameInput input = {};
|
||||
@@ -330,6 +380,10 @@ void Application::RenderFrame() {
|
||||
m_activeScenario->id == PopupMenuOverlayValidationScene::ScenarioId) {
|
||||
m_popupMenuOverlayScene.AppendDrawData(drawData, viewportRect);
|
||||
}
|
||||
if (m_activeScenario != nullptr &&
|
||||
m_activeScenario->id == DragDropValidationScene::ScenarioId) {
|
||||
m_dragDropValidationScene.AppendDrawData(drawData, viewportRect);
|
||||
}
|
||||
|
||||
if (drawData.Empty()) {
|
||||
m_runtimeStatus = "Core UI Validation | Load Error";
|
||||
@@ -447,6 +501,7 @@ bool Application::LoadStructuredScreen(const char* triggerReason) {
|
||||
: (scenarioLoadWarning.empty()
|
||||
? m_screenPlayer.GetLastError()
|
||||
: scenarioLoadWarning + " | " + m_screenPlayer.GetLastError());
|
||||
m_dragDropValidationScene.Reset();
|
||||
m_popupMenuOverlayScene.Reset();
|
||||
RebuildTrackedFileStates();
|
||||
return loaded;
|
||||
@@ -641,7 +696,11 @@ void Application::AppendRuntimeOverlay(UIDrawData& drawData, float width, float
|
||||
} else if (!m_autoScreenshot.GetLastCaptureSummary().empty()) {
|
||||
detailLines.push_back(TruncateText(m_autoScreenshot.GetLastCaptureSummary(), 78u));
|
||||
} else {
|
||||
detailLines.push_back("Screenshots: F12 -> current scenario captures/");
|
||||
detailLines.push_back(
|
||||
"Screenshots: F12 -> " +
|
||||
TruncateText(
|
||||
m_autoScreenshot.GetLatestCapturePath().parent_path().string(),
|
||||
60u));
|
||||
}
|
||||
|
||||
if (!m_runtimeError.empty()) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "AutoScreenshot.h"
|
||||
#include "CoreValidationScenario.h"
|
||||
#include "DragDropValidationScene.h"
|
||||
#include "InputModifierTracker.h"
|
||||
#include "NativeRenderer.h"
|
||||
#include "PopupMenuOverlayValidationScene.h"
|
||||
@@ -73,6 +74,7 @@ private:
|
||||
std::uint64_t m_frameIndex = 0;
|
||||
std::vector<::XCEngine::UI::UIInputEvent> m_pendingInputEvents = {};
|
||||
Host::InputModifierTracker m_inputModifierTracker = {};
|
||||
DragDropValidationScene m_dragDropValidationScene = {};
|
||||
PopupMenuOverlayValidationScene m_popupMenuOverlayScene = {};
|
||||
bool m_trackingMouseLeave = false;
|
||||
bool m_useStructuredScreen = false;
|
||||
|
||||
@@ -4,21 +4,80 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace XCEngine::Tests::CoreUI::Host {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsAutoCaptureOnStartupEnabled() {
|
||||
const char* value = std::getenv("XCUI_AUTO_CAPTURE_ON_STARTUP");
|
||||
if (value == nullptr || value[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string normalized = value;
|
||||
for (char& character : normalized) {
|
||||
character = static_cast<char>(std::tolower(static_cast<unsigned char>(character)));
|
||||
}
|
||||
|
||||
return normalized != "0" &&
|
||||
normalized != "false" &&
|
||||
normalized != "off" &&
|
||||
normalized != "no";
|
||||
}
|
||||
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
const DWORD copied = ::GetModuleFileNameW(
|
||||
nullptr,
|
||||
buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (copied == 0u) {
|
||||
return std::filesystem::current_path().lexically_normal();
|
||||
}
|
||||
|
||||
if (copied < buffer.size() - 1u) {
|
||||
return std::filesystem::path(std::wstring(buffer.data(), copied))
|
||||
.parent_path()
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
buffer.resize(buffer.size() * 2u);
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveBuildCaptureRoot(const std::filesystem::path& requestedCaptureRoot) {
|
||||
std::filesystem::path captureRoot = GetExecutableDirectory() / "captures";
|
||||
const std::filesystem::path scenarioPath = requestedCaptureRoot.parent_path().filename();
|
||||
if (!scenarioPath.empty() && scenarioPath != "captures") {
|
||||
captureRoot /= scenarioPath;
|
||||
}
|
||||
|
||||
return captureRoot.lexically_normal();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AutoScreenshotController::Initialize(const std::filesystem::path& captureRoot) {
|
||||
m_captureRoot = captureRoot.lexically_normal();
|
||||
m_captureRoot = ResolveBuildCaptureRoot(captureRoot);
|
||||
m_historyRoot = (m_captureRoot / "history").lexically_normal();
|
||||
m_latestCapturePath = (m_captureRoot / "latest.png").lexically_normal();
|
||||
m_captureCount = 0;
|
||||
m_capturePending = false;
|
||||
m_pendingReason.clear();
|
||||
m_lastCaptureSummary.clear();
|
||||
m_lastCaptureSummary = "Output: " + m_captureRoot.string();
|
||||
m_lastCaptureError.clear();
|
||||
if (IsAutoCaptureOnStartupEnabled()) {
|
||||
RequestCapture("startup");
|
||||
}
|
||||
}
|
||||
|
||||
void AutoScreenshotController::Shutdown() {
|
||||
|
||||
@@ -24,8 +24,17 @@ fs::path RepoRelative(const char* relativePath) {
|
||||
return (RepoRootPath() / relativePath).lexically_normal();
|
||||
}
|
||||
|
||||
const std::array<CoreValidationScenario, 10>& GetCoreValidationScenarios() {
|
||||
static const std::array<CoreValidationScenario, 10> scenarios = { {
|
||||
const std::array<CoreValidationScenario, 11>& GetCoreValidationScenarios() {
|
||||
static const std::array<CoreValidationScenario, 11> scenarios = { {
|
||||
{
|
||||
"core.input.drag_drop_basic",
|
||||
UIValidationDomain::Core,
|
||||
"input",
|
||||
"Core Input | Drag Drop Contract",
|
||||
RepoRelative("tests/UI/Core/integration/input/drag_drop_basic/View.xcui"),
|
||||
RepoRelative("tests/UI/Core/integration/shared/themes/core_validation.xctheme"),
|
||||
RepoRelative("tests/UI/Core/integration/input/drag_drop_basic/captures")
|
||||
},
|
||||
{
|
||||
"core.input.keyboard_focus",
|
||||
UIValidationDomain::Core,
|
||||
@@ -123,6 +132,12 @@ const std::array<CoreValidationScenario, 10>& GetCoreValidationScenarios() {
|
||||
} // namespace
|
||||
|
||||
const CoreValidationScenario& GetDefaultCoreValidationScenario() {
|
||||
for (const CoreValidationScenario& scenario : GetCoreValidationScenarios()) {
|
||||
if (scenario.id == "core.input.keyboard_focus") {
|
||||
return scenario;
|
||||
}
|
||||
}
|
||||
|
||||
return GetCoreValidationScenarios().front();
|
||||
}
|
||||
|
||||
|
||||
504
tests/UI/Core/integration/shared/src/DragDropValidationScene.cpp
Normal file
504
tests/UI/Core/integration/shared/src/DragDropValidationScene.cpp
Normal file
@@ -0,0 +1,504 @@
|
||||
#include "DragDropValidationScene.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <array>
|
||||
#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::UIPoint;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using ::XCEngine::UI::Widgets::BeginUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::CancelUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::EndUIDragDrop;
|
||||
using ::XCEngine::UI::Widgets::HasResolvedUIDragDropTarget;
|
||||
using ::XCEngine::UI::Widgets::IsUIDragDropInProgress;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropOperation;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropPayload;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropResult;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropSourceDescriptor;
|
||||
using ::XCEngine::UI::Widgets::UIDragDropTargetDescriptor;
|
||||
using ::XCEngine::UI::Widgets::UpdateUIDragDropPointer;
|
||||
using ::XCEngine::UI::Widgets::UpdateUIDragDropTarget;
|
||||
|
||||
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 kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
constexpr UIColor kCardHover(0.24f, 0.24f, 0.24f, 1.0f);
|
||||
constexpr UIColor kCardBorder(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
constexpr UIColor kAccept(0.36f, 0.46f, 0.36f, 1.0f);
|
||||
constexpr UIColor kAcceptBorder(0.56f, 0.72f, 0.56f, 1.0f);
|
||||
constexpr UIColor kReject(0.34f, 0.22f, 0.22f, 1.0f);
|
||||
constexpr UIColor kRejectBorder(0.72f, 0.38f, 0.38f, 1.0f);
|
||||
constexpr UIColor kGhostBg(0.28f, 0.28f, 0.28f, 0.95f);
|
||||
constexpr UIColor kGhostBorder(0.78f, 0.78f, 0.78f, 1.0f);
|
||||
constexpr UIColor kTextPrimary(0.93f, 0.93f, 0.93f, 1.0f);
|
||||
constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f);
|
||||
constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f);
|
||||
constexpr UIColor kTextSuccess(0.62f, 0.82f, 0.62f, 1.0f);
|
||||
constexpr UIColor kTextDanger(0.84f, 0.48f, 0.48f, 1.0f);
|
||||
|
||||
constexpr std::uint64_t kTextureSourceOwnerId = 1001u;
|
||||
constexpr std::uint64_t kEntitySourceOwnerId = 1002u;
|
||||
constexpr std::uint64_t kProjectTargetOwnerId = 2001u;
|
||||
constexpr std::uint64_t kHierarchyTargetOwnerId = 2002u;
|
||||
|
||||
std::string DescribeOperation(UIDragDropOperation operation) {
|
||||
switch (operation) {
|
||||
case UIDragDropOperation::Copy:
|
||||
return "Copy";
|
||||
case UIDragDropOperation::Move:
|
||||
return "Move";
|
||||
case UIDragDropOperation::Link:
|
||||
return "Link";
|
||||
default:
|
||||
return "(none)";
|
||||
}
|
||||
}
|
||||
|
||||
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 DrawCard(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
std::string_view title,
|
||||
std::string_view subtitle,
|
||||
const UIColor& fillColor,
|
||||
const UIColor& borderColor) {
|
||||
DrawPanel(drawList, rect, fillColor, borderColor, 10.0f);
|
||||
drawList.AddText(UIPoint(rect.x + 14.0f, rect.y + 12.0f), std::string(title), kTextPrimary, 15.0f);
|
||||
drawList.AddText(UIPoint(rect.x + 14.0f, rect.y + 34.0f), std::string(subtitle), kTextMuted, 12.0f);
|
||||
}
|
||||
|
||||
UIDragDropSourceDescriptor BuildTextureSource(const UIPoint& pointerPosition) {
|
||||
UIDragDropSourceDescriptor source = {};
|
||||
source.ownerId = kTextureSourceOwnerId;
|
||||
source.sourceId = "project.texture";
|
||||
source.pointerDownPosition = pointerPosition;
|
||||
source.payload = UIDragDropPayload{ "asset.texture", "tex-001", "Checker.png" };
|
||||
source.allowedOperations = UIDragDropOperation::Copy | UIDragDropOperation::Move;
|
||||
source.activationDistance = 6.0f;
|
||||
return source;
|
||||
}
|
||||
|
||||
UIDragDropSourceDescriptor BuildEntitySource(const UIPoint& pointerPosition) {
|
||||
UIDragDropSourceDescriptor source = {};
|
||||
source.ownerId = kEntitySourceOwnerId;
|
||||
source.sourceId = "hierarchy.entity";
|
||||
source.pointerDownPosition = pointerPosition;
|
||||
source.payload = UIDragDropPayload{ "scene.entity", "entity-hero", "HeroRoot" };
|
||||
source.allowedOperations = UIDragDropOperation::Move;
|
||||
source.activationDistance = 6.0f;
|
||||
return source;
|
||||
}
|
||||
|
||||
UIDragDropTargetDescriptor BuildProjectTarget() {
|
||||
static constexpr std::array<std::string_view, 2> kAcceptedTypes = {
|
||||
"asset.texture",
|
||||
"asset.material"
|
||||
};
|
||||
|
||||
UIDragDropTargetDescriptor target = {};
|
||||
target.ownerId = kProjectTargetOwnerId;
|
||||
target.targetId = "project.browser";
|
||||
target.acceptedPayloadTypes = kAcceptedTypes;
|
||||
target.acceptedOperations =
|
||||
UIDragDropOperation::Copy |
|
||||
UIDragDropOperation::Move;
|
||||
target.preferredOperation = UIDragDropOperation::Copy;
|
||||
return target;
|
||||
}
|
||||
|
||||
UIDragDropTargetDescriptor BuildHierarchyTarget() {
|
||||
static constexpr std::array<std::string_view, 1> kAcceptedTypes = {
|
||||
"scene.entity"
|
||||
};
|
||||
|
||||
UIDragDropTargetDescriptor target = {};
|
||||
target.ownerId = kHierarchyTargetOwnerId;
|
||||
target.targetId = "hierarchy.parent";
|
||||
target.acceptedPayloadTypes = kAcceptedTypes;
|
||||
target.acceptedOperations = UIDragDropOperation::Move;
|
||||
target.preferredOperation = UIDragDropOperation::Move;
|
||||
return target;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void DragDropValidationScene::Reset() {
|
||||
m_dragState = {};
|
||||
m_pointerPosition = {};
|
||||
m_hasPointer = false;
|
||||
m_resultText = "Result: Ready";
|
||||
m_lastDropText = "(none)";
|
||||
}
|
||||
|
||||
void DragDropValidationScene::Update(
|
||||
const std::vector<UIInputEvent>& events,
|
||||
const UIRect& viewportRect,
|
||||
bool windowFocused) {
|
||||
const Geometry geometry = BuildGeometry(viewportRect);
|
||||
|
||||
if (!windowFocused &&
|
||||
IsUIDragDropInProgress(m_dragState)) {
|
||||
HandleCancel("Result: focus lost, drag cancelled");
|
||||
}
|
||||
|
||||
for (const UIInputEvent& event : events) {
|
||||
switch (event.type) {
|
||||
case UIInputEventType::PointerMove:
|
||||
m_pointerPosition = event.position;
|
||||
m_hasPointer = true;
|
||||
HandlePointerMove(geometry, event.position);
|
||||
break;
|
||||
case UIInputEventType::PointerLeave:
|
||||
m_hasPointer = false;
|
||||
break;
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
if (event.pointerButton == UIPointerButton::Left) {
|
||||
m_pointerPosition = event.position;
|
||||
m_hasPointer = true;
|
||||
HandlePointerDown(geometry, event.position);
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
if (event.pointerButton == UIPointerButton::Left) {
|
||||
m_pointerPosition = event.position;
|
||||
m_hasPointer = true;
|
||||
HandlePointerUp(geometry, event.position);
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::KeyDown:
|
||||
if (event.keyCode == static_cast<std::int32_t>(KeyCode::Escape)) {
|
||||
HandleCancel("Result: Escape cancelled current drag");
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::FocusLost:
|
||||
HandleCancel("Result: focus lost, drag cancelled");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DragDropValidationScene::AppendDrawData(
|
||||
UIDrawData& drawData,
|
||||
const UIRect& viewportRect) const {
|
||||
const Geometry geometry = BuildGeometry(viewportRect);
|
||||
const bool hoverTexture = m_hasPointer && RectContains(geometry.textureSourceRect, m_pointerPosition);
|
||||
const bool hoverEntity = m_hasPointer && RectContains(geometry.entitySourceRect, m_pointerPosition);
|
||||
const bool hoverProject = m_hasPointer && RectContains(geometry.projectTargetRect, m_pointerPosition);
|
||||
const bool hoverHierarchy = m_hasPointer && RectContains(geometry.hierarchyTargetRect, m_pointerPosition);
|
||||
const bool dragProject =
|
||||
m_dragState.active && m_dragState.targetOwnerId == kProjectTargetOwnerId;
|
||||
const bool dragHierarchy =
|
||||
m_dragState.active && m_dragState.targetOwnerId == kHierarchyTargetOwnerId;
|
||||
const bool rejectProject =
|
||||
m_dragState.active && hoverProject && !dragProject;
|
||||
const bool rejectHierarchy =
|
||||
m_dragState.active && hoverHierarchy && !dragHierarchy;
|
||||
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("Core Drag Drop Primitive Lab");
|
||||
DrawPanel(drawList, geometry.labRect, kLabPanelBg, kLabPanelBorder, 12.0f);
|
||||
DrawPanel(drawList, geometry.statusRect, kStatusBg, kCardBorder, 10.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 14.0f, geometry.statusRect.y + 12.0f),
|
||||
"测试内容:Core Drag / Drop Contract",
|
||||
kTextPrimary,
|
||||
14.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 14.0f, geometry.statusRect.y + 34.0f),
|
||||
m_resultText,
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 14.0f, geometry.statusRect.y + 54.0f),
|
||||
"Payload: " + (m_dragState.payload.label.empty() ? std::string("(none)") : m_dragState.payload.label),
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 280.0f, geometry.statusRect.y + 34.0f),
|
||||
std::string("Armed: ") + (m_dragState.armed ? "true" : "false"),
|
||||
m_dragState.armed ? kTextPrimary : kTextWeak,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 280.0f, geometry.statusRect.y + 54.0f),
|
||||
std::string("Active: ") + (m_dragState.active ? "true" : "false"),
|
||||
m_dragState.active ? kTextSuccess : kTextWeak,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 430.0f, geometry.statusRect.y + 34.0f),
|
||||
"Hover Target: " +
|
||||
(m_dragState.targetId.empty() ? std::string("(none)") : m_dragState.targetId),
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 430.0f, geometry.statusRect.y + 54.0f),
|
||||
"Preview Op: " + DescribeOperation(m_dragState.previewOperation),
|
||||
m_dragState.previewOperation == UIDragDropOperation::None ? kTextWeak : kTextSuccess,
|
||||
12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.statusRect.x + 650.0f, geometry.statusRect.y + 34.0f),
|
||||
"Last Drop: " + m_lastDropText,
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.sourcesRect,
|
||||
"Sources",
|
||||
"按下 source 后先保持,越过阈值才会进入 active。",
|
||||
kCardBg,
|
||||
kCardBorder);
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.targetsRect,
|
||||
"Targets",
|
||||
"右侧同时展示 accept / reject 与预览操作。",
|
||||
kCardBg,
|
||||
kCardBorder);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.textureSourceRect,
|
||||
"Texture Asset",
|
||||
"type=asset.texture | Copy/Move",
|
||||
hoverTexture ? kCardHover : kCardBg,
|
||||
kCardBorder);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.textureSourceRect.x + 14.0f, geometry.textureSourceRect.y + 58.0f),
|
||||
"Checker.png",
|
||||
kTextPrimary,
|
||||
13.0f);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.entitySourceRect,
|
||||
"Scene Entity",
|
||||
"type=scene.entity | Move only",
|
||||
hoverEntity ? kCardHover : kCardBg,
|
||||
kCardBorder);
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.entitySourceRect.x + 14.0f, geometry.entitySourceRect.y + 58.0f),
|
||||
"HeroRoot",
|
||||
kTextPrimary,
|
||||
13.0f);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.projectTargetRect,
|
||||
"Project Browser",
|
||||
"accepts asset.texture / asset.material | preferred Copy",
|
||||
dragProject ? kAccept : (rejectProject ? kReject : (hoverProject ? kCardHover : kCardBg)),
|
||||
dragProject ? kAcceptBorder : (rejectProject ? kRejectBorder : kCardBorder));
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.projectTargetRect.x + 14.0f, geometry.projectTargetRect.y + 58.0f),
|
||||
dragProject ? "Accepting current payload" : "拖入 texture 时应显示 Copy",
|
||||
dragProject ? kTextSuccess : (rejectProject ? kTextDanger : kTextMuted),
|
||||
12.0f);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
geometry.hierarchyTargetRect,
|
||||
"Hierarchy Parent",
|
||||
"accepts scene.entity | preferred Move",
|
||||
dragHierarchy ? kAccept : (rejectHierarchy ? kReject : (hoverHierarchy ? kCardHover : kCardBg)),
|
||||
dragHierarchy ? kAcceptBorder : (rejectHierarchy ? kRejectBorder : kCardBorder));
|
||||
drawList.AddText(
|
||||
UIPoint(geometry.hierarchyTargetRect.x + 14.0f, geometry.hierarchyTargetRect.y + 58.0f),
|
||||
dragHierarchy ? "Accepting current payload" : "拖入 entity 时应显示 Move",
|
||||
dragHierarchy ? kTextSuccess : (rejectHierarchy ? kTextDanger : kTextMuted),
|
||||
12.0f);
|
||||
|
||||
if (m_dragState.active) {
|
||||
const UIRect ghostRect(
|
||||
m_pointerPosition.x + 16.0f,
|
||||
m_pointerPosition.y + 12.0f,
|
||||
188.0f,
|
||||
64.0f);
|
||||
DrawPanel(drawList, ghostRect, kGhostBg, kGhostBorder, 8.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(ghostRect.x + 12.0f, ghostRect.y + 12.0f),
|
||||
m_dragState.payload.label.empty() ? std::string("(payload)") : m_dragState.payload.label,
|
||||
kTextPrimary,
|
||||
14.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(ghostRect.x + 12.0f, ghostRect.y + 34.0f),
|
||||
"type=" + m_dragState.payload.typeId + " | op=" + DescribeOperation(m_dragState.previewOperation),
|
||||
kTextMuted,
|
||||
12.0f);
|
||||
}
|
||||
}
|
||||
|
||||
DragDropValidationScene::Geometry DragDropValidationScene::BuildGeometry(
|
||||
const UIRect& viewportRect) const {
|
||||
Geometry geometry = {};
|
||||
const float availableWidth = (std::max)(720.0f, viewportRect.width - 48.0f);
|
||||
const float availableHeight = (std::max)(360.0f, viewportRect.height - 256.0f);
|
||||
geometry.labRect = UIRect(
|
||||
24.0f,
|
||||
220.0f,
|
||||
(std::min)(980.0f, availableWidth),
|
||||
(std::min)(460.0f, availableHeight));
|
||||
geometry.statusRect = UIRect(
|
||||
geometry.labRect.x + 20.0f,
|
||||
geometry.labRect.y + 18.0f,
|
||||
geometry.labRect.width - 40.0f,
|
||||
84.0f);
|
||||
geometry.sourcesRect = UIRect(
|
||||
geometry.labRect.x + 20.0f,
|
||||
geometry.statusRect.y + geometry.statusRect.height + 18.0f,
|
||||
280.0f,
|
||||
geometry.labRect.height - 140.0f);
|
||||
geometry.targetsRect = UIRect(
|
||||
geometry.sourcesRect.x + geometry.sourcesRect.width + 18.0f,
|
||||
geometry.sourcesRect.y,
|
||||
geometry.labRect.width - 338.0f,
|
||||
geometry.sourcesRect.height);
|
||||
geometry.textureSourceRect = UIRect(
|
||||
geometry.sourcesRect.x + 14.0f,
|
||||
geometry.sourcesRect.y + 54.0f,
|
||||
geometry.sourcesRect.width - 28.0f,
|
||||
96.0f);
|
||||
geometry.entitySourceRect = UIRect(
|
||||
geometry.textureSourceRect.x,
|
||||
geometry.textureSourceRect.y + geometry.textureSourceRect.height + 16.0f,
|
||||
geometry.textureSourceRect.width,
|
||||
96.0f);
|
||||
geometry.projectTargetRect = UIRect(
|
||||
geometry.targetsRect.x + 14.0f,
|
||||
geometry.targetsRect.y + 54.0f,
|
||||
geometry.targetsRect.width - 28.0f,
|
||||
112.0f);
|
||||
geometry.hierarchyTargetRect = UIRect(
|
||||
geometry.projectTargetRect.x,
|
||||
geometry.projectTargetRect.y + geometry.projectTargetRect.height + 18.0f,
|
||||
geometry.projectTargetRect.width,
|
||||
112.0f);
|
||||
return geometry;
|
||||
}
|
||||
|
||||
void DragDropValidationScene::HandlePointerDown(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) {
|
||||
if (RectContains(geometry.textureSourceRect, position)) {
|
||||
BeginUIDragDrop(BuildTextureSource(position), m_dragState);
|
||||
SetResult("Result: armed Texture Asset, move beyond threshold to activate");
|
||||
return;
|
||||
}
|
||||
if (RectContains(geometry.entitySourceRect, position)) {
|
||||
BeginUIDragDrop(BuildEntitySource(position), m_dragState);
|
||||
SetResult("Result: armed Scene Entity, move beyond threshold to activate");
|
||||
}
|
||||
}
|
||||
|
||||
void DragDropValidationScene::HandlePointerMove(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) {
|
||||
if (!IsUIDragDropInProgress(m_dragState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIDragDropResult result = {};
|
||||
UpdateUIDragDropPointer(m_dragState, position, &result);
|
||||
if (result.activated) {
|
||||
SetResult("Result: drag became active after crossing activation distance");
|
||||
}
|
||||
UpdateHoveredTarget(geometry, position);
|
||||
}
|
||||
|
||||
void DragDropValidationScene::HandlePointerUp(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) {
|
||||
if (!IsUIDragDropInProgress(m_dragState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateHoveredTarget(geometry, position);
|
||||
UIDragDropResult result = {};
|
||||
if (!EndUIDragDrop(m_dragState, result)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.completed) {
|
||||
m_lastDropText =
|
||||
result.payloadItemId + " -> " + result.targetId + " (" + DescribeOperation(result.operation) + ")";
|
||||
SetResult("Result: drop completed on " + result.targetId + " with " + DescribeOperation(result.operation));
|
||||
return;
|
||||
}
|
||||
|
||||
SetResult("Result: pointer released without accepted target, drag cancelled");
|
||||
}
|
||||
|
||||
void DragDropValidationScene::HandleCancel(std::string reason) {
|
||||
if (!IsUIDragDropInProgress(m_dragState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIDragDropResult result = {};
|
||||
CancelUIDragDrop(m_dragState, &result);
|
||||
SetResult(std::move(reason));
|
||||
}
|
||||
|
||||
void DragDropValidationScene::UpdateHoveredTarget(
|
||||
const Geometry& geometry,
|
||||
const UIPoint& position) {
|
||||
if (!m_dragState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIDragDropResult result = {};
|
||||
if (RectContains(geometry.projectTargetRect, position)) {
|
||||
const UIDragDropTargetDescriptor projectTarget = BuildProjectTarget();
|
||||
UpdateUIDragDropTarget(m_dragState, &projectTarget, &result);
|
||||
} else if (RectContains(geometry.hierarchyTargetRect, position)) {
|
||||
const UIDragDropTargetDescriptor hierarchyTarget = BuildHierarchyTarget();
|
||||
UpdateUIDragDropTarget(m_dragState, &hierarchyTarget, &result);
|
||||
} else {
|
||||
UpdateUIDragDropTarget(m_dragState, nullptr, &result);
|
||||
}
|
||||
|
||||
if (result.targetChanged) {
|
||||
if (!HasResolvedUIDragDropTarget(m_dragState)) {
|
||||
SetResult("Result: current hover target rejects payload type or operation");
|
||||
} else {
|
||||
SetResult("Result: hover target accepts payload with " + DescribeOperation(m_dragState.previewOperation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DragDropValidationScene::SetResult(std::string text) {
|
||||
m_resultText = std::move(text);
|
||||
}
|
||||
|
||||
bool DragDropValidationScene::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,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Widgets/UIDragDropInteraction.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::Tests::CoreUI {
|
||||
|
||||
class DragDropValidationScene {
|
||||
public:
|
||||
static constexpr const char* ScenarioId = "core.input.drag_drop_basic";
|
||||
|
||||
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 sourcesRect = {};
|
||||
::XCEngine::UI::UIRect targetsRect = {};
|
||||
::XCEngine::UI::UIRect textureSourceRect = {};
|
||||
::XCEngine::UI::UIRect entitySourceRect = {};
|
||||
::XCEngine::UI::UIRect projectTargetRect = {};
|
||||
::XCEngine::UI::UIRect hierarchyTargetRect = {};
|
||||
};
|
||||
|
||||
Geometry BuildGeometry(const ::XCEngine::UI::UIRect& viewportRect) const;
|
||||
void HandlePointerDown(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
void HandlePointerMove(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
void HandlePointerUp(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
void HandleCancel(std::string reason);
|
||||
void UpdateHoveredTarget(
|
||||
const Geometry& geometry,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
void SetResult(std::string text);
|
||||
static bool RectContains(
|
||||
const ::XCEngine::UI::UIRect& rect,
|
||||
const ::XCEngine::UI::UIPoint& position);
|
||||
|
||||
::XCEngine::UI::Widgets::UIDragDropState m_dragState = {};
|
||||
::XCEngine::UI::UIPoint m_pointerPosition = {};
|
||||
bool m_hasPointer = false;
|
||||
std::string m_resultText = "Result: Ready";
|
||||
std::string m_lastDropText = "(none)";
|
||||
};
|
||||
|
||||
} // namespace XCEngine::Tests::CoreUI
|
||||
Reference in New Issue
Block a user