Refactor XCUI editor module layout

This commit is contained in:
2026-04-10 00:41:28 +08:00
parent 4b47764f26
commit 02a0e626fe
263 changed files with 12396 additions and 7592 deletions

View File

@@ -25,6 +25,12 @@ endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/number_field_basic/CMakeLists.txt")
add_subdirectory(number_field_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/asset_field_basic/CMakeLists.txt")
add_subdirectory(asset_field_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/object_field_basic/CMakeLists.txt")
add_subdirectory(object_field_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/text_field_basic/CMakeLists.txt")
add_subdirectory(text_field_basic)
endif()

View File

@@ -0,0 +1,31 @@
add_executable(editor_ui_asset_field_basic_validation WIN32
main.cpp
)
target_include_directories(editor_ui_asset_field_basic_validation PRIVATE
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
${CMAKE_SOURCE_DIR}/new_editor/include
${CMAKE_SOURCE_DIR}/new_editor/app
${CMAKE_SOURCE_DIR}/engine/include
)
target_compile_definitions(editor_ui_asset_field_basic_validation PRIVATE
UNICODE
_UNICODE
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
)
if(MSVC)
target_compile_options(editor_ui_asset_field_basic_validation PRIVATE /utf-8 /FS)
set_property(TARGET editor_ui_asset_field_basic_validation PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
target_link_libraries(editor_ui_asset_field_basic_validation PRIVATE
XCUIEditorLib
XCUIEditorHost
)
set_target_properties(editor_ui_asset_field_basic_validation PROPERTIES
OUTPUT_NAME "XCUIEditorAssetFieldBasicValidation"
)

View File

@@ -0,0 +1,736 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorAssetField.h>
#include <XCEditor/Fields/UIEditorAssetFieldInteraction.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <iterator>
#include <string>
#include <string_view>
#include <vector>
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
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::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::UIEditorAssetFieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorAssetFieldInteractionResult;
using XCEngine::UI::Editor::UIEditorAssetFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorAssetFieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorAssetField;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorAssetField;
using XCEngine::UI::Editor::Widgets::UIEditorAssetFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorAssetFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorAssetFieldPalette;
using XCEngine::UI::Editor::Widgets::UIEditorAssetFieldSpec;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorAssetFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | AssetField Basic";
enum class ActionId : unsigned char {
None = 0,
Reset,
Capture
};
struct ButtonLayout {
ActionId action = ActionId::None;
const char* label = "";
UIRect rect = {};
};
struct ScenarioLayout {
UIRect introRect = {};
UIRect controlRect = {};
UIRect stateRect = {};
UIRect previewRect = {};
UIRect fieldRect = {};
std::vector<ButtonLayout> buttons = {};
};
struct SampleAsset {
const char* assetId = "";
const char* displayName = "";
const char* statusText = "";
UIColor tint = {};
};
constexpr SampleAsset kSampleAssets[] = {
{
"assets/textures/crate_albedo",
"Crate_Albedo",
"Ready",
UIColor(0.30f, 0.55f, 0.84f, 1.0f)
},
{
"assets/materials/sci_fi_panel",
"SciFi_Panel",
"Dirty",
UIColor(0.77f, 0.56f, 0.28f, 1.0f)
}
};
std::filesystem::path ResolveRepoRootPath() {
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
root = root.substr(1u, root.size() - 2u);
}
return std::filesystem::path(root).lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
y >= rect.y &&
y <= rect.y + rect.height;
}
std::int32_t MapAssetFieldKey(UINT keyCode) {
switch (keyCode) {
case VK_SPACE:
return static_cast<std::int32_t>(KeyCode::Space);
case VK_RETURN:
return static_cast<std::int32_t>(KeyCode::Enter);
case VK_DELETE:
return static_cast<std::int32_t>(KeyCode::Delete);
case VK_BACK:
return static_cast<std::int32_t>(KeyCode::Backspace);
default:
return static_cast<std::int32_t>(KeyCode::None);
}
}
ScenarioLayout BuildScenarioLayout(
float width,
float height,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
const float margin = shellMetrics.margin;
constexpr float leftWidth = 470.0f;
const float gap = shellMetrics.gap;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 260.0f);
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
layout.stateRect = UIRect(
margin,
layout.controlRect.y + layout.controlRect.height + gap,
leftWidth,
(std::max)(220.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
layout.previewRect = UIRect(
leftWidth + margin * 2.0f,
margin,
(std::max)(420.0f, width - leftWidth - margin * 3.0f),
height - margin * 2.0f);
layout.fieldRect = UIRect(
layout.previewRect.x + 28.0f,
layout.previewRect.y + 96.0f,
(std::min)(460.0f, layout.previewRect.width - 56.0f),
22.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
{ ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
};
return layout;
}
void DrawCard(
UIDrawList& drawList,
const UIRect& rect,
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
std::string_view title,
std::string_view subtitle = {}) {
drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius);
drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
drawList.AddText(
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
std::string(title),
shellPalette.textPrimary,
shellMetrics.titleFontSize);
if (!subtitle.empty()) {
drawList.AddText(
UIPoint(rect.x + 16.0f, rect.y + 40.0f),
std::string(subtitle),
shellPalette.textMuted,
shellMetrics.bodyFontSize);
}
}
void DrawButton(
UIDrawList& drawList,
const ButtonLayout& button,
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
bool hovered) {
drawList.AddFilledRect(
button.rect,
hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground,
shellMetrics.buttonRadius);
drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius);
drawList.AddText(
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
button.label,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
}
std::string DescribeHitTarget(const UIEditorAssetFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorAssetFieldHitTargetKind::ValueBox:
return "value_box";
case UIEditorAssetFieldHitTargetKind::PickerButton:
return "picker_button";
case UIEditorAssetFieldHitTargetKind::ClearButton:
return "clear_button";
case UIEditorAssetFieldHitTargetKind::Row:
return "row";
case UIEditorAssetFieldHitTargetKind::None:
default:
return "none";
}
}
UIInputEvent MakePointerEvent(
UIInputEventType type,
const UIPoint& position,
UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = position;
event.pointerButton = button;
return event;
}
UIInputEvent MakeKeyEvent(std::int32_t keyCode) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = keyCode;
return event;
}
UIInputEvent MakeFocusEvent(UIInputEventType type) {
UIInputEvent event = {};
event.type = type;
return event;
}
class ScenarioApp {
public:
int Run(HINSTANCE hInstance, int nCmdShow) {
if (!Initialize(hInstance, nCmdShow)) {
Shutdown();
return 1;
}
MSG message = {};
while (message.message != WM_QUIT) {
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
TranslateMessage(&message);
DispatchMessageW(&message);
continue;
}
RenderFrame();
Sleep(8);
}
Shutdown();
return static_cast<int>(message.wParam);
}
private:
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCCREATE) {
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
auto* app = static_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
if (app == nullptr) {
return DefWindowProcW(hwnd, message, wParam, lParam);
}
return app->HandleMessage(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
m_hInstance = hInstance;
WNDCLASSEXW windowClass = {};
windowClass.cbSize = sizeof(windowClass);
windowClass.style = CS_HREDRAW | CS_VREDRAW;
windowClass.lpfnWndProc = &ScenarioApp::WndProc;
windowClass.hInstance = hInstance;
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
windowClass.lpszClassName = kWindowClassName;
m_windowClassAtom = RegisterClassExW(&windowClass);
if (m_windowClassAtom == 0) {
return false;
}
m_hwnd = CreateWindowExW(
0,
kWindowClassName,
kWindowTitle,
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT,
CW_USEDEFAULT,
1440,
860,
nullptr,
nullptr,
hInstance,
this);
if (m_hwnd == nullptr) {
return false;
}
ShowWindow(m_hwnd, nCmdShow);
UpdateWindow(m_hwnd);
if (!m_renderer.Initialize(m_hwnd)) {
return false;
}
m_shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
m_shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
m_fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorAssetFieldMetrics();
m_fieldPalette = XCEngine::UI::Editor::ResolveUIEditorAssetFieldPalette();
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/asset_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
ResetScenario();
return true;
}
void Shutdown() {
m_autoScreenshot.Shutdown();
m_renderer.Shutdown();
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
DestroyWindow(m_hwnd);
}
m_hwnd = nullptr;
if (m_windowClassAtom != 0 && m_hInstance != nullptr) {
UnregisterClassW(kWindowClassName, m_hInstance);
m_windowClassAtom = 0;
}
}
void ResetScenario() {
m_spec = {};
m_spec.fieldId = "renderer.base_map";
m_spec.label = "Base Map";
m_spec.emptyText = "None (Asset)";
ApplySampleAsset(0u);
m_interactionState = {};
m_lastResult = "已重置到默认 AssetField 状态";
m_activateCount = 0u;
m_pressedAction = ActionId::None;
}
void ApplySampleAsset(std::size_t sampleIndex) {
m_currentSampleIndex = sampleIndex % std::size(kSampleAssets);
const SampleAsset& sample = kSampleAssets[m_currentSampleIndex];
m_spec.assetId = sample.assetId;
m_spec.displayName = sample.displayName;
m_spec.statusText = sample.statusText;
m_spec.tint = sample.tint;
}
void ApplyNextSampleAsset() {
ApplySampleAsset(m_currentSampleIndex + 1u);
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L));
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
return BuildScenarioLayout(width, height, m_shellMetrics);
}
void RefreshFrame(const UIRect& fieldRect) {
m_frame = UpdateUIEditorAssetFieldInteraction(
m_interactionState,
m_spec,
fieldRect,
{},
m_fieldMetrics);
}
void RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L));
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
const ScenarioLayout layout = BuildScenarioLayout(width, height, m_shellMetrics);
std::vector<UIInputEvent> events = std::move(m_pendingInputEvents);
m_pendingInputEvents.clear();
m_frame = UpdateUIEditorAssetFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
events,
m_fieldMetrics);
ApplyInteractionResult(m_frame.result, layout.fieldRect);
const UIEditorAssetFieldHitTarget currentHit =
HitTestUIEditorAssetField(m_frame.layout, m_mousePosition);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorAssetFieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), m_shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
m_shellPalette,
m_shellMetrics,
"这个测试在验证什么功能?",
"验证 Editor 基础 AssetField 的固定风格、清晰 hit target以及 activate / picker / clear 三类最小请求。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 74.0f),
"1. 字段视觉固定为 Unity 风格对象槽:标签、预览块、值文本、状态标记、选择按钮、清空按钮。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 98.0f),
"2. 点击值区域只产生 activateRequested基础层不决定 ping、打开面板或业务跳转。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 122.0f),
"3. 点击 o或 focused 后按 Enter / Space只产生 pickerRequested。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 146.0f),
"4. 点击 X或 focused 后按 Delete / Backspace清空当前引用。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 170.0f),
"5. 本壳程序只用两个固定样本资产模拟 picker 外部响应,不引入 object picker 弹窗系统。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 194.0f),
"6. 左侧状态区观察 hover / focus / assetId / displayName / status / resultF12 截图。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, m_shellPalette, m_shellMetrics, "操作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
m_shellPalette,
m_shellMetrics,
ContainsPoint(button.rect, m_mousePosition.x, m_mousePosition.y));
}
DrawCard(
drawList,
layout.stateRect,
m_shellPalette,
m_shellMetrics,
"状态摘要",
"重点看 hit、focus、值是否变化以及请求是否保持薄语义。");
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 72.0f),
"Hover: " + DescribeHitTarget(currentHit),
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 96.0f),
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "yes" : "no"),
m_interactionState.fieldState.focused ? m_shellPalette.textSuccess : m_shellPalette.textMuted,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 120.0f),
"AssetId: " + (m_spec.assetId.empty() ? std::string("(empty)") : m_spec.assetId),
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 144.0f),
"Display: " + (m_spec.displayName.empty() ? std::string("(empty)") : m_spec.displayName),
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 168.0f),
"Status: " + (m_spec.statusText.empty() ? std::string("(empty)") : m_spec.statusText),
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 192.0f),
"Activate Count: " + std::to_string(m_activateCount),
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 216.0f),
"Result: " + m_lastResult,
m_shellPalette.textMuted,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 240.0f),
m_autoScreenshot.HasPendingCapture()
? "截图排队中..."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/integration/shell/asset_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary()),
m_shellPalette.textWeak,
m_shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
m_shellPalette,
m_shellMetrics,
"AssetField 预览",
"这里只放一个基础 AssetField不接业务面板也不接 picker。");
AppendUIEditorAssetField(
drawList,
layout.fieldRect,
m_spec,
m_interactionState.fieldState,
m_fieldPalette,
m_fieldMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
void ApplyInteractionResult(
const UIEditorAssetFieldInteractionResult& result,
const UIRect& fieldRect) {
bool refreshNeeded = false;
if (result.pickerRequested) {
ApplyNextSampleAsset();
m_lastResult = "收到 pickerRequested壳程序已用固定样本资产模拟外部选择结果";
refreshNeeded = true;
} else if (result.clearRequested) {
m_lastResult = "收到 clearRequested当前引用已清空";
refreshNeeded = true;
} else if (result.activateRequested) {
++m_activateCount;
m_lastResult = "收到 activateRequested基础层只发请求不绑定业务动作";
} else if (result.focusChanged) {
m_lastResult = std::string("焦点变化: ") +
(m_interactionState.fieldState.focused ? "focused" : "lost");
} else if (result.consumed) {
m_lastResult = "输入已被当前字段消费";
}
if (refreshNeeded) {
RefreshFrame(fieldRect);
}
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::Reset:
ResetScenario();
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "已请求截图,输出到 captures/latest.png";
break;
case ActionId::None:
default:
break;
}
}
const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const {
for (const ButtonLayout& button : layout.buttons) {
if (ContainsPoint(button.rect, x, y)) {
return &button;
}
}
return nullptr;
}
LRESULT HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_SIZE:
if (wParam != SIZE_MINIMIZED) {
m_renderer.Resize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE: {
m_mousePosition = UIPoint(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
TRACKMOUSEEVENT trackEvent = {};
trackEvent.cbSize = sizeof(trackEvent);
trackEvent.dwFlags = TME_LEAVE;
trackEvent.hwndTrack = hwnd;
TrackMouseEvent(&trackEvent);
m_pendingInputEvents.push_back(MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition));
return 0;
}
case WM_MOUSELEAVE:
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_pendingInputEvents.push_back(MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition));
return 0;
case WM_LBUTTONDOWN: {
SetFocus(hwnd);
m_mousePosition = UIPoint(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, m_mousePosition.x, m_mousePosition.y);
m_pressedAction = button != nullptr ? button->action : ActionId::None;
if (button == nullptr) {
m_pendingInputEvents.push_back(
MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left));
}
return 0;
}
case WM_LBUTTONUP: {
m_mousePosition = UIPoint(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, m_mousePosition.x, m_mousePosition.y);
if (m_pressedAction != ActionId::None &&
button != nullptr &&
button->action == m_pressedAction) {
ExecuteAction(button->action);
} else {
m_pendingInputEvents.push_back(
MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left));
}
m_pressedAction = ActionId::None;
return 0;
}
case WM_SETFOCUS:
m_pendingInputEvents.push_back(MakeFocusEvent(UIInputEventType::FocusGained));
return 0;
case WM_KILLFOCUS:
m_pendingInputEvents.push_back(MakeFocusEvent(UIInputEventType::FocusLost));
return 0;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (wParam == VK_F12) {
m_autoScreenshot.RequestCapture("manual_f12");
m_lastResult = "已请求截图,输出到 captures/latest.png";
return 0;
}
if (wParam == VK_F6) {
m_pendingInputEvents.push_back(MakeFocusEvent(UIInputEventType::FocusLost));
return 0;
}
if (const std::int32_t keyCode = MapAssetFieldKey(static_cast<UINT>(wParam));
keyCode != static_cast<std::int32_t>(KeyCode::None)) {
m_pendingInputEvents.push_back(MakeKeyEvent(keyCode));
return 0;
}
break;
case WM_PAINT:
RenderFrame();
ValidateRect(hwnd, nullptr);
return 0;
case WM_ERASEBKGND:
return 1;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
HWND m_hwnd = nullptr;
HINSTANCE m_hInstance = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
XCEngine::Tests::EditorUI::EditorValidationShellPalette m_shellPalette = {};
XCEngine::Tests::EditorUI::EditorValidationShellMetrics m_shellMetrics = {};
UIEditorAssetFieldSpec m_spec = {};
UIEditorAssetFieldInteractionState m_interactionState = {};
UIEditorAssetFieldInteractionFrame m_frame = {};
UIEditorAssetFieldPalette m_fieldPalette = {};
XCEngine::UI::Editor::Widgets::UIEditorAssetFieldMetrics m_fieldMetrics = {};
std::vector<UIInputEvent> m_pendingInputEvents = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_pressedAction = ActionId::None;
std::string m_lastResult = {};
std::size_t m_currentSampleIndex = 0u;
std::size_t m_activateCount = 0u;
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -1,10 +1,10 @@
#ifndef NOMINMAX
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorBoolFieldInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorBoolField.h>
#include <XCEditor/Fields/UIEditorBoolFieldInteraction.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorBoolField.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -47,7 +47,6 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorBoolField;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorBoolFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | BoolField Basic";
@@ -80,10 +79,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
@@ -371,14 +366,6 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/bool_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
@@ -407,7 +394,7 @@ private:
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
void ResetScenario() {
@@ -430,7 +417,7 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme);
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics();
m_frame = UpdateUIEditorBoolFieldInteraction(
m_interactionState,
m_value,
@@ -531,7 +518,7 @@ private:
UIEditorBoolFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme);
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics();
m_frame = UpdateUIEditorBoolFieldInteraction(
m_interactionState,
m_value,
@@ -578,15 +565,15 @@ private:
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
const UIEditorBoolFieldHitTarget currentHit =
HitTestUIEditorBoolField(m_frame.layout, m_mousePosition);
const auto boolMetrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme);
const auto boolPalette = XCEngine::UI::Editor::ResolveUIEditorBoolFieldPalette(m_theme);
const auto boolMetrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics();
const auto boolPalette = XCEngine::UI::Editor::ResolveUIEditorBoolFieldPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorBoolFieldBasic");
@@ -597,8 +584,8 @@ private:
layout.introRect,
shellPalette,
shellMetrics,
"这个测试在验证什么功能",
"验证 Editor BoolField 的基础交互契约,不涉及 PropertyGrid 或业务 Inspector");
"这个测试在验证什么功能",
"验证 Editor BoolField 的点击切换、键盘切换和状态同步,样式固定为 Editor 字段风格");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 row 或 checkbox检查 true / false 是否稳定切换。",
@@ -606,7 +593,7 @@ private:
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 控件获得 focus 后按 Space / Enter也必须能切换值。",
"2. 控件获得 focus 后按 Space / Enter也必须能切换值。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
@@ -644,12 +631,12 @@ private:
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "" : ""),
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "" : ""),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Active: ") + (m_interactionState.fieldState.active ? "" : ""),
std::string("Active: ") + (m_interactionState.fieldState.active ? "" : ""),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
@@ -674,11 +661,6 @@ private:
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
@@ -686,7 +668,7 @@ private:
shellPalette,
shellMetrics,
"BoolField 预览",
"这里只放一个 Unity 风格 BoolField。");
"这里只放一个固定样式 BoolField,不混入其他业务控件");
UIEditorBoolFieldSpec previewSpec = m_spec;
previewSpec.value = m_value;
AppendUIEditorBoolField(
@@ -715,12 +697,10 @@ private:
bool m_value = false;
UIEditorBoolFieldInteractionState m_interactionState = {};
UIEditorBoolFieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
std::string m_themeStatus = "fallback";
};
} // namespace

View File

@@ -1,10 +1,10 @@
#ifndef NOMINMAX
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorColorFieldInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorColorField.h>
#include <XCEditor/Fields/UIEditorColorFieldInteraction.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorColorField.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -48,8 +48,6 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorColorField;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldSpec;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorColorFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ColorField Basic";
@@ -81,11 +79,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool IsTruthyEnvironmentFlag(const char* name) {
const char* value = std::getenv(name);
if (value == nullptr || value[0] == '\0') {
@@ -135,7 +128,7 @@ ScenarioLayout BuildScenarioLayout(
layout.previewRect.x + 28.0f,
layout.previewRect.y + 72.0f,
360.0f,
32.0f);
22.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
@@ -379,14 +372,6 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/color_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
if (IsTruthyEnvironmentFlag("XCUI_COLOR_FIELD_OPEN_POPUP_ON_STARTUP")) {
@@ -405,6 +390,7 @@ private:
});
m_lastResult = "已自动打开 ColorField 弹窗";
}
return true;
}
@@ -431,7 +417,7 @@ private:
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
UIRect GetViewportRect() const {
@@ -465,7 +451,7 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics(m_theme);
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics();
m_frame = UpdateUIEditorColorFieldInteraction(
m_interactionState,
m_spec,
@@ -558,7 +544,7 @@ private:
UIEditorColorFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics(m_theme);
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics();
m_frame = UpdateUIEditorColorFieldInteraction(
m_interactionState,
m_spec,
@@ -594,6 +580,7 @@ private:
case ActionId::Reset:
ResetScenario();
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "已请求截图,输出到 captures/latest.png";
@@ -602,16 +589,7 @@ private:
}
std::string BuildColorSummary() const {
char buffer[128] = {};
std::snprintf(
buffer,
sizeof(buffer),
"R %.3f G %.3f B %.3f A %.3f",
m_spec.value.r,
m_spec.value.g,
m_spec.value.b,
m_spec.value.a);
return std::string(buffer);
return XCEngine::UI::Editor::Widgets::FormatUIEditorColorFieldRgbaText(m_spec);
}
void RenderFrame() {
@@ -620,19 +598,20 @@ private:
}
const UIRect viewportRect = GetViewportRect();
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const ScenarioLayout layout = BuildScenarioLayout(viewportRect.width, viewportRect.height, shellMetrics);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(
viewportRect.width,
viewportRect.height,
shellMetrics);
RefreshFrame();
const UIEditorColorFieldHitTarget currentHit =
HitTestUIEditorColorField(
m_frame.layout,
m_interactionState.colorFieldState.popupOpen,
m_mousePosition);
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics(m_theme);
const auto fieldPalette = XCEngine::UI::Editor::ResolveUIEditorColorFieldPalette(m_theme);
const UIEditorColorFieldHitTarget currentHit = HitTestUIEditorColorField(
m_frame.layout,
m_interactionState.colorFieldState.popupOpen,
m_mousePosition);
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics();
const auto fieldPalette = XCEngine::UI::Editor::ResolveUIEditorColorFieldPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorColorFieldBasic");
@@ -643,31 +622,31 @@ private:
layout.introRect,
shellPalette,
shellMetrics,
"这个测试在验证什么功能",
"验证 Editor ColorField 的独立样式、弹窗结构和拾色交互,不混入 PropertyGrid");
"这个测试在验证什么功能",
"验证 Editor ColorField 的 swatch、popup、SV square、hue wheel、RGBA slider 和关闭行为");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 swatch检查 popup 是否在控件下方稳定展开",
"1. 点击 swatch打开 popup。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. SV square 内拖拽,检查颜色是否连续变化。",
"2. 拖动 SV square检查颜色实时变化。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 拖 hue wheel R/G/B/A slider检查颜色和透明度是否同步更新",
"3. 拖 hue wheel R / G / B / A slider检查结果同步",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 点击右上角 close 或点击控件外部,检查 popup 是否关闭",
"4. 点击 close 或外部区域,关闭 popup。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 检查 Hexadecimal、数值框、handle、checkerboard 与截图路径是否同步",
"5. 左侧重点看 Hex、RGBA、Popup 和 Result",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
@@ -687,7 +666,7 @@ private:
shellPalette,
shellMetrics,
"状态摘要",
"重点检查 hover / popup / result / hex / rgba");
"重点 hoverpopup、hex、rgba 和结果");
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
@@ -713,6 +692,7 @@ private:
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队中..."
@@ -724,11 +704,6 @@ private:
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
@@ -736,7 +711,7 @@ private:
shellPalette,
shellMetrics,
"ColorField 预览",
"这里只放一个独立 ColorField便于单点检查样式与交互。");
"这里只放一个 ColorField用来验证 Editor 基础字段的 popup 交互。");
AppendUIEditorColorField(
drawList,
layout.fieldRect,
@@ -763,12 +738,10 @@ private:
UIEditorColorFieldSpec m_spec = {};
UIEditorColorFieldInteractionState m_interactionState = {};
UIEditorColorFieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
std::string m_themeStatus = "fallback";
};
} // namespace

View File

@@ -2,13 +2,13 @@
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorCommandDispatcher.h>
#include <XCEditor/Core/UIEditorMenuModel.h>
#include <XCEditor/Core/UIEditorMenuSession.h>
#include <XCEditor/Core/UIEditorShortcutManager.h>
#include <XCEditor/Core/UIEditorWorkspaceController.h>
#include <XCEditor/Core/UIEditorWorkspaceModel.h>
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
#include <XCEditor/Foundation/UIEditorCommandDispatcher.h>
#include <XCEditor/Shell/UIEditorMenuModel.h>
#include <XCEditor/Shell/UIEditorMenuSession.h>
#include <XCEditor/Foundation/UIEditorShortcutManager.h>
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
#include <XCEditor/Shell/UIEditorMenuPopup.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -767,7 +767,7 @@ void ScenarioApp::ResetScenario() {
SetCustomResult(
"等待操作",
"Ready",
"右键 Context Target 打开菜单hover `Workspace Tools` 展开子菜单Esc / 外部点击关闭。");
"右键 Context Target 打开菜单hover `Workspace Tools` 展开子菜单Esc / 点击外部关闭。");
}
void ScenarioApp::OnResize(UINT width, UINT height) {
@@ -913,7 +913,7 @@ void ScenarioApp::HandleMouseMove(float x, float y) {
SetCustomResult(
"Hover 收起更深层子菜单",
"Dismissed",
"鼠标移到普通菜单项后,更深层 child popup 已收起。closed: " +
"鼠标移到普通菜单项后,更深层 child popup 已收起。Closed: " +
JoinClosedPopupIds(mutation));
dirty = true;
}
@@ -925,7 +925,7 @@ void ScenarioApp::HandleMouseMove(float x, float y) {
SetCustomResult(
"Hover popup 空白区",
"Dismissed",
"鼠标停留在 popup 空白区后,更深层 child popup 已回收。closed: " +
"鼠标停留在 popup 空白区后,更深层 child popup 已回收。Closed: " +
JoinClosedPopupIds(mutation));
dirty = true;
}
@@ -952,7 +952,7 @@ void ScenarioApp::HandleLeftClick(float x, float y) {
mutation.changed
? "已展开 `" + hoveredItem->label +
"` 子菜单。正常行为是 hover 也会直接展开。"
: "子菜单已经处于开状态。");
: "子菜单已经处于开状态。");
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
@@ -983,7 +983,7 @@ void ScenarioApp::HandleLeftClick(float x, float y) {
SetCustomResult(
"点击 popup 空白区",
"Dismissed",
"点击当前 popup 空白区后,仅更深层子菜单被关闭。closed: " +
"点击当前 popup 空白区后,仅更深层子菜单被关闭。Closed: " +
JoinClosedPopupIds(mutation));
ClearHoverWhenMenuClosed();
InvalidateRect(m_hwnd, nullptr, FALSE);
@@ -998,7 +998,7 @@ void ScenarioApp::HandleLeftClick(float x, float y) {
"点击菜单外区域",
mutation.changed ? "Dismissed" : "NoOp",
mutation.changed
? "点击外部区域后,整条菜单链已关闭。closed: " +
? "点击外部区域后,整条菜单链已关闭。Closed: " +
JoinClosedPopupIds(mutation)
: "菜单链没有变化。");
ClearHoverWhenMenuClosed();
@@ -1015,7 +1015,7 @@ void ScenarioApp::HandleRightClick(float x, float y) {
"右键外部区域",
mutation.changed ? "Dismissed" : "NoOp",
mutation.changed
? "右键菜单外部区域后,整条菜单链已关闭。closed: " +
? "右键菜单外部区域后,整条菜单链已关闭。Closed: " +
JoinClosedPopupIds(mutation)
: "菜单链没有变化。");
ClearHoverWhenMenuClosed();
@@ -1045,7 +1045,7 @@ void ScenarioApp::HandleKeyDown(UINT keyCode) {
"Escape 关闭菜单",
mutation.changed ? "Dismissed" : "NoOp",
mutation.changed
? "按下 Escape 后topmost popup 已关闭。closed: " +
? "按下 Escape 后topmost popup 已关闭。Closed: " +
JoinClosedPopupIds(mutation)
: "当前没有可关闭的 popup。");
ClearHoverWhenMenuClosed();
@@ -1166,13 +1166,13 @@ void ScenarioApp::BuildDrawData(UIDrawData& drawData, float width, float height)
DrawCard(
drawList,
m_headerRect,
"测试功能Editor ContextMenu 基础层",
"这个测试验证什么功能?",
"本场景只验证 root popup 锚点、submenu hover、outside/Esc dismiss、命令派发不验证业务面板。");
drawList.AddText(UIPoint(m_headerRect.x + 18.0f, m_headerRect.y + 70.0f), "1. 在左侧 Context Target 内右键root popup 必须贴近鼠标位置打开。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(m_headerRect.x + 18.0f, m_headerRect.y + 92.0f), "2. hover `Workspace Tools`,右侧 child popup 必须立即弹出。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(m_headerRect.x + 18.0f, m_headerRect.y + 114.0f), "3. 点击 Show/Activate/Hide/Reset右侧 Details visible / active / activePanel 状态必须同步变化。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(m_headerRect.x + 18.0f, m_headerRect.y + 136.0f), "4. 点击 popup 外部区域应整条关闭;如果 child popup 已打开Esc 先关 topmost再关 root。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(m_headerRect.x + 18.0f, m_headerRect.y + 158.0f), "5. F12 截图R 直接触发 Reset Workspace便于确认命令派发仍正常。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(m_headerRect.x + 18.0f, m_headerRect.y + 158.0f), "5. F12 截图R 直接触发 Reset Workspace便于确认命令派发仍正常。", kTextPrimary, 13.0f);
DrawCard(drawList, m_targetRect, "Context Target", "只在这里接受右键打开 ContextMenu。");
DrawCard(drawList, m_stateRect, "状态摘要", "重点看 popup chain、anchor 和 Details 状态。");
@@ -1190,22 +1190,22 @@ void ScenarioApp::BuildDrawData(UIDrawData& drawData, float width, float height)
drawList.AddRectOutline(targetSurface, kCardBorder, 1.0f, 10.0f);
drawList.AddText(UIPoint(targetSurface.x + 18.0f, targetSurface.y + 18.0f), "在这块区域右键打开 ContextMenu", kTextPrimary, 16.0f);
drawList.AddText(UIPoint(targetSurface.x + 18.0f, targetSurface.y + 46.0f), "当前 anchor: " + FormatAnchorPoint(m_contextAnchorPoint, m_hasContextAnchor), kTextMuted, 12.0f);
drawList.AddText(UIPoint(targetSurface.x + 18.0f, targetSurface.y + 68.0f), "Visible Panels: " + JoinVisiblePanelIds(workspace, session), kTextMuted, 12.0f);
drawList.AddText(UIPoint(targetSurface.x + 18.0f, targetSurface.y + 90.0f), "Details visible: " + std::string(detailsState != nullptr && detailsState->visible ? "true" : "false"), kTextMuted, 12.0f);
drawList.AddText(UIPoint(targetSurface.x + 18.0f, targetSurface.y + 112.0f), "Details active: " + std::string(workspace.activePanelId == "details" ? "true" : "false"), kTextMuted, 12.0f);
drawList.AddText(UIPoint(targetSurface.x + 18.0f, targetSurface.y + 68.0f), "可见面板: " + JoinVisiblePanelIds(workspace, session), kTextMuted, 12.0f);
drawList.AddText(UIPoint(targetSurface.x + 18.0f, targetSurface.y + 90.0f), "Details 可见: " + std::string(detailsState != nullptr && detailsState->visible ? "true" : "false"), kTextMuted, 12.0f);
drawList.AddText(UIPoint(targetSurface.x + 18.0f, targetSurface.y + 112.0f), "Details 激活: " + std::string(workspace.activePanelId == "details" ? "true" : "false"), kTextMuted, 12.0f);
const UIRect stateBox(m_stateRect.x + 18.0f, m_stateRect.y + 74.0f, m_stateRect.width - 36.0f, 220.0f);
drawList.AddFilledRect(stateBox, kIndicatorBg, 10.0f);
drawList.AddRectOutline(stateBox, kCardBorder, 1.0f, 10.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 14.0f), "Open root menu", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 14.0f), "已打开 root menu", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 34.0f), m_menuSession.HasOpenMenu() ? std::string(m_menuSession.GetOpenRootMenuId()) : "(none)", kTextPrimary, 14.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 62.0f), "Popup chain", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 62.0f), "Popup ", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 82.0f), JoinPopupChainIds(m_menuSession), kTextPrimary, 14.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 110.0f), "Submenu path", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 110.0f), "Submenu 路径", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 130.0f), JoinSubmenuPathIds(m_menuSession), kTextPrimary, 14.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 158.0f), "Active panel", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 158.0f), "当前激活面板", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 178.0f), workspace.activePanelId.empty() ? "(none)" : workspace.activePanelId, kTextPrimary, 14.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 202.0f), "Menu validation: " + std::string(menuValidation.IsValid() ? "OK" : menuValidation.message), menuValidation.IsValid() ? kSuccess : kDanger, 12.0f);
drawList.AddText(UIPoint(stateBox.x + 14.0f, stateBox.y + 202.0f), "菜单验证: " + std::string(menuValidation.IsValid() ? "OK" : menuValidation.message), menuValidation.IsValid() ? kSuccess : kDanger, 12.0f);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()

View File

@@ -2,10 +2,10 @@
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorDockHostInteraction.h>
#include <XCEditor/Core/UIEditorWorkspaceController.h>
#include <XCEditor/Core/UIEditorWorkspaceModel.h>
#include <XCEditor/Widgets/UIEditorDockHost.h>
#include <XCEditor/Shell/UIEditorDockHostInteraction.h>
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
#include <XCEditor/Shell/UIEditorDockHost.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -584,15 +584,15 @@ private:
UIDrawList& drawList = drawData.EmplaceDrawList("DockHostBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(drawList, m_introRect, "这个测试验证什么功能", "只验证 DockHost 基础交互 contract不做 editor 业务面板。");
DrawCard(drawList, m_introRect, "这个测试验证什么功能", "只验证 DockHost 基础交互 contract不做 editor 业务面板。");
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 72.0f), "1. 验证 splitter drag 是否只通过 DockHostInteraction + WorkspaceController 完成。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f), "2. 验证 tab activate / tab close / standalone panel activate / panel close。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 116.0f), "3. 验证 active panel、visible panels、split ratio 是否统一收口到 controller。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 138.0f), "4. 验证 pointer capture / release 请求是否 contract 明确返回。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 162.0f), "建议操作: 先拖中间 splitter再点 Document A。", kTextWeak, 11.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 180.0f), "然后关闭 Document B最后点 Details Console 的 X。", kTextWeak, 11.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 138.0f), "4. 验证 pointer capture / release 请求是否通过 contract 明确返回。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 162.0f), "建议操作先拖中间 splitter再点 Document A。", kTextWeak, 11.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 180.0f), "然后关闭 Document B最后点 Details Console 的 X。", kTextWeak, 11.0f);
DrawCard(drawList, m_controlsRect, "操作", "只保留当前场景必要按钮。");
DrawCard(drawList, m_controlsRect, "操作", "这里只保留当前场景必要按钮。");
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
@@ -605,25 +605,25 @@ private:
};
addStateLine("Hover: " + DescribeHitTarget(m_interactionState.dockHostState.hoveredTarget), kTextPrimary, 11.0f);
addStateLine("Result: " + m_lastStatus, m_lastColor);
addStateLine("结果: " + m_lastStatus, m_lastColor);
drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY + 4.0f), m_lastMessage, kTextMuted, 11.0f);
stateY += 34.0f;
addStateLine("Active Panel: " + (m_controller.GetWorkspace().activePanelId.empty() ? std::string("(none)") : m_controller.GetWorkspace().activePanelId));
addStateLine("Visible Panels: " + JoinVisiblePanelIds(m_controller), kTextWeak, 11.0f);
addStateLine("Focused: " + FormatBool(m_cachedFrame.focused), m_cachedFrame.focused ? kSuccess : kTextMuted);
addStateLine("Capture: " + FormatBool(GetCapture() == m_hwnd), GetCapture() == m_hwnd ? kSuccess : kTextMuted);
addStateLine("当前面板: " + (m_controller.GetWorkspace().activePanelId.empty() ? std::string("(none)") : m_controller.GetWorkspace().activePanelId));
addStateLine("可见面板: " + JoinVisiblePanelIds(m_controller), kTextWeak, 11.0f);
addStateLine("焦点: " + FormatBool(m_cachedFrame.focused), m_cachedFrame.focused ? kSuccess : kTextMuted);
addStateLine("捕获: " + FormatBool(GetCapture() == m_hwnd), GetCapture() == m_hwnd ? kSuccess : kTextMuted);
addStateLine(
"Dragging Splitter: " +
"拖拽 Splitter: " +
(m_interactionState.dockHostState.activeSplitterNodeId.empty()
? std::string("(none)")
: m_interactionState.dockHostState.activeSplitterNodeId),
kTextWeak,
11.0f);
addStateLine("root-split ratio: " + FormatFloat(m_controller.GetWorkspace().root.splitRatio), kTextWeak, 11.0f);
addStateLine("根分割比例: " + FormatFloat(m_controller.GetWorkspace().root.splitRatio), kTextWeak, 11.0f);
if (m_controller.GetWorkspace().root.children.size() > 1u &&
m_controller.GetWorkspace().root.children[1].children.size() > 1u) {
addStateLine(
"right-split ratio: " +
"右侧分割比例: " +
FormatFloat(m_controller.GetWorkspace().root.children[1].splitRatio),
kTextWeak,
11.0f);
@@ -638,7 +638,7 @@ private:
kTextWeak,
11.0f);
DrawCard(drawList, m_previewRect, "Preview", "真实 DockHost 交互预览,只看基础层,不接旧 editor。");
DrawCard(drawList, m_previewRect, "预览", "真实 DockHost 交互预览,只看基础层,不接旧 editor。");
drawList.AddFilledRect(m_dockHostRect, kPreviewBg, 8.0f);
AppendUIEditorDockHostBackground(drawList, m_cachedFrame.layout);
AppendUIEditorDockHostForeground(drawList, m_cachedFrame.layout);

View File

@@ -2,10 +2,12 @@
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorPanelRegistry.h>
#include <XCEditor/Core/UIEditorShellCompose.h>
#include <XCEditor/Core/UIEditorWorkspaceController.h>
#include <XCEditor/Core/UIEditorWorkspaceModel.h>
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
#include <XCEditor/Shell/UIEditorShellCompose.h>
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include "../../shared/src/EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -41,6 +43,8 @@ using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationRequest;
using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName;
using XCEngine::UI::Editor::ResolveUIEditorShellComposeMetrics;
using XCEngine::UI::Editor::ResolveUIEditorShellComposePalette;
using XCEngine::UI::Editor::ResolveUIEditorShellComposeRequest;
using XCEngine::UI::Editor::UIEditorPanelPresentationKind;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
@@ -61,19 +65,12 @@ using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::Widgets::UIEditorMenuBarItem;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
const auto kShellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const auto kShellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
constexpr const wchar_t* kWindowClassName = L"XCUIEditorShellComposeValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Shell Compose";
constexpr const wchar_t* kWindowTitle = L"XCUI <EFBFBD>༭??| <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>";
constexpr UIColor kWindowBg(0.10f, 0.10f, 0.10f, 1.0f);
constexpr UIColor kCardBg(0.17f, 0.17f, 0.17f, 1.0f);
constexpr UIColor kCardBorder(0.28f, 0.28f, 0.28f, 1.0f);
constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 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 kButtonBg(0.24f, 0.24f, 0.24f, 1.0f);
constexpr UIColor kButtonOnBg(0.36f, 0.36f, 0.36f, 1.0f);
constexpr UIColor kButtonBorder(0.46f, 0.46f, 0.46f, 1.0f);
constexpr UIColor kSuccess(0.46f, 0.72f, 0.50f, 1.0f);
constexpr UIColor kDanger(0.78f, 0.35f, 0.35f, 1.0f);
@@ -103,6 +100,7 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -134,27 +132,27 @@ void DrawCard(
const UIRect& rect,
std::string_view title,
std::string_view subtitle = {}) {
drawList.AddFilledRect(rect, kCardBg, 10.0f);
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
drawList.AddFilledRect(rect, kShellPalette.cardBackground, kShellMetrics.cardRadius);
drawList.AddRectOutline(rect, kShellPalette.cardBorder, 1.0f, kShellMetrics.cardRadius);
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kShellPalette.textPrimary, kShellMetrics.titleFontSize);
if (!subtitle.empty()) {
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 38.0f), std::string(subtitle), kTextMuted, 12.0f);
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 38.0f), std::string(subtitle), kShellPalette.textMuted, kShellMetrics.bodyFontSize);
}
}
void DrawButton(UIDrawList& drawList, const ButtonState& button) {
drawList.AddFilledRect(button.rect, button.selected ? kButtonOnBg : kButtonBg, 8.0f);
drawList.AddRectOutline(button.rect, kButtonBorder, 1.0f, 8.0f);
drawList.AddText(UIPoint(button.rect.x + 12.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f);
drawList.AddFilledRect(button.rect, button.selected ? kShellPalette.buttonHoverBackground : kShellPalette.buttonBackground, kShellMetrics.buttonRadius);
drawList.AddRectOutline(button.rect, kShellPalette.cardBorder, 1.0f, kShellMetrics.buttonRadius);
drawList.AddText(UIPoint(button.rect.x + 12.0f, button.rect.y + 10.0f), button.label, kShellPalette.textPrimary, kShellMetrics.bodyFontSize);
}
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "hierarchy", "Hierarchy", UIEditorPanelPresentationKind::Placeholder, true, true, false },
{ "scene", "Scene", UIEditorPanelPresentationKind::ViewportShell, false, true, false },
{ "document", "Document", UIEditorPanelPresentationKind::Placeholder, true, true, true },
{ "inspector", "Inspector", UIEditorPanelPresentationKind::Placeholder, true, true, true }
{ "hierarchy", "层级", UIEditorPanelPresentationKind::Placeholder, true, true, false },
{ "scene", "场景", UIEditorPanelPresentationKind::ViewportShell, false, true, false },
{ "document", "文档", UIEditorPanelPresentationKind::Placeholder, true, true, true },
{ "inspector", "检视器", UIEditorPanelPresentationKind::Placeholder, true, true, true }
};
return registry;
}
@@ -165,7 +163,7 @@ UIEditorWorkspaceModel BuildWorkspace() {
"root-left-main",
UIEditorWorkspaceSplitAxis::Horizontal,
0.23f,
BuildUIEditorWorkspacePanel("hierarchy-node", "hierarchy", "Hierarchy", true),
BuildUIEditorWorkspacePanel("hierarchy-node", "hierarchy", "层级", true),
BuildUIEditorWorkspaceSplit(
"main-center-right",
UIEditorWorkspaceSplitAxis::Horizontal,
@@ -173,11 +171,11 @@ UIEditorWorkspaceModel BuildWorkspace() {
BuildUIEditorWorkspaceTabStack(
"center-tabs",
{
BuildUIEditorWorkspacePanel("scene-node", "scene", "Scene"),
BuildUIEditorWorkspacePanel("document-node", "document", "Document", true)
BuildUIEditorWorkspacePanel("scene-node", "scene", "场景"),
BuildUIEditorWorkspacePanel("document-node", "document", "文档", true)
},
0u),
BuildUIEditorWorkspacePanel("inspector-node", "inspector", "Inspector", true)));
BuildUIEditorWorkspacePanel("inspector-node", "inspector", "检视器", true)));
workspace.activePanelId = "scene";
return workspace;
}
@@ -318,7 +316,7 @@ private:
m_showTopBar = true;
m_showBottomBar = true;
m_textureEnabled = true;
m_lastResult = "Ready";
m_lastResult = "就绪";
}
void UpdateLayoutForCurrentWindow() {
@@ -350,13 +348,13 @@ private:
const float widthAvailable = m_controlsRect.width - 32.0f;
const bool sceneSelected = GetSelectedTabId() == "scene";
m_buttons = {
{ ActionId::ActivateScene, "切到 Scene", UIRect(left, top, widthAvailable, buttonHeight), sceneSelected },
{ ActionId::ActivateDocument, "切到 Document", UIRect(left, top + (buttonHeight + gap), widthAvailable, buttonHeight), !sceneSelected },
{ ActionId::ToggleTopBar, std::string("TopBar: ") + (m_showTopBar ? "" : ""), UIRect(left, top + (buttonHeight + gap) * 2.0f, widthAvailable, buttonHeight), m_showTopBar },
{ ActionId::ToggleBottomBar, std::string("BottomBar: ") + (m_showBottomBar ? "" : ""), UIRect(left, top + (buttonHeight + gap) * 3.0f, widthAvailable, buttonHeight), m_showBottomBar },
{ ActionId::ToggleTexture, std::string("Texture: ") + (m_textureEnabled ? "" : ""), UIRect(left, top + (buttonHeight + gap) * 4.0f, widthAvailable, buttonHeight), m_textureEnabled },
{ ActionId::ActivateScene, "Scene", UIRect(left, top, widthAvailable, buttonHeight), sceneSelected },
{ ActionId::ActivateDocument, "Document", UIRect(left, top + (buttonHeight + gap), widthAvailable, buttonHeight), !sceneSelected },
{ ActionId::ToggleTopBar, std::string("Top Bar: ") + (m_showTopBar ? "on" : "off"), UIRect(left, top + (buttonHeight + gap) * 2.0f, widthAvailable, buttonHeight), m_showTopBar },
{ ActionId::ToggleBottomBar, std::string("Bottom Bar: ") + (m_showBottomBar ? "on" : "off"), UIRect(left, top + (buttonHeight + gap) * 3.0f, widthAvailable, buttonHeight), m_showBottomBar },
{ ActionId::ToggleTexture, std::string("Texture: ") + (m_textureEnabled ? "on" : "off"), UIRect(left, top + (buttonHeight + gap) * 4.0f, widthAvailable, buttonHeight), m_textureEnabled },
{ ActionId::Reset, "Reset", UIRect(left, top + (buttonHeight + gap) * 5.0f, widthAvailable, buttonHeight), false },
{ ActionId::Capture, "截图", UIRect(left, top + (buttonHeight + gap) * 6.0f, widthAvailable, buttonHeight), false }
{ ActionId::Capture, "Capture", UIRect(left, top + (buttonHeight + gap) * 6.0f, widthAvailable, buttonHeight), false }
};
}
@@ -369,7 +367,7 @@ private:
};
model.statusSegments = {
UIEditorStatusBarSegment{ "mode", GetSelectedTabId() == "scene" ? "Scene" : "Document", UIEditorStatusBarSlot::Leading, {}, true, true, 86.0f },
UIEditorStatusBarSegment{ "chrome", std::string("Top ") + (m_showTopBar ? "On" : "Off"), UIEditorStatusBarSlot::Leading, {}, true, false, 82.0f },
UIEditorStatusBarSegment{ "chrome", std::string("Top ") + (m_showTopBar ? "on" : "off"), UIEditorStatusBarSlot::Leading, {}, true, false, 82.0f },
UIEditorStatusBarSegment{ "branch", m_textureEnabled ? "Texture" : "Fallback", UIEditorStatusBarSlot::Trailing, {}, true, true, 96.0f }
};
@@ -377,7 +375,7 @@ private:
presentation.panelId = "scene";
presentation.kind = UIEditorPanelPresentationKind::ViewportShell;
presentation.viewportShellModel.spec.chrome.title = "Scene";
presentation.viewportShellModel.spec.chrome.subtitle = "Editor Shell 基础层";
presentation.viewportShellModel.spec.chrome.subtitle = "Shell compose validation";
presentation.viewportShellModel.spec.chrome.showTopBar = m_showTopBar;
presentation.viewportShellModel.spec.chrome.showBottomBar = m_showBottomBar;
presentation.viewportShellModel.spec.chrome.topBarHeight = 40.0f;
@@ -387,16 +385,16 @@ private:
};
presentation.viewportShellModel.spec.statusSegments = {
{ "view", "Shell", UIEditorStatusBarSlot::Leading, {}, true, true, 64.0f },
{ "branch", m_textureEnabled ? "Texture" : "Fallback", UIEditorStatusBarSlot::Trailing, {}, true, false, 96.0f }
{ "branch", m_textureEnabled ? "纹理" : "回退", UIEditorStatusBarSlot::Trailing, {}, true, false, 96.0f }
};
if (m_textureEnabled) {
presentation.viewportShellModel.frame.hasTexture = true;
presentation.viewportShellModel.frame.texture = { 1u, 1280u, 720u };
presentation.viewportShellModel.frame.presentedSize = UISize(1280.0f, 720.0f);
presentation.viewportShellModel.frame.statusText = "Fake viewport frame";
presentation.viewportShellModel.frame.statusText = "Simulated viewport frame";
} else {
presentation.viewportShellModel.frame.hasTexture = false;
presentation.viewportShellModel.frame.statusText = "这里只验证 Editor shell compose不接 Scene/Game 业务。";
presentation.viewportShellModel.frame.statusText = "Simulated viewport frame";
}
model.workspacePresentations = { presentation };
return model;
@@ -411,6 +409,7 @@ private:
void UpdateShellFrame() {
const UIEditorShellComposeModel model = BuildShellModel();
const auto metrics = ResolveUIEditorShellComposeMetrics();
m_shellRequest = ResolveUIEditorShellComposeRequest(
m_shellRect,
m_controller.GetPanelRegistry(),
@@ -418,7 +417,8 @@ private:
m_controller.GetSession(),
model,
{},
m_shellState);
m_shellState,
metrics);
m_shellFrame = UpdateUIEditorShellCompose(
m_shellState,
m_shellRect,
@@ -426,7 +426,9 @@ private:
m_controller.GetWorkspace(),
m_controller.GetSession(),
model,
{});
{},
{},
metrics);
m_cachedModel = model;
}
@@ -445,30 +447,30 @@ private:
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::ActivateScene:
DispatchWorkspaceCommand(UIEditorWorkspaceCommandKind::ActivatePanel, "scene", "Activate Scene");
DispatchWorkspaceCommand(UIEditorWorkspaceCommandKind::ActivatePanel, "scene", "切到场景");
break;
case ActionId::ActivateDocument:
DispatchWorkspaceCommand(UIEditorWorkspaceCommandKind::ActivatePanel, "document", "Activate Document");
DispatchWorkspaceCommand(UIEditorWorkspaceCommandKind::ActivatePanel, "document", "切到文档");
break;
case ActionId::ToggleTopBar:
m_showTopBar = !m_showTopBar;
m_lastResult = m_showTopBar ? "TopBar 已打开" : "TopBar 已关闭";
m_lastResult = m_showTopBar ? "Top bar enabled" : "Top bar disabled";
break;
case ActionId::ToggleBottomBar:
m_showBottomBar = !m_showBottomBar;
m_lastResult = m_showBottomBar ? "BottomBar 已打开" : "BottomBar 已关闭";
m_lastResult = m_showBottomBar ? "Bottom bar enabled" : "Bottom bar disabled";
break;
case ActionId::ToggleTexture:
m_textureEnabled = !m_textureEnabled;
m_lastResult = m_textureEnabled ? "切到 Texture 分支" : "切到 Fallback 分支";
m_lastResult = m_textureEnabled ? "Texture branch enabled" : "Fallback branch enabled";
break;
case ActionId::Reset:
ResetScenario();
m_lastResult = "状态已重置";
m_lastResult = "Scenario reset";
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "截图已排队";
m_lastResult = "Capture queued";
InvalidateRect(m_hwnd, nullptr, FALSE);
UpdateWindow(m_hwnd);
break;
@@ -504,53 +506,46 @@ private:
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorShellCompose");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(drawList, m_introRect, "测试功能Editor 根壳层 compose", "只验证 MenuBar / Workspace / StatusBar 三段根壳组合。");
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 66.0f), "检查 1顶部 MenuBar、中间 WorkspaceCompose、底部 StatusBar 必须各自占带,不能互相挤压或重叠。", kTextMuted, 11.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 88.0f), "检查 2切换 Scene / Document 时,只影响中间 Workspace body presentation不应破坏顶栏和底栏。", kTextMuted, 11.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 110.0f), "检查 3切换 TopBar / BottomBar / Texture 时Request Size 必须同步,但 MenuBar / StatusBar 位置保持稳定。", kTextMuted, 11.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 132.0f), "这一层暂时不验证 menu popup 或 shortcut只验证 Editor 根框架 compose", kTextWeak, 11.0f);
DrawCard(drawList, m_controlsRect, "操作", "这里只保留当前这个根壳层需要检查的 7 个控件。");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kShellPalette.windowBackground);
DrawCard(drawList, m_introRect, "Validation Goal", "Verify root shell composition only.");
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 66.0f), "1. Menu bar, workspace, and status bar must compose into one stable shell.", kShellPalette.textMuted, 11.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 88.0f), "2. Scene should host the viewport shell, while Document should fall back to a placeholder.", kShellPalette.textMuted, 11.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 110.0f), "3. Top bar, bottom bar, and texture toggles must update layout and branch selection together.", kShellPalette.textMuted, 11.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 132.0f), "4. This scene validates shell composition only. No business panels are involved.", kShellPalette.textWeak, 11.0f);
DrawCard(drawList, m_controlsRect, "Actions", "Only controls required for this validation are exposed.");
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, m_stateRect, "状态", "左侧直接回显根壳层三段布局与中间 workspace request。");
DrawCard(drawList, m_stateRect, "State", "Inspect current shell layout, request size, and validation result.");
float stateY = m_stateRect.y + 66.0f;
auto addStateLine = [&](std::string text, const UIColor& color, float fontSize = 12.0f) {
drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY), std::move(text), color, fontSize);
stateY += 18.0f;
};
addStateLine("Active Panel: " + m_controller.GetWorkspace().activePanelId, kTextPrimary);
addStateLine("Selected Tab: " + GetSelectedTabId(), kTextPrimary);
addStateLine("Selected Presentation: " + selectedPresentation, kTextPrimary);
addStateLine("MenuBar Rect: " + FormatRect(m_shellFrame.layout.menuBarRect), kTextMuted, 11.0f);
addStateLine("Workspace Rect: " + FormatRect(m_shellFrame.layout.workspaceRect), kTextMuted, 11.0f);
addStateLine("StatusBar Rect: " + FormatRect(m_shellFrame.layout.statusBarRect), kTextMuted, 11.0f);
addStateLine("Requested viewport size: " + (viewportRequest != nullptr ? FormatSize(viewportRequest->viewportShellRequest.requestedViewportSize) : std::string("n/a")), kShellPalette.textPrimary);
addStateLine("Result: " + m_lastResult, kShellPalette.textMuted);
addStateLine(
"Request Size: " +
(viewportRequest != nullptr
? FormatSize(viewportRequest->viewportShellRequest.requestedViewportSize)
: std::string("n/a")),
kTextPrimary);
addStateLine("Result: " + m_lastResult, kTextMuted);
addStateLine(
validation.IsValid() ? "Workspace Validation: OK" : "Workspace Validation: " + validation.message,
validation.IsValid() ? "工作区校验:正常" : "工作区校验:" + validation.message,
validation.IsValid() ? kSuccess : kDanger,
11.0f);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队中..."
? "Capture queued..."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("截图:F12 或按钮 -> editor_shell_compose/captures/")
? std::string("F12 or the Capture button -> editor_shell_compose/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
addStateLine(captureSummary, kTextWeak, 11.0f);
DrawCard(drawList, m_previewRect, "Preview", "这里只有 Editor 根壳层 compose不混入业务面板。");
AppendUIEditorShellCompose(drawList, m_shellFrame, m_cachedModel, m_shellState);
addStateLine(captureSummary, kShellPalette.textWeak, 11.0f);
DrawCard(drawList, m_previewRect, "Preview", "Live UIEditorShellCompose preview.");
const auto palette = ResolveUIEditorShellComposePalette();
const auto metrics = ResolveUIEditorShellComposeMetrics();
AppendUIEditorShellCompose(
drawList,
m_shellFrame,
m_cachedModel,
m_shellState,
palette,
metrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(

View File

@@ -2,13 +2,15 @@
#define NOMINMAX
#endif
#include "Core/EditorShellAsset.h"
#include "Shell/EditorShellAsset.h"
#include <XCEditor/Core/UIEditorCommandDispatcher.h>
#include <XCEditor/Core/UIEditorMenuModel.h>
#include <XCEditor/Core/UIEditorShellInteraction.h>
#include <XCEditor/Core/UIEditorWorkspaceController.h>
#include <XCEditor/Core/UIEditorWorkspaceModel.h>
#include <XCEditor/Foundation/UIEditorCommandDispatcher.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Shell/UIEditorMenuModel.h>
#include <XCEditor/Shell/UIEditorShellInteraction.h>
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
#include "../../shared/src/EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -19,6 +21,8 @@
#include <windowsx.h>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <sstream>
#include <string>
@@ -54,6 +58,8 @@ using XCEngine::UI::Editor::EditorShellAsset;
using XCEngine::UI::Editor::EditorShellAssetValidationResult;
using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationFrame;
using XCEngine::UI::Editor::GetUIEditorCommandDispatchStatusName;
using XCEngine::UI::Editor::ResolveUIEditorShellInteractionMetrics;
using XCEngine::UI::Editor::ResolveUIEditorShellInteractionPalette;
using XCEngine::UI::Editor::UpdateUIEditorShellInteraction;
using XCEngine::UI::Editor::UIEditorCommandDispatchResult;
using XCEngine::UI::Editor::UIEditorCommandDispatcher;
@@ -85,19 +91,12 @@ using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
using XCEngine::UI::Widgets::UIPopupDismissReason;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
const auto kShellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const auto kShellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
constexpr const wchar_t* kWindowClassName = L"XCUIEditorShellInteractionValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Shell Interaction";
constexpr UIColor kWindowBg(0.10f, 0.10f, 0.10f, 1.0f);
constexpr UIColor kCardBg(0.17f, 0.17f, 0.17f, 1.0f);
constexpr UIColor kCardBorder(0.28f, 0.28f, 0.28f, 1.0f);
constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 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 kButtonBg(0.24f, 0.24f, 0.24f, 1.0f);
constexpr UIColor kButtonHover(0.32f, 0.32f, 0.32f, 1.0f);
constexpr UIColor kButtonBorder(0.46f, 0.46f, 0.46f, 1.0f);
constexpr UIColor kSuccess(0.46f, 0.72f, 0.50f, 1.0f);
constexpr UIColor kWarning(0.82f, 0.67f, 0.35f, 1.0f);
constexpr UIColor kDanger(0.78f, 0.35f, 0.35f, 1.0f);
@@ -122,6 +121,24 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
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";
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -248,18 +265,18 @@ void DrawCard(
const UIRect& rect,
std::string_view title,
std::string_view subtitle = {}) {
drawList.AddFilledRect(rect, kCardBg, 10.0f);
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
drawList.AddFilledRect(rect, kShellPalette.cardBackground, kShellMetrics.cardRadius);
drawList.AddRectOutline(rect, kShellPalette.cardBorder, 1.0f, kShellMetrics.cardRadius);
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kShellPalette.textPrimary, kShellMetrics.titleFontSize);
if (!subtitle.empty()) {
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 38.0f), std::string(subtitle), kTextMuted, 12.0f);
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 38.0f), std::string(subtitle), kShellPalette.textMuted, kShellMetrics.bodyFontSize);
}
}
void DrawButton(UIDrawList& drawList, const ButtonState& button) {
drawList.AddFilledRect(button.rect, button.hovered ? kButtonHover : kButtonBg, 8.0f);
drawList.AddRectOutline(button.rect, kButtonBorder, 1.0f, 8.0f);
drawList.AddText(UIPoint(button.rect.x + 14.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f);
drawList.AddFilledRect(button.rect, button.hovered ? kShellPalette.buttonHoverBackground : kShellPalette.buttonBackground, kShellMetrics.buttonRadius);
drawList.AddRectOutline(button.rect, kShellPalette.cardBorder, 1.0f, kShellMetrics.buttonRadius);
drawList.AddText(UIPoint(button.rect.x + 14.0f, button.rect.y + 10.0f), button.label, kShellPalette.textPrimary, kShellMetrics.bodyFontSize);
}
std::int32_t MapVirtualKeyToUIKeyCode(WPARAM wParam) {
@@ -433,7 +450,7 @@ EditorShellAsset BuildScenarioShellAsset() {
presentation.viewportShellModel.spec.chrome.showBottomBar = true;
presentation.viewportShellModel.frame.hasTexture = false;
presentation.viewportShellModel.frame.statusText =
"这里只验证 Editor 根壳交互,不接 editor 业务面板。";
"该场景只验证 Editor Shell 交互契约,不接 editor 业务面板。";
}
asset.shellDefinition.workspacePresentations.push_back(std::move(presentation));
}
@@ -441,7 +458,6 @@ EditorShellAsset BuildScenarioShellAsset() {
asset.shortcutAsset.commandRegistry = BuildCommandRegistry();
return asset;
}
class ScenarioApp {
public:
int Run(HINSTANCE hInstance, int nCmdShow);
@@ -485,7 +501,7 @@ private:
bool m_trackingMouseLeave = false;
std::string m_lastStatus = {};
std::string m_lastMessage = {};
UIColor m_lastColor = kTextMuted;
UIColor m_lastColor = kShellPalette.textMuted;
};
int ScenarioApp::Run(HINSTANCE hInstance, int nCmdShow) {
@@ -666,6 +682,11 @@ bool ScenarioApp::Initialize(HINSTANCE hInstance, int nCmdShow) {
}
ResetScenario();
if (IsAutoCaptureOnStartupEnabled()) {
m_lastStatus = "Capture";
m_lastMessage = "启动自动截图已排队。先核对首帧,再检查 root switch、child popup、dismiss 与 workspace suppression。";
m_lastColor = kWarning;
}
return true;
}
@@ -708,10 +729,9 @@ void ScenarioApp::ResetScenario() {
return;
}
m_lastStatus = "Ready";
m_lastMessage = "等待交互。这里只验证 Editor 根壳统一交互 contract不接旧 editor 业务";
m_lastMessage = "等待交互。重点检查根菜单切换、hover 子菜单、关闭链路与 workspace 抑制;设 XCUI_AUTO_CAPTURE_ON_STARTUP=1 可抓启动首帧";
m_lastColor = kWarning;
}
void ScenarioApp::UpdateLayout() {
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
@@ -734,7 +754,6 @@ void ScenarioApp::UpdateLayout() {
{ ActionId::Capture, "截图(F12)", UIRect(left + buttonWidth + 12.0f, top, buttonWidth, 36.0f), false }
};
}
void ScenarioApp::HandleMouseMove(float x, float y) {
UpdateLayout();
for (ButtonState& button : m_buttons) {
@@ -775,17 +794,17 @@ void ScenarioApp::ExecuteAction(ActionId action) {
if (action == ActionId::Reset) {
ResetScenario();
m_lastStatus = "Ready";
m_lastMessage = "场景状态已重置。请重新检查 root open / child popup / dismiss 行为。";
m_lastMessage = "已重置场景。请重新检查 root open / child popup / dismiss 行为。";
m_lastColor = kWarning;
return;
}
m_autoScreenshot.RequestCapture("manual_button");
m_lastStatus = "Ready";
m_lastMessage = "截图已排队,输出到 tests/UI/Editor/integration/shell/editor_shell_interaction/captures/。";
m_lastMessage =
"截图请求已排队。检查状态卡片中的 Output/latest.png。";
m_lastColor = kWarning;
}
bool ScenarioApp::HasInteractiveCaptureState() const {
if (m_interactionState.workspaceInteractionState.dockHostInteractionState.splitterDragState.active) {
return true;
@@ -833,7 +852,7 @@ void ScenarioApp::SetInteractionResult(const UIEditorShellInteractionResult& res
if (result.commandTriggered) {
m_lastStatus = "Triggered";
m_lastMessage = "命令已命中,但本帧没有完成 dispatch。请检查 shell services / asset contract";
m_lastMessage = "命令已触发但尚未派发,请检查 dispatch 链路";
m_lastColor = kWarning;
return;
}
@@ -842,19 +861,19 @@ void ScenarioApp::SetInteractionResult(const UIEditorShellInteractionResult& res
if (!result.itemId.empty() && !result.menuMutation.openedPopupId.empty()) {
m_lastStatus = "Changed";
m_lastMessage =
"已展开子菜单 `" + result.itemId + "`。请检查 child popup 是否在 hover 直接弹出。";
"`" + result.itemId + "` 已展开 child popuphover 直接弹出。";
m_lastColor = kSuccess;
} else if (!result.menuId.empty() && !result.menuMutation.openedPopupId.empty()) {
m_lastStatus = "Changed";
m_lastMessage =
"当前根菜单为 `" + result.menuId + "`。请检查 root popup 是否在打开态下可直接切换。";
"当前根菜单切换为 `" + result.menuId + "`root popup 应立即切换。";
m_lastColor = kSuccess;
} else {
m_lastStatus = "Dismissed";
m_lastMessage =
"菜单链已收起。DismissReason = " +
FormatDismissReason(result.menuMutation.dismissReason) +
"请检查 outside pointer down / Esc / focus loss 的收束是否正确";
"请检查 outside pointer down / Esc / focus loss。";
m_lastColor = kWarning;
}
return;
@@ -888,41 +907,41 @@ void ScenarioApp::SetInteractionResult(const UIEditorShellInteractionResult& res
if (result.requestPointerCapture) {
m_lastStatus = "Capture";
m_lastMessage = "宿主已收到 root shell 返回的 pointer capture 请求";
m_lastMessage = "控件请求了 pointer capture。";
m_lastColor = kSuccess;
return;
}
if (result.releasePointerCapture) {
m_lastStatus = "Release";
m_lastMessage = "宿主已执行 root shell 返回的 pointer release。";
m_lastMessage = "控件释放了 pointer capture。";
m_lastColor = kWarning;
return;
}
if (result.consumed) {
m_lastStatus = "NoOp";
m_lastMessage = "这次输入被根壳交互层消费,但没有触发额外状态变化。";
m_lastMessage = "输入已被消费,但没有产生更高层状态变化。";
m_lastColor = kWarning;
}
}
void ScenarioApp::SetDispatchResult(const UIEditorCommandDispatchResult& result) {
m_lastStatus = std::string(GetUIEditorCommandDispatchStatusName(result.status));
m_lastMessage = result.message.empty() ? std::string("命令派发完成") : result.message;
m_lastMessage = result.message.empty() ? std::string("命令派发。") : result.message;
m_lastColor = m_lastStatus == "Dispatched" ? kSuccess : kDanger;
}
void ScenarioApp::RenderFrame() {
UpdateLayout();
const UIEditorShellInteractionDefinition definition = BuildInteractionDefinition();
const auto metrics = ResolveUIEditorShellInteractionMetrics();
m_cachedFrame = UpdateUIEditorShellInteraction(
m_interactionState,
m_controller,
m_shellRect,
definition,
m_pendingInputEvents,
m_shellServices);
m_shellServices,
metrics);
m_pendingInputEvents.clear();
ApplyHostCaptureRequests(m_cachedFrame.result);
SetInteractionResult(m_cachedFrame.result);
@@ -939,71 +958,73 @@ void ScenarioApp::RenderFrame() {
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorShellInteraction");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kShellPalette.windowBackground);
DrawCard(drawList, m_introRect, "这个测试验证什么功能", "只验证 Editor 根壳统一交互 contract不做业务面板。");
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 70.0f), "1. 验证 MenuBar 的 root open / root switch 行为是否统一稳定。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 92.0f), "2. 验证 hover 子菜单时child popup 是否直接展开,不需要额外点击。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 114.0f), "3. 验证 outside pointer down / Esc / focus loss 是否能正确收起 popup chain。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 136.0f), "4. 验证菜单打开时会屏蔽 workspace 输入菜单关闭后workspace 交互立即恢复。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 158.0f), "5. 验证菜单命令会在 root shell 内直接 dispatch宿主不再二次派发。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 182.0f), "建议操作:点击 Filehover `Workspace Tools`,点预览外空白处,再点 `Document` 或拖 splitter。", kTextWeak, 11.0f);
DrawCard(
drawList,
m_introRect,
"这个测试验证什么?",
"只验证 Editor Shell 统一交互 contract不接 editor 业务面板。");
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 70.0f), "1. 验证 MenuBar 的 root open / root switch 行为是否统一。", kShellPalette.textPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 92.0f), "2. 验证 hover 子菜单项时child popup 是否直接弹出。", kShellPalette.textPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 114.0f), "3. 验证 outside pointer down / Esc / focus loss 是否正确关闭 popup chain。", kShellPalette.textPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 136.0f), "4. 验证菜单打开时是否抑制 workspace 输入;菜单关闭后是否恢复。", kShellPalette.textPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 158.0f), "5. 验证命令是否由 root shell 直接 dispatch不依赖 editor 业务层。", kShellPalette.textPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 182.0f), "建议操作:点击 Filehover `Workspace Tools`;再切换 `Document` 标签并拖动 splitter。设 XCUI_AUTO_CAPTURE_ON_STARTUP=1 可自动抓首帧。", kShellPalette.textWeak, 11.0f);
DrawCard(drawList, m_controlsRect, "操作", "保留这个场景必要的控制");
DrawCard(drawList, m_controlsRect, "操作", "放当前批次需要检查的交互");
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, m_stateRect, "状态", "重点检查根壳 contract 当前状态。");
DrawCard(drawList, m_stateRect, "状态", "直接观察当前 contract 状态。");
float stateY = m_stateRect.y + 66.0f;
auto addStateLine = [&](std::string label, std::string value, const UIColor& color, float fontSize = 12.0f) {
drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY), std::move(label) + ": " + std::move(value), color, fontSize);
stateY += 20.0f;
};
addStateLine("Open Root", m_cachedFrame.openRootMenuId.empty() ? "(none)" : m_cachedFrame.openRootMenuId, kTextPrimary);
addStateLine("Popup Chain", JoinPopupChainIds(m_interactionState), kTextPrimary, 11.0f);
addStateLine("Submenu Path", JoinSubmenuPathIds(m_interactionState), kTextPrimary, 11.0f);
addStateLine("根菜单", m_cachedFrame.openRootMenuId.empty() ? "(none)" : m_cachedFrame.openRootMenuId, kShellPalette.textPrimary);
addStateLine("Popup ", JoinPopupChainIds(m_interactionState), kShellPalette.textPrimary, 11.0f);
addStateLine("子菜单路径", JoinSubmenuPathIds(m_interactionState), kShellPalette.textPrimary, 11.0f);
addStateLine("资源校验", m_assetValidation.IsValid() ? "OK" : m_assetValidation.message, m_assetValidation.IsValid() ? kSuccess : kDanger, 11.0f);
addStateLine("当前呈现", selectedPresentation, kShellPalette.textPrimary, 11.0f);
addStateLine("当前面板", m_controller.GetWorkspace().activePanelId.empty() ? "(none)" : m_controller.GetWorkspace().activePanelId, kShellPalette.textPrimary, 11.0f);
addStateLine("焦点", FormatBool(m_cachedFrame.focused), m_cachedFrame.focused ? kSuccess : kShellPalette.textMuted);
addStateLine("菜单模态", FormatBool(m_cachedFrame.result.menuModal), m_cachedFrame.result.menuModal ? kSuccess : kShellPalette.textMuted, 11.0f);
addStateLine("Workspace 抑制", FormatBool(m_cachedFrame.result.workspaceInputSuppressed), m_cachedFrame.result.workspaceInputSuppressed ? kWarning : kShellPalette.textMuted, 11.0f);
addStateLine(
"Asset Validation",
m_assetValidation.IsValid() ? "OK" : m_assetValidation.message,
m_assetValidation.IsValid() ? kSuccess : kDanger,
11.0f);
addStateLine("Selected Presentation", selectedPresentation, kTextPrimary, 11.0f);
addStateLine("Active Panel", m_controller.GetWorkspace().activePanelId.empty() ? "(none)" : m_controller.GetWorkspace().activePanelId, kTextPrimary, 11.0f);
addStateLine("Focused", FormatBool(m_cachedFrame.focused), m_cachedFrame.focused ? kSuccess : kTextMuted);
addStateLine("Menu Modal", FormatBool(m_cachedFrame.result.menuModal), m_cachedFrame.result.menuModal ? kSuccess : kTextMuted, 11.0f);
addStateLine(
"Workspace Suppressed",
FormatBool(m_cachedFrame.result.workspaceInputSuppressed),
m_cachedFrame.result.workspaceInputSuppressed ? kWarning : kTextMuted,
11.0f);
addStateLine(
"Command Dispatch",
"命令派发",
m_cachedFrame.result.commandDispatched
? std::string(GetUIEditorCommandDispatchStatusName(m_cachedFrame.result.commandDispatchResult.status))
: std::string("(none)"),
m_cachedFrame.result.commandDispatched && m_cachedFrame.result.commandDispatchResult.commandExecuted
? kSuccess
: kTextMuted,
: kShellPalette.textMuted,
11.0f);
addStateLine("Result", m_lastStatus, m_lastColor);
drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY + 4.0f), m_lastMessage, kTextMuted, 11.0f);
addStateLine("结果", m_lastStatus, m_lastColor);
drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY + 4.0f), m_lastMessage, kShellPalette.textMuted, 11.0f);
stateY += 34.0f;
addStateLine("Visible Panels", JoinVisiblePanelIds(m_controller.GetWorkspace(), m_controller.GetSession()), kTextWeak, 11.0f);
addStateLine("Host Capture", FormatBool(GetCapture() == m_hwnd), GetCapture() == m_hwnd ? kSuccess : kTextMuted, 11.0f);
addStateLine("可见面板", JoinVisiblePanelIds(m_controller.GetWorkspace(), m_controller.GetSession()), kShellPalette.textWeak, 11.0f);
addStateLine("宿主捕获", FormatBool(GetCapture() == m_hwnd), GetCapture() == m_hwnd ? kSuccess : kShellPalette.textMuted, 11.0f);
addStateLine(
"Screenshot",
"截图",
m_autoScreenshot.HasPendingCapture()
? "截图排队中..."
? "截图请求排队中..."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 或 按钮 -> captures/")
? std::string("F12 或按钮 -> captures/")
: m_autoScreenshot.GetLastCaptureSummary()),
kTextWeak,
kShellPalette.textWeak,
11.0f);
DrawCard(drawList, m_previewRect, "Preview", "真实 UIEditorShellInteraction 预览,不接 editor 业务。");
AppendUIEditorShellInteraction(drawList, m_cachedFrame, m_interactionState);
DrawCard(drawList, m_previewRect, "预览", "这里是实际 UIEditorShellInteraction 预览,不接 editor 业务");
const auto palette = ResolveUIEditorShellInteractionPalette();
AppendUIEditorShellInteraction(
drawList,
m_cachedFrame,
m_interactionState,
palette,
metrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
@@ -1013,9 +1034,9 @@ void ScenarioApp::RenderFrame() {
static_cast<unsigned int>(height),
framePresented);
}
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -1,11 +1,11 @@
#ifndef NOMINMAX
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorEnumFieldInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorEnumField.h>
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
#include <XCEditor/Fields/UIEditorEnumFieldInteraction.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorEnumField.h>
#include <XCEditor/Shell/UIEditorMenuPopup.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -51,8 +51,6 @@ using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupInvalidIndex;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorEnumFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | EnumField Basic";
@@ -84,11 +82,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -142,7 +135,7 @@ ScenarioLayout BuildScenarioLayout(
layout.previewRect.x + 24.0f,
layout.previewRect.y + 82.0f,
340.0f,
32.0f);
22.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
@@ -387,14 +380,6 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/enum_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
@@ -423,7 +408,7 @@ private:
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
UIRect GetViewportRect() const {
@@ -458,8 +443,8 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme);
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics();
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics();
m_spec.selectedIndex = m_selectedIndex;
m_frame = UpdateUIEditorEnumFieldInteraction(
m_interactionState,
@@ -563,8 +548,8 @@ private:
UIEditorEnumFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme);
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics();
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics();
m_spec.selectedIndex = m_selectedIndex;
m_frame = UpdateUIEditorEnumFieldInteraction(
m_interactionState,
@@ -581,7 +566,7 @@ private:
void UpdateResultText(const UIEditorEnumFieldInteractionResult& result) {
if (result.selectionChanged && m_selectedIndex < m_spec.options.size()) {
m_lastResult = std::string("已切换: ") + m_spec.options[m_selectedIndex];
m_lastResult = std::string("已切换选项: ") + m_spec.options[m_selectedIndex];
return;
}
if (result.popupOpened) {
@@ -627,17 +612,17 @@ private:
}
const UIRect viewportRect = GetViewportRect();
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(viewportRect.width, viewportRect.height, shellMetrics);
RefreshFrame();
const UIEditorEnumFieldHitTarget currentHit =
HitTestUIEditorEnumField(m_frame.layout, m_mousePosition);
const auto enumMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme);
const auto enumPalette = XCEngine::UI::Editor::ResolveUIEditorEnumFieldPalette(m_theme);
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
const auto popupPalette = XCEngine::UI::Editor::ResolveUIEditorMenuPopupPalette(m_theme);
const auto enumMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics();
const auto enumPalette = XCEngine::UI::Editor::ResolveUIEditorEnumFieldPalette();
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics();
const auto popupPalette = XCEngine::UI::Editor::ResolveUIEditorMenuPopupPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorEnumFieldBasic");
@@ -648,26 +633,26 @@ private:
layout.introRect,
shellPalette,
shellMetrics,
"这个测试在验证什么功能",
"验证 Editor EnumField 的基础交互契约,不涉及 PropertyGrid 或业务 Inspector。");
"这个测试在验证什么功能",
"验证枚举字段的下拉打开/关闭、键盘切换、高亮同步,以及固定 Inspector 风格承载");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 value box 或 dropdown arrow检查下拉菜单是否展开/收起",
"1. 点击 value box 或 dropdown arrow应该打开或关闭下拉菜单",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 开后 hover 列表项,检查高亮是否稳定跟随",
"2. 开后检查 hover 高亮是否同步更新",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 获得 focus 后Up / Down / Home / End,再按 Enter / Space 选中,Esc 关闭。",
"3. 获得 focus 后Up / Down / Home / End 切换高亮;Enter / Space 确认;Esc 关闭。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 检查 Hover / Popup / Highlight / Selected / Result 是否同步更新",
"4. 观察 Hover / Popup / Highlight / Selected / Result 是否同步。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
@@ -735,11 +720,6 @@ private:
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
@@ -747,7 +727,7 @@ private:
shellPalette,
shellMetrics,
"EnumField 预览",
"这里只放一个 Unity 风格 EnumField");
"这里只放一个固定样式的枚举字段预览");
AppendUIEditorEnumField(
drawList,
layout.fieldRect,
@@ -783,12 +763,10 @@ private:
std::size_t m_selectedIndex = 0u;
UIEditorEnumFieldInteractionState m_interactionState = {};
UIEditorEnumFieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
std::string m_themeStatus = "fallback";
};
} // namespace

View File

@@ -643,31 +643,31 @@ private:
DrawCard(
drawList,
layout.introRect,
"这个测试验证什么功能",
"只验证 Editor ListView 基础控件不涉及任何业务面板。");
"这个测试验证什么功能",
"只验证 Editor ListView 基础控件行布局、单选、focus 和键盘导航。不涉及任何业务面板。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 验证列表行垂直排是否稳定:主标题和次标题不能互相挤压。",
"1. 检查列表行垂直排是否稳定:主标题和次标题不能互相挤压。",
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 点击 row 只切换 selectionhover、selected、focused 三种状态要能区分。",
"2. 点击 row 只切换 selectionhover、selected、focused 必须能区分。",
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 列表获得 focus 后,按 Up / Down / Home / End 应稳定移动当前选择。",
"3. 列表获得 focus 后,按 Up / Down / Home / End 应稳定移动当前选择。",
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 点击列表外空白后focus 应清除,但 selection 不应丢失",
"4. 点击列表外空白后focus 应清除,但 selection 必须保留",
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 按 F12 手动截图;设置 XCUI_AUTO_CAPTURE_ON_STARTUP=1 可自动截图。",
"5. 按 F12 手动截图;设置 XCUI_AUTO_CAPTURE_ON_STARTUP=1 可启动自动截图。",
kTextPrimary,
12.0f);
@@ -727,7 +727,7 @@ private:
kTextWeak,
12.0f);
DrawCard(drawList, layout.previewRect, "ListView 预览", "这里只放一个 ListView不混入 Project/Console 等业务内容。");
DrawCard(drawList, layout.previewRect, "ListView 预览", "这里只放一个 ListView不混入 Project / Console 等业务内容。");
AppendUIEditorListViewBackground(
drawList,
m_listFrame.layout,

View File

@@ -1,10 +1,11 @@
#ifndef NOMINMAX
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Collections/UIEditorInlineRenameSession.h>
#include <XCEditor/Collections/UIEditorListView.h>
#include <XCEditor/Collections/UIEditorListViewInteraction.h>
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Fields/UIEditorTextField.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include "EditorValidationTheme.h"
@@ -14,7 +15,6 @@
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Style/Theme.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h>
#include <windows.h>
@@ -33,6 +33,8 @@
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
// 场景说明:验证固定样式下 ListView 的 inline rename 提交、取消与外部点击提交。
namespace {
using XCEngine::Input::KeyCode;
@@ -47,8 +49,8 @@ using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Widgets::UISelectionModel;
using XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics;
using XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette;
using XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldMetrics;
using XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldPalette;
using XCEngine::UI::Editor::BuildUIEditorInlineRenameTextFieldMetrics;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::InputModifierTracker;
@@ -77,8 +79,6 @@ using XCEngine::UI::Editor::Widgets::UIEditorListViewInvalidIndex;
using XCEngine::UI::Editor::Widgets::UIEditorListViewItem;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldPalette;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorListViewInlineRenameValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ListView Inline Rename";
@@ -111,11 +111,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -242,12 +237,12 @@ void DrawButton(
std::vector<UIEditorListViewItem> BuildListItems() {
return {
{ "scene", "SampleScene.unity", "Scene | 4 分钟前修改", 0.0f },
{ "lighting", "LightingProfile.asset", "Preset | 3 profile", 0.0f },
{ "scene", "SampleScene.unity", "Scene | modified 4 minutes ago", 0.0f },
{ "lighting", "LightingProfile.asset", "Preset | 3 profiles", 0.0f },
{ "material", "RobotBody.mat", "Material | Metallic Workflow", 0.0f },
{ "script", "PlayerController.cs", "C# Script | 3.4 KB", 0.0f },
{ "texture", "Checker_AO.png", "Texture2D | 2048x2048", 0.0f },
{ "prefab", "Robot.prefab", "Prefab | 9 个组件", 0.0f },
{ "prefab", "Robot.prefab", "Prefab | 9 children", 0.0f },
{ "anim", "Walk.anim", "Animation Clip | 1.2 s", 0.0f },
{ "shader", "Outline.shader", "Shader | URP compatible", 0.0f }
};
@@ -346,8 +341,6 @@ private:
AutoScreenshotController m_autoScreenshot = {};
InputModifierTracker m_modifierTracker = {};
std::filesystem::path m_captureRoot = {};
Style::UITheme m_theme = {};
std::string m_themeStatus = "fallback";
std::vector<UIEditorListViewItem> m_items = {};
UISelectionModel m_selectionModel = {};
UIEditorListViewInteractionState m_listInteractionState = {};
@@ -524,13 +517,6 @@ bool ScenarioApp::Initialize(HINSTANCE hInstance, int nCmdShow) {
m_captureRoot = ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/list_view_inline_rename/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad = XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
m_modifierTracker.SyncFromSystemState();
ResetScenario();
@@ -560,7 +546,7 @@ ScenarioLayout ScenarioApp::GetLayout() const {
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
void ScenarioApp::ResetScenario() {
@@ -769,7 +755,7 @@ void ScenarioApp::HandleWindowFocusLost() {
PumpRenameEvents({ MakeFocusEvent(UIInputEventType::FocusLost) });
} else {
PumpListEvents({ MakeFocusEvent(UIInputEventType::FocusLost) });
m_lastResult = "窗口失去焦点,ListView focus 已清除。";
m_lastResult = "Window focus lost. ListView focus cleared.";
}
InvalidateRect(m_hwnd, nullptr, FALSE);
@@ -867,7 +853,6 @@ void ScenarioApp::ApplyRenameFrame(const UIEditorInlineRenameSessionFrame& frame
m_lastCommittedValue = result.valueAfter;
RefreshListFrame();
}
m_lastResult = "已提交 rename: " + result.itemId + " -> " + result.valueAfter;
return;
}
@@ -918,9 +903,9 @@ UIEditorInlineRenameSessionRequest ScenarioApp::BuildRenameRequest(bool beginSes
}
UIEditorTextFieldMetrics ScenarioApp::ResolveHostedTextFieldMetrics() const {
const auto propertyMetrics = ResolveUIEditorPropertyGridMetrics(m_theme);
const auto textMetrics = ResolveUIEditorTextFieldMetrics(m_theme);
return BuildUIEditorHostedTextFieldMetrics(propertyMetrics, textMetrics);
const auto propertyMetrics = ResolveUIEditorPropertyGridMetrics();
const auto textMetrics = ResolveUIEditorTextFieldMetrics();
return BuildUIEditorPropertyGridTextFieldMetrics(propertyMetrics, textMetrics);
}
UIEditorTextFieldMetrics ScenarioApp::ResolveInlineRenameMetrics(const UIRect& bounds) const {
@@ -928,9 +913,9 @@ UIEditorTextFieldMetrics ScenarioApp::ResolveInlineRenameMetrics(const UIRect& b
}
UIEditorTextFieldPalette ScenarioApp::ResolveInlineRenamePalette() const {
const auto propertyPalette = ResolveUIEditorPropertyGridPalette(m_theme);
const auto textPalette = ResolveUIEditorTextFieldPalette(m_theme);
return BuildUIEditorHostedTextFieldPalette(propertyPalette, textPalette);
const auto propertyPalette = ResolveUIEditorPropertyGridPalette();
const auto textPalette = ResolveUIEditorTextFieldPalette();
return BuildUIEditorPropertyGridTextFieldPalette(propertyPalette, textPalette);
}
void ScenarioApp::UpdateListResultText(const UIEditorListViewInteractionResult& result) {
@@ -978,8 +963,8 @@ void ScenarioApp::RenderFrame() {
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshListFrame();
@@ -1002,36 +987,36 @@ void ScenarioApp::RenderFrame() {
layout.introRect,
shellPalette,
shellMetrics,
"这个测试验证什么功能?",
"只验证 Editor ListView 的 inline rename 契约:默认直接进入编辑、字符编辑、Enter 提交、Esc 取消、点击外部提交。");
"这个测试验证什么功能?",
"只验证 Editor ListView 的 inline rename默认进入编辑、字符编辑、Enter 提交、Esc 取消、点击外部提交。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 启动后默认直接进入 script 的 rename检查输入框是否只覆盖当前选中行主文本。",
"1. 启动后默认直接进入 script 的 rename输入框只覆盖当前选中行主文本。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 输入字符后,检查 Draft 是否实时变化,且列表原文本被 overlay 正确遮住,不应双层叠字。",
"2. 输入字符后Draft 必须实时变化,原文本不能和 overlay 叠字。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 按 Enter,检查名称是否写回到对应 ListViewItem.primaryText关闭 rename。",
"3. 按 Enter:名称写回 ListViewItem.primaryText退出 rename。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 按 Esc,检查是否取消编辑保留原文本。",
"4. 按 Esc取消编辑保留原文本。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. rename 过程中点击输入框外部,检查是否提交当前草稿并退出编辑态。",
"5. rename 过程中点击输入框外部提交当前草稿并退出编辑态。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. 需要时先按 Esc 退出,再用 Up/Down/Home/End 切换选中项后按 F2F12 或按钮触发截图。",
"6. 需要时先按 Esc 退出,再用 Up/Down/Home/End 切换选中项后按 F2F12 截图。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
@@ -1116,7 +1101,7 @@ void ScenarioApp::RenderFrame() {
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 310.0f),
"Theme: " + m_themeStatus,
"Style: fixed",
shellPalette.textWeak,
shellMetrics.bodyFontSize);

View File

@@ -11,7 +11,6 @@
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Style/Theme.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h>
#include <windows.h>
@@ -56,8 +55,6 @@ using XCEngine::UI::Editor::Widgets::AppendUIEditorListViewForeground;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorListView;
using XCEngine::UI::Editor::Widgets::UIEditorListViewHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorListViewItem;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorListViewMultiSelectValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ListView MultiSelect";
@@ -90,11 +87,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -196,12 +188,12 @@ void DrawButton(
std::vector<UIEditorListViewItem> BuildListItems() {
return {
{ "scene", "SampleScene.unity", "Scene | 4 分钟前修改", 0.0f },
{ "lighting", "LightingProfile.asset", "Preset | 3 profile", 0.0f },
{ "scene", "SampleScene.unity", "Scene | modified 4 minutes ago", 0.0f },
{ "lighting", "LightingProfile.asset", "Preset | 3 profiles", 0.0f },
{ "material", "RobotBody.mat", "Material | Metallic Workflow", 0.0f },
{ "script", "PlayerController.cs", "C# Script | 3.4 KB", 0.0f },
{ "texture", "Checker_AO.png", "Texture2D | 2048x2048", 0.0f },
{ "prefab", "Robot.prefab", "Prefab | 9 个组件", 0.0f },
{ "prefab", "Robot.prefab", "Prefab | 9 children", 0.0f },
{ "anim", "Walk.anim", "Animation Clip | 1.2 s", 0.0f },
{ "shader", "Outline.shader", "Shader | URP compatible", 0.0f },
{ "mesh", "Robot.fbx", "Model | 38k triangles", 0.0f },
@@ -439,14 +431,6 @@ private:
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/list_view_multiselect/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
m_modifierTracker.SyncFromSystemState();
ResetScenario();
@@ -476,7 +460,7 @@ private:
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
void ResetScenario() {
@@ -489,7 +473,7 @@ private:
m_hasHoveredAction = false;
m_hoveredAction = ActionId::Reset;
m_lastModifiers = {};
m_lastResult = "已重置到默认多选状态";
m_lastResult = "Reset to the default multiselect state.";
RefreshFrame();
}
@@ -605,7 +589,7 @@ private:
if (wParam == VK_F12) {
m_autoScreenshot.RequestCapture("manual_f12");
m_lastResult = "已请求截图,输出到 captures/latest.png";
m_lastResult = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>??captures/latest.png";
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
@@ -675,13 +659,13 @@ private:
}
if (result.keyboardNavigated && !result.selectedItemId.empty()) {
m_lastResult = "键盘导航到: " + result.selectedItemId;
m_lastResult = "<EFBFBD><EFBFBD><EFBFBD>̵<EFBFBD><EFBFBD><EFBFBD>?? " + result.selectedItemId;
return;
}
if (result.selectionChanged) {
m_lastResult =
"选集已更新: primary=" + m_selectionModel.GetSelectedId() +
"ѡ<EFBFBD><EFBFBD><EFBFBD>Ѹ<EFBFBD>?? primary=" + m_selectionModel.GetSelectedId() +
" | ids=" + JoinSelectedIds(m_selectionModel);
return;
}
@@ -692,7 +676,7 @@ private:
}
if (insideList) {
m_lastResult = "点击列表内空白,只更新 hover / focus";
m_lastResult = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><EFBFBD>ڿհף<EFBFBD>ֻ<EFBFBD><EFBFBD>??hover / focus";
return;
}
@@ -706,7 +690,7 @@ private:
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "已请求截图,输出到 captures/latest.png";
m_lastResult = "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>??captures/latest.png";
break;
}
}
@@ -720,8 +704,8 @@ private:
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
@@ -737,7 +721,7 @@ private:
layout.introRect,
shellPalette,
shellMetrics,
"这个测试验证什么功能?",
"这个测试验证什么功能?",
"只验证 Editor ListView 多选 contractCtrl/Shift 选集、右键 primary 切换、键盘范围扩展,不涉及任何业务面板。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
@@ -845,11 +829,6 @@ private:
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 310.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
@@ -881,8 +860,6 @@ private:
AutoScreenshotController m_autoScreenshot = {};
InputModifierTracker m_modifierTracker = {};
std::filesystem::path m_captureRoot = {};
Style::UITheme m_theme = {};
std::string m_themeStatus = "fallback";
std::vector<UIEditorListViewItem> m_items = {};
UISelectionModel m_selectionModel = {};
UIEditorListViewInteractionState m_interactionState = {};

View File

@@ -2,12 +2,12 @@
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorCommandDispatcher.h>
#include <XCEditor/Core/UIEditorMenuModel.h>
#include <XCEditor/Core/UIEditorMenuSession.h>
#include <XCEditor/Core/UIEditorShortcutManager.h>
#include <XCEditor/Widgets/UIEditorMenuBar.h>
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
#include <XCEditor/Foundation/UIEditorCommandDispatcher.h>
#include <XCEditor/Shell/UIEditorMenuModel.h>
#include <XCEditor/Shell/UIEditorMenuSession.h>
#include <XCEditor/Foundation/UIEditorShortcutManager.h>
#include <XCEditor/Shell/UIEditorMenuBar.h>
#include <XCEditor/Shell/UIEditorMenuPopup.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -866,9 +866,9 @@ void ScenarioApp::ResetScenario() {
m_menuPopups.clear();
m_menuItems.clear();
SetCustomResult(
"等待操作",
"绛夊緟鎿嶄綔",
"Pending",
"先点 File / Window / Layout,确认一次只会打开一个菜单;再点菜单外区域或按 Escape 关闭。");
"鍏堢偣 File / Window / Layout锛岀‘璁や竴娆″彧浼氭墦寮€涓€涓彍鍗曪紱鍐嶇偣鑿滃崟澶栧尯鍩熸垨鎸?Escape 鍏抽棴銆?);
#endif
}
@@ -1004,7 +1004,7 @@ void ScenarioApp::HandleMouseMove(float x, float y) {
SetCustomResult(
"Hover 切换顶层菜单",
"Changed",
"已切换到顶层菜单 `" + hoveredButton->label + "`。重点检查:旧 root popup 被替换,新 root popup 立即出现。");
"已切换到顶层菜单 `" + hoveredButton->label + "`。重点检查:旧 root popup 被替换,新 root popup 是否立即出现。");
dirty = true;
}
} else {
@@ -1014,7 +1014,7 @@ void ScenarioApp::HandleMouseMove(float x, float y) {
SetCustomResult(
"Hover 收起子菜单",
"Dismissed",
"鼠标回到顶层按钮后,旧的 child popup 已收起。closed: " +
"鼠标回到顶层按钮后,旧的 child popup 已收起。Closed: " +
JoinClosedPopupIds(mutation));
dirty = true;
}
@@ -1040,7 +1040,7 @@ void ScenarioApp::HandleMouseMove(float x, float y) {
SetCustomResult(
"Hover 收起子菜单",
"Dismissed",
"鼠标移到普通菜单项后,旧的 child popup 已收起。closed: " +
"鼠标移到普通菜单项后,旧的 child popup 已收起。Closed: " +
JoinClosedPopupIds(mutation));
dirty = true;
}
@@ -1052,7 +1052,7 @@ void ScenarioApp::HandleMouseMove(float x, float y) {
SetCustomResult(
"Hover popup 空白区",
"Dismissed",
"鼠标停留在 popup 空白区后,子菜单链已回收到当前 popup。closed: " +
"鼠标停留在 popup 空白区后,子菜单链已回收到当前 popup。Closed: " +
JoinClosedPopupIds(mutation));
dirty = true;
}
@@ -1075,7 +1075,7 @@ void ScenarioApp::HandleClick(float x, float y) {
"点击关闭顶层菜单",
mutation.changed ? "Dismissed" : "NoOp",
mutation.changed
? "再次点击当前顶层按钮后,整条菜单链已关闭。closed: " +
? "再次点击当前顶层按钮后,整条菜单链已关闭。Closed: " +
JoinClosedPopupIds(mutation)
: "当前顶层菜单没有发生变化。");
} else {
@@ -1108,8 +1108,8 @@ void ScenarioApp::HandleClick(float x, float y) {
mutation.changed ? "Changed" : "NoOp",
mutation.changed
? "已展开 `" + hoveredItem->label +
"` 子菜单。这个场景正常行为是 hover 也会直接展开。"
: "子菜单已经处于开状态。");
"` 子菜单。这个场景正常行为是 hover 也会直接展开。"
: "子菜单已经处于开状态。");
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
@@ -1140,7 +1140,7 @@ void ScenarioApp::HandleClick(float x, float y) {
SetCustomResult(
"点击 popup 空白区",
"Dismissed",
"点击当前 popup 空白区后,仅更深层子菜单被关闭。closed: " +
"点击当前 popup 空白区后,仅更深层子菜单被关闭。Closed: " +
JoinClosedPopupIds(mutation));
ClearHoverWhenMenuClosed();
InvalidateRect(m_hwnd, nullptr, FALSE);
@@ -1155,7 +1155,7 @@ void ScenarioApp::HandleClick(float x, float y) {
"点击菜单外区域",
mutation.changed ? "Dismissed" : "NoOp",
mutation.changed
? "点击外部区域后,整条菜单链已关闭。closed: " +
? "点击外部区域后,整条菜单链已关闭。Closed: " +
JoinClosedPopupIds(mutation)
: "菜单链没有变化。");
ClearHoverWhenMenuClosed();
@@ -1172,14 +1172,14 @@ void ScenarioApp::HandleClick(float x, float y) {
if (m_openMenuId == button.menuId) {
m_openMenuId.clear();
m_hoveredItemId.clear();
SetCustomResult("关闭菜单", "NoOp", "再次点击当前菜单按钮,菜单已关闭。");
SetCustomResult("鍏抽棴鑿滃崟", "NoOp", "鍐嶆鐐瑰嚮褰撳墠鑿滃崟鎸夐挳锛岃彍鍗曞凡鍏抽棴銆?);
} else {
m_openMenuId = button.menuId;
m_hoveredItemId.clear();
SetCustomResult(
"打开菜单",
"鎵撳紑鑿滃崟",
"Changed",
"当前激活菜单: " + button.label + "。确认同一时刻只存在一个下拉菜单。");
"褰撳墠婵€娲昏彍鍗? " + button.label + "銆傜‘璁ゅ悓涓€鏃跺埢鍙瓨鍦ㄤ竴涓笅鎷夎彍鍗曘€?);
}
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
@@ -1197,9 +1197,9 @@ void ScenarioApp::HandleClick(float x, float y) {
if (!item.enabled) {
SetCustomResult(
"菜单项不可执行",
"鑿滃崟椤逛笉鍙墽琛?,
"Disabled",
"当前工作区状态下 `" + item.label + "` 不可执行。");
"褰撳墠宸ヤ綔鍖虹姸鎬佷笅 `" + item.label + "` 涓嶅彲鎵ц銆?);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
@@ -1216,7 +1216,7 @@ void ScenarioApp::HandleClick(float x, float y) {
if (!m_openMenuId.empty()) {
m_openMenuId.clear();
m_hoveredItemId.clear();
SetCustomResult("菜单失焦", "Dismissed", "点击菜单外区域,菜单已关闭。");
SetCustomResult("鑿滃崟澶辩劍", "Dismissed", "鐐瑰嚮鑿滃崟澶栧尯鍩燂紝鑿滃崟宸插叧闂€?);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
#endif
@@ -1255,13 +1255,13 @@ void ScenarioApp::HandleKeyDown(UINT keyCode) {
if (!m_openMenuId.empty()) {
m_openMenuId.clear();
m_hoveredItemId.clear();
SetCustomResult("Esc 关闭菜单", "Dismissed", "按下 Escape 后,菜单已关闭。");
SetCustomResult("Esc 鍏抽棴鑿滃崟", "Dismissed", "鎸変笅 Escape 鍚庯紝鑿滃崟宸插叧闂€?);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
break;
case 'R':
SetDispatchResult(
"键盘 Reset Workspace",
"閿洏 Reset Workspace",
m_commandDispatcher.Dispatch("workspace.reset", m_controller));
InvalidateRect(m_hwnd, nullptr, FALSE);
break;
@@ -1446,13 +1446,13 @@ void ScenarioApp::BuildDrawData(UIDrawData& drawData, float width, float height)
DrawCard(
drawList,
headerRect,
"测试内容Editor Menu 会话状态层",
"这个测试验证什么功能?",
"本场景只验证顶层菜单切换、child popup hover、Esc / outside dismiss、命令派发不验证业务面板。");
drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 70.0f), "1. 点击 File 打开 root popup再 hover `Workspace Tools`,右侧 child popup 应立即弹出。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 92.0f), "2. root popup 打开时,把鼠标移到 Window / Layout 顶层按钮,旧 popup 应直接被新的 root popup 替换。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 114.0f), "3. 点击 Window -> Activate Details再点 Window -> Hide Active检查右侧 details visible / active 是否按状态变化。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 136.0f), "4. 如果 child popup 已打开,按一次 Esc 只关 topmost再按一次 Esc 关闭整条菜单链。点击外部空白区也必须整条关闭。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 158.0f), "5. F12 保存截图R 可直接触发 Reset Workspace方便确认命令派发仍正常。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 158.0f), "5. F12 保存截图R 可直接触发 Reset Workspace方便确认命令派发仍正常。", kTextPrimary, 13.0f);
DrawCard(drawList, shellRect, "操作区", "这里只保留 MenuBar 和当前 popup overlay。");
DrawCard(drawList, stateRect, "状态摘要", "重点看 menu session 当前链路和 Details 状态,不放无关杂项。");
@@ -1464,15 +1464,15 @@ void ScenarioApp::BuildDrawData(UIDrawData& drawData, float width, float height)
const UIRect shellInfoRect(shellRect.x + 18.0f, shellRect.y + 144.0f, shellRect.width - 36.0f, 190.0f);
drawList.AddFilledRect(shellInfoRect, kIndicatorBg, 8.0f);
drawList.AddRectOutline(shellInfoRect, kCardBorder, 1.0f, 8.0f);
drawList.AddText(UIPoint(shellInfoRect.x + 14.0f, shellInfoRect.y + 14.0f), "Open root menu", kTextMuted, 12.0f);
drawList.AddText(UIPoint(shellInfoRect.x + 14.0f, shellInfoRect.y + 14.0f), "已打开 root menu", kTextMuted, 12.0f);
drawList.AddText(UIPoint(shellInfoRect.x + 14.0f, shellInfoRect.y + 34.0f), m_menuSession.HasOpenMenu() ? std::string(m_menuSession.GetOpenRootMenuId()) : "(none)", kTextPrimary, 14.0f);
drawList.AddText(UIPoint(shellInfoRect.x + 14.0f, shellInfoRect.y + 62.0f), "Popup chain", kTextMuted, 12.0f);
drawList.AddText(UIPoint(shellInfoRect.x + 14.0f, shellInfoRect.y + 62.0f), "Popup ", kTextMuted, 12.0f);
drawList.AddText(UIPoint(shellInfoRect.x + 14.0f, shellInfoRect.y + 82.0f), JoinPopupChainIds(m_menuSession), kTextPrimary, 14.0f);
drawList.AddText(UIPoint(shellInfoRect.x + 14.0f, shellInfoRect.y + 110.0f), "Submenu path", kTextMuted, 12.0f);
drawList.AddText(UIPoint(shellInfoRect.x + 14.0f, shellInfoRect.y + 110.0f), "Submenu 路径", kTextMuted, 12.0f);
drawList.AddText(UIPoint(shellInfoRect.x + 14.0f, shellInfoRect.y + 130.0f), JoinSubmenuPathIds(m_menuSession), kTextPrimary, 14.0f);
drawList.AddText(UIPoint(shellInfoRect.x + 14.0f, shellInfoRect.y + 158.0f), "提示popup overlay 必须压在右侧状态面板上方,这一轮会专门检查这个层级关系。", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 72.0f), "Hover summary", kAccent, 15.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 72.0f), "Hover 摘要", kAccent, 15.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 100.0f), "hover menu", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 120.0f), m_hoveredMenuId.empty() ? "(none)" : m_hoveredMenuId, kTextPrimary, 14.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 150.0f), "hover popup", kTextMuted, 12.0f);
@@ -1480,19 +1480,19 @@ void ScenarioApp::BuildDrawData(UIDrawData& drawData, float width, float height)
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 200.0f), "hover item", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 220.0f), m_hoveredItemId.empty() ? "(none)" : m_hoveredItemId, kTextPrimary, 14.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 262.0f), "Workspace summary", kAccent, 15.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 290.0f), "active panel", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 262.0f), "Workspace 摘要", kAccent, 15.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 290.0f), "当前激活面板", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 310.0f), workspace.activePanelId.empty() ? "(none)" : workspace.activePanelId, kTextPrimary, 14.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 338.0f), "visible panels", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 338.0f), "可见面板", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 358.0f), JoinVisiblePanelIds(workspace, session), kTextPrimary, 14.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 386.0f), "details visible", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 386.0f), "Details 可见", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 406.0f), detailsState != nullptr && detailsState->visible ? "true" : "false", detailsState != nullptr && detailsState->visible ? kSuccess : kWarning, 14.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 434.0f), "details active", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 434.0f), "Details 激活", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 454.0f), workspace.activePanelId == "details" ? "true" : "false", workspace.activePanelId == "details" ? kSuccess : kTextMuted, 14.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 482.0f), "menu model validation", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 482.0f), "菜单模型验证", kTextMuted, 12.0f);
drawList.AddText(UIPoint(stateRect.x + 18.0f, stateRect.y + 502.0f), menuValidation.IsValid() ? "OK" : menuValidation.message, menuValidation.IsValid() ? kSuccess : kDanger, 12.0f);
drawList.AddText(UIPoint(footerRect.x + 18.0f, footerRect.y + 56.0f), "Last interaction: " + m_lastActionName + " | Result: " + m_lastStatusLabel, m_lastStatusColor, 13.0f);
drawList.AddText(UIPoint(footerRect.x + 18.0f, footerRect.y + 56.0f), "最近交互: " + m_lastActionName + " | Result: " + m_lastStatusLabel, m_lastStatusColor, 13.0f);
drawList.AddText(UIPoint(footerRect.x + 18.0f, footerRect.y + 74.0f), m_lastMessage, kTextPrimary, 12.0f);
const std::string captureSummary =
@@ -1630,7 +1630,7 @@ void ScenarioApp::BuildDrawData(UIDrawData& drawData, float width, float height)
DrawCard(
drawList,
headerRect,
"测试内容Editor Menu 基础壳层验证",
"这个测试验证什么功能?",
"只验证 MenuBar / 下拉展开 / hover / 菜单关闭 / command dispatch不验证业务面板不验证完整编辑器菜单体系。");
drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 70.0f), "1. 点击 File / Window / Layout确认同一时刻只会有一个菜单展开。", kTextPrimary, 13.0f);
drawList.AddText(UIPoint(headerRect.x + 18.0f, headerRect.y + 92.0f), "2. 展开菜单后移动鼠标,确认 hover 高亮稳定disabled 项不会误显示成可点击。", kTextPrimary, 13.0f);

View File

@@ -1,10 +1,11 @@
#ifndef NOMINMAX
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorNumberFieldInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorNumberField.h>
#include <XCEditor/Fields/UIEditorNumberFieldInteraction.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Fields/UIEditorNumberField.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -48,7 +49,6 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorNumberFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | NumberField Basic";
@@ -84,10 +84,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
@@ -170,18 +166,16 @@ ScenarioLayout BuildScenarioLayout(
return layout;
}
XCEngine::UI::Editor::Widgets::UIEditorNumberFieldMetrics ResolveHostedNumberFieldMetrics(
const Style::UITheme& theme) {
const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(theme);
const auto numberMetrics = XCEngine::UI::Editor::ResolveUIEditorNumberFieldMetrics(theme);
return XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldMetrics(propertyMetrics, numberMetrics);
XCEngine::UI::Editor::Widgets::UIEditorNumberFieldMetrics ResolvePropertyGridNumberFieldMetrics() {
return XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldMetrics(
XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(),
XCEngine::UI::Editor::ResolveUIEditorNumberFieldMetrics());
}
XCEngine::UI::Editor::Widgets::UIEditorNumberFieldPalette ResolveHostedNumberFieldPalette(
const Style::UITheme& theme) {
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(theme);
const auto numberPalette = XCEngine::UI::Editor::ResolveUIEditorNumberFieldPalette(theme);
return XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldPalette(propertyPalette, numberPalette);
XCEngine::UI::Editor::Widgets::UIEditorNumberFieldPalette ResolvePropertyGridNumberFieldPalette() {
return XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldPalette(
XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(),
XCEngine::UI::Editor::ResolveUIEditorNumberFieldPalette());
}
void DrawCard(
@@ -430,14 +424,6 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/number_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
@@ -466,7 +452,7 @@ private:
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
void ResetScenario() {
@@ -494,7 +480,7 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolveHostedNumberFieldMetrics(m_theme);
const auto metrics = ResolvePropertyGridNumberFieldMetrics();
m_frame = UpdateUIEditorNumberFieldInteraction(
m_interactionState,
m_spec,
@@ -604,7 +590,7 @@ private:
UIEditorNumberFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolveHostedNumberFieldMetrics(m_theme);
const auto metrics = ResolvePropertyGridNumberFieldMetrics();
m_frame = UpdateUIEditorNumberFieldInteraction(
m_interactionState,
m_spec,
@@ -616,7 +602,7 @@ private:
void UpdateResultText(const UIEditorNumberFieldInteractionResult& result) {
if (result.editCommitRejected) {
m_lastResult = "提交失败,仍保持在编辑";
m_lastResult = "提交失败,仍保持在编辑状态。";
return;
}
if (result.editCommitted) {
@@ -624,11 +610,11 @@ private:
return;
}
if (result.editCanceled) {
m_lastResult = "已取消编辑";
m_lastResult = "已取消编辑";
return;
}
if (result.editStarted) {
m_lastResult = "已进入编辑";
m_lastResult = "已进入编辑状态。";
return;
}
if (result.valueChanged || result.stepApplied) {
@@ -636,7 +622,7 @@ private:
return;
}
if (result.consumed) {
m_lastResult = "控件已消费输入";
m_lastResult = "控件已消费输入";
return;
}
m_lastResult = "等待交互";
@@ -664,16 +650,16 @@ private:
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
const UIEditorNumberFieldHitTarget currentHit =
HitTestUIEditorNumberField(m_frame.layout, m_mousePosition);
const auto numberMetrics = ResolveHostedNumberFieldMetrics(m_theme);
const auto numberPalette = ResolveHostedNumberFieldPalette(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
const auto numberMetrics = ResolvePropertyGridNumberFieldMetrics();
const auto numberPalette = ResolvePropertyGridNumberFieldPalette();
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorNumberFieldBasic");
@@ -684,7 +670,7 @@ private:
layout.introRect,
shellPalette,
shellMetrics,
"这个测试在验证什么功能",
"这个测试在验证什么功能",
"验证 Inspector 宿主中的 NumberField 交互契约和默认宿主风格。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
@@ -698,7 +684,7 @@ private:
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 按 Enter 进入编辑态,直接输入字符Enter commitEsc cancel。",
"3. 按 Enter 进入编辑态,直接输入字符Enter commitEsc cancel。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
@@ -773,7 +759,7 @@ private:
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
"Theme: " + m_themeStatus,
"Style: fixed",
shellPalette.textWeak,
shellMetrics.bodyFontSize);
@@ -783,7 +769,7 @@ private:
shellPalette,
shellMetrics,
"NumberField 预览",
"这里预览 Inspector 宿主中的 Unity 风格 Number 字段。");
"这里预览 Inspector 宿主中的 Unity 风格 Number 字段。");
drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor);
drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f);
drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground);
@@ -825,12 +811,10 @@ private:
UIEditorNumberFieldSpec m_spec = {};
UIEditorNumberFieldInteractionState m_interactionState = {};
UIEditorNumberFieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
std::string m_themeStatus = "fallback";
};
} // namespace

View File

@@ -0,0 +1,31 @@
add_executable(editor_ui_object_field_basic_validation WIN32
main.cpp
)
target_include_directories(editor_ui_object_field_basic_validation PRIVATE
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
${CMAKE_SOURCE_DIR}/new_editor/include
${CMAKE_SOURCE_DIR}/new_editor/app
${CMAKE_SOURCE_DIR}/engine/include
)
target_compile_definitions(editor_ui_object_field_basic_validation PRIVATE
UNICODE
_UNICODE
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
)
if(MSVC)
target_compile_options(editor_ui_object_field_basic_validation PRIVATE /utf-8 /FS)
set_property(TARGET editor_ui_object_field_basic_validation PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
target_link_libraries(editor_ui_object_field_basic_validation PRIVATE
XCUIEditorLib
XCUIEditorHost
)
set_target_properties(editor_ui_object_field_basic_validation PROPERTIES
OUTPUT_NAME "XCUIEditorObjectFieldBasicValidation"
)

View File

@@ -0,0 +1,736 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorObjectField.h>
#include <XCEditor/Fields/UIEditorObjectFieldInteraction.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
namespace {
using XCEngine::Input::KeyCode;
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::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::UIEditorObjectFieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorObjectFieldInteractionResult;
using XCEngine::UI::Editor::UIEditorObjectFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorObjectFieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorObjectField;
using XCEngine::UI::Editor::Widgets::UIEditorObjectFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorObjectFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorObjectFieldSpec;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorObjectFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ObjectField Basic";
enum class ActionId : unsigned char {
Reset = 0,
Capture
};
struct ButtonLayout {
ActionId action = ActionId::Reset;
const char* label = "";
UIRect rect = {};
};
struct ScenarioLayout {
UIRect introRect = {};
UIRect controlRect = {};
UIRect stateRect = {};
UIRect previewRect = {};
UIRect fieldRect = {};
std::vector<ButtonLayout> buttons = {};
};
std::filesystem::path ResolveRepoRootPath() {
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
root = root.substr(1u, root.size() - 2u);
}
return std::filesystem::path(root).lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
y >= rect.y &&
y <= rect.y + rect.height;
}
std::int32_t MapObjectFieldKey(UINT keyCode) {
switch (keyCode) {
case VK_SPACE:
return static_cast<std::int32_t>(KeyCode::Space);
case VK_RETURN:
return static_cast<std::int32_t>(KeyCode::Enter);
case VK_DELETE:
return static_cast<std::int32_t>(KeyCode::Delete);
case VK_BACK:
return static_cast<std::int32_t>(KeyCode::Backspace);
default:
return static_cast<std::int32_t>(KeyCode::None);
}
}
ScenarioLayout BuildScenarioLayout(
float width,
float height,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
const float margin = shellMetrics.margin;
constexpr float leftWidth = 440.0f;
const float gap = shellMetrics.gap;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 236.0f);
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
layout.stateRect = UIRect(
margin,
layout.controlRect.y + layout.controlRect.height + gap,
leftWidth,
(std::max)(232.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
layout.previewRect = UIRect(
leftWidth + margin * 2.0f,
margin,
(std::max)(420.0f, width - leftWidth - margin * 3.0f),
height - margin * 2.0f);
layout.fieldRect = UIRect(
layout.previewRect.x + 24.0f,
layout.previewRect.y + 82.0f,
(std::min)(440.0f, layout.previewRect.width - 48.0f),
22.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
{ ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
};
return layout;
}
void DrawCard(
UIDrawList& drawList,
const UIRect& rect,
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
std::string_view title,
std::string_view subtitle = {}) {
drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius);
drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
drawList.AddText(
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
std::string(title),
shellPalette.textPrimary,
shellMetrics.titleFontSize);
if (!subtitle.empty()) {
drawList.AddText(
UIPoint(rect.x + 16.0f, rect.y + 40.0f),
std::string(subtitle),
shellPalette.textMuted,
shellMetrics.bodyFontSize);
}
}
void DrawButton(
UIDrawList& drawList,
const ButtonLayout& button,
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
bool hovered) {
drawList.AddFilledRect(
button.rect,
hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground,
shellMetrics.buttonRadius);
drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius);
drawList.AddText(
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
button.label,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
}
std::string DescribeHitTarget(const UIEditorObjectFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorObjectFieldHitTargetKind::ValueBox:
return "value_box";
case UIEditorObjectFieldHitTargetKind::ClearButton:
return "clear_button";
case UIEditorObjectFieldHitTargetKind::PickerButton:
return "picker_button";
case UIEditorObjectFieldHitTargetKind::Row:
return "row";
case UIEditorObjectFieldHitTargetKind::None:
default:
return "none";
}
}
UIInputEvent MakePointerEvent(
UIInputEventType type,
const UIPoint& position,
UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = position;
event.pointerButton = button;
return event;
}
UIInputEvent MakeFocusEvent(UIInputEventType type) {
UIInputEvent event = {};
event.type = type;
return event;
}
UIInputEvent MakeKeyEvent(std::int32_t keyCode) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = keyCode;
return event;
}
class ScenarioApp {
public:
int Run(HINSTANCE hInstance, int nCmdShow) {
if (!Initialize(hInstance, nCmdShow)) {
Shutdown();
return 1;
}
MSG message = {};
while (message.message != WM_QUIT) {
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
TranslateMessage(&message);
DispatchMessageW(&message);
continue;
}
RenderFrame();
Sleep(8);
}
Shutdown();
return static_cast<int>(message.wParam);
}
private:
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCCREATE) {
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
auto* app = static_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
if (app == nullptr) {
return DefWindowProcW(hwnd, message, wParam, lParam);
}
return app->HandleMessage(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
m_hInstance = hInstance;
WNDCLASSEXW windowClass = {};
windowClass.cbSize = sizeof(windowClass);
windowClass.style = CS_HREDRAW | CS_VREDRAW;
windowClass.lpfnWndProc = &ScenarioApp::WndProc;
windowClass.hInstance = hInstance;
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
windowClass.lpszClassName = kWindowClassName;
m_windowClassAtom = RegisterClassExW(&windowClass);
if (m_windowClassAtom == 0) {
return false;
}
m_hwnd = CreateWindowExW(
0,
kWindowClassName,
kWindowTitle,
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT,
CW_USEDEFAULT,
1360,
820,
nullptr,
nullptr,
hInstance,
this);
if (m_hwnd == nullptr) {
return false;
}
ShowWindow(m_hwnd, nCmdShow);
UpdateWindow(m_hwnd);
if (!m_renderer.Initialize(m_hwnd)) {
return false;
}
m_shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
m_shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
m_fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorObjectFieldMetrics();
m_fieldPalette = XCEngine::UI::Editor::ResolveUIEditorObjectFieldPalette();
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/object_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
ResetScenario();
return true;
}
void Shutdown() {
m_autoScreenshot.Shutdown();
m_renderer.Shutdown();
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
DestroyWindow(m_hwnd);
}
m_hwnd = nullptr;
if (m_windowClassAtom != 0 && m_hInstance != nullptr) {
UnregisterClassW(kWindowClassName, m_hInstance);
m_windowClassAtom = 0;
}
}
void ResetScenario() {
m_spec = {};
m_spec.fieldId = "target";
m_spec.label = "Target";
m_spec.objectName = "Main Camera";
m_spec.objectTypeName = "Camera";
m_spec.emptyText = "None (GameObject)";
m_spec.hasValue = true;
m_spec.showClearButton = true;
m_spec.showPickerButton = true;
m_interactionState = {};
m_activateCount = 0u;
m_lastResult = "已重置到默认 ObjectField 状态";
m_hoveredAction = ActionId::Reset;
m_pressedAction = ActionId::Reset;
m_hasHoveredAction = false;
m_hasPressedAction = false;
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L));
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
return BuildScenarioLayout(width, height, m_shellMetrics);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
for (const ButtonLayout& button : layout.buttons) {
if (ContainsPoint(button.rect, x, y)) {
m_hoveredAction = button.action;
m_hasHoveredAction = true;
return;
}
}
m_hasHoveredAction = false;
}
const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const {
for (const ButtonLayout& button : layout.buttons) {
if (ContainsPoint(button.rect, x, y)) {
return &button;
}
}
return nullptr;
}
void ApplyInteractionResult(const UIEditorObjectFieldInteractionResult& result) {
if (result.activateRequested) {
++m_activateCount;
m_lastResult = "已触发 activateRequested";
return;
}
if (result.clearRequested) {
m_spec.hasValue = false;
m_spec.objectName.clear();
m_spec.objectTypeName.clear();
m_lastResult = "已触发 clearRequested当前引用已清空";
return;
}
if (result.focusChanged) {
m_lastResult = std::string("焦点已切换: ") + (m_interactionState.fieldState.focused ? "focused" : "blurred");
return;
}
if (result.consumed) {
m_lastResult = "输入已消费";
return;
}
}
void QueueInput(UIInputEvent event) {
m_pendingInputEvents.push_back(std::move(event));
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::Reset:
ResetScenario();
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "截图已排队,输出到 captures/latest.png";
break;
}
}
void HandleMouseMove(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
UpdateHoveredAction(layout, x, y);
TRACKMOUSEEVENT trackEvent = {};
trackEvent.cbSize = sizeof(trackEvent);
trackEvent.dwFlags = TME_LEAVE;
trackEvent.hwndTrack = m_hwnd;
TrackMouseEvent(&trackEvent);
QueueInput(MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition));
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
QueueInput(MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition));
}
void HandleLeftButtonDown(float x, float y) {
SetFocus(m_hwnd);
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
UpdateHoveredAction(layout, x, y);
if (const ButtonLayout* button = HitTestAction(layout, x, y)) {
m_pressedAction = button->action;
m_hasPressedAction = true;
return;
}
m_hasPressedAction = false;
QueueInput(MakePointerEvent(
UIInputEventType::PointerButtonDown,
m_mousePosition,
UIPointerButton::Left));
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
UpdateHoveredAction(layout, x, y);
if (const ButtonLayout* button = HitTestAction(layout, x, y)) {
if (m_hasPressedAction && m_pressedAction == button->action) {
ExecuteAction(button->action);
}
m_hasPressedAction = false;
return;
}
m_hasPressedAction = false;
QueueInput(MakePointerEvent(
UIInputEventType::PointerButtonUp,
m_mousePosition,
UIPointerButton::Left));
}
void HandleKeyDown(std::int32_t keyCode) {
QueueInput(MakeKeyEvent(keyCode));
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
}
void RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L));
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
const ScenarioLayout layout = BuildScenarioLayout(width, height, m_shellMetrics);
std::vector<UIInputEvent> events = std::move(m_pendingInputEvents);
m_pendingInputEvents.clear();
m_frame = UpdateUIEditorObjectFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
events,
m_fieldMetrics);
ApplyInteractionResult(m_frame.result);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorObjectFieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), m_shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
m_shellPalette,
m_shellMetrics,
"这个测试在验证什么功能?",
"验证 Unity 风格 ObjectField 的值框、类型标签、clear / picker 按钮,以及 focus、activate、clear 契约。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 value box 或 picker 按钮,应触发 activateRequested。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 点击 clear 按钮,或 focused 时按 Delete / Backspace应清空当前对象。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. focused 后按 Enter / Space应继续走 activate 契约。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 重点检查 Hover、Focused、Has Value、Activate Count、Result 是否同步。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 按 F12 或点击截图按钮,可导出当前窗口截图。",
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.controlRect,
m_shellPalette,
m_shellMetrics,
"操作",
"这里只保留当前场景需要的最小操作。");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
m_shellPalette,
m_shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
m_shellPalette,
m_shellMetrics,
"状态摘要",
"重点检查 hit、focus、value、activate、result。");
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 74.0f),
"Hover: " + DescribeHitTarget(m_frame.result.hitTarget),
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 96.0f),
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "" : ""),
m_interactionState.fieldState.focused ? m_shellPalette.textSuccess : m_shellPalette.textMuted,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Has Value: ") + (m_spec.hasValue ? "" : ""),
m_spec.hasValue ? m_shellPalette.textSuccess : m_shellPalette.textMuted,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 140.0f),
"Value: " + (m_spec.hasValue ? m_spec.objectName : m_spec.emptyText),
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 162.0f),
"Type: " + (m_spec.objectTypeName.empty() ? std::string("(none)") : m_spec.objectTypeName),
m_shellPalette.textMuted,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 184.0f),
"Activate Count: " + std::to_string(m_activateCount),
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 206.0f),
"Result: " + m_lastResult,
m_shellPalette.textMuted,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 232.0f),
m_autoScreenshot.HasPendingCapture()
? "截图排队中..."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/integration/shell/object_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary()),
m_shellPalette.textWeak,
m_shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
m_shellPalette,
m_shellMetrics,
"ObjectField 预览",
"这里只放一个 Unity 风格 ObjectField不混入业务 Inspector。");
AppendUIEditorObjectField(
drawList,
layout.fieldRect,
m_spec,
m_interactionState.fieldState,
m_fieldPalette,
m_fieldMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
LRESULT HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
m_hwnd = nullptr;
PostQuitMessage(0);
return 0;
case WM_SIZE:
if (wParam != SIZE_MINIMIZED) {
OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_ERASEBKGND:
return 1;
case WM_MOUSEMOVE:
HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
case WM_MOUSELEAVE:
HandleMouseLeave();
return 0;
case WM_LBUTTONDOWN:
HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
case WM_LBUTTONUP:
HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
case WM_SETFOCUS:
QueueInput(MakeFocusEvent(UIInputEventType::FocusGained));
return 0;
case WM_KILLFOCUS:
QueueInput(MakeFocusEvent(UIInputEventType::FocusLost));
return 0;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (wParam == VK_F12) {
m_autoScreenshot.RequestCapture("manual_f12");
m_lastResult = "截图已排队,输出到 captures/latest.png";
return 0;
}
if (const std::int32_t keyCode = MapObjectFieldKey(static_cast<UINT>(wParam));
keyCode != static_cast<std::int32_t>(KeyCode::None)) {
HandleKeyDown(keyCode);
return 0;
}
break;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
HWND m_hwnd = nullptr;
HINSTANCE m_hInstance = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
XCEngine::Tests::EditorUI::EditorValidationShellPalette m_shellPalette = {};
XCEngine::Tests::EditorUI::EditorValidationShellMetrics m_shellMetrics = {};
XCEngine::UI::Editor::Widgets::UIEditorObjectFieldPalette m_fieldPalette = {};
XCEngine::UI::Editor::Widgets::UIEditorObjectFieldMetrics m_fieldMetrics = {};
UIEditorObjectFieldSpec m_spec = {};
UIEditorObjectFieldInteractionState m_interactionState = {};
UIEditorObjectFieldInteractionFrame m_frame = {};
std::vector<UIInputEvent> m_pendingInputEvents = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
ActionId m_pressedAction = ActionId::Reset;
bool m_hasHoveredAction = false;
bool m_hasPressedAction = false;
std::string m_lastResult = {};
std::size_t m_activateCount = 0u;
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -2,9 +2,9 @@
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorPanelContentHost.h>
#include <XCEditor/Core/UIEditorWorkspaceCompose.h>
#include <XCEditor/Core/UIEditorWorkspaceController.h>
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
#include <XCEditor/Shell/UIEditorWorkspaceCompose.h>
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -376,7 +376,7 @@ private:
m_composeState = {};
m_lastStatus = "Ready";
m_lastMessage =
"当前先看 doc-a inspector:两者都会 mount切到 Console 后tab body 会回退成 placeholder";
"默认状态下 doc-a inspector 会挂到外部内容宿主Console 仍然走占位内容路径";
UpdateComposeFrame();
}
@@ -406,8 +406,8 @@ private:
{ ActionId::ActivateInspector, "Activate Inspector", UIRect(buttonLeft + buttonWidth + 16.0f, buttonTop + buttonHeight + rowGap, buttonWidth, buttonHeight), false },
{ ActionId::CloseInspector, "Close Inspector", UIRect(buttonLeft, buttonTop + (buttonHeight + rowGap) * 2.0f, buttonWidth, buttonHeight), false },
{ ActionId::OpenInspector, "Open Inspector", UIRect(buttonLeft + buttonWidth + 16.0f, buttonTop + (buttonHeight + rowGap) * 2.0f, buttonWidth, buttonHeight), false },
{ ActionId::Reset, "Reset", UIRect(buttonLeft, buttonTop + (buttonHeight + rowGap) * 3.0f, buttonWidth, buttonHeight), false },
{ ActionId::Capture, "Capture(F12)", UIRect(buttonLeft + buttonWidth + 16.0f, buttonTop + (buttonHeight + rowGap) * 3.0f, buttonWidth, buttonHeight), false }
{ ActionId::Reset, "重置", UIRect(buttonLeft, buttonTop + (buttonHeight + rowGap) * 3.0f, buttonWidth, buttonHeight), false },
{ ActionId::Capture, "截图(F12)", UIRect(buttonLeft + buttonWidth + 16.0f, buttonTop + (buttonHeight + rowGap) * 3.0f, buttonWidth, buttonHeight), false }
};
}
@@ -443,7 +443,7 @@ private:
m_autoScreenshot.RequestCapture("manual_button");
m_lastStatus = "Ready";
m_lastMessage =
"截图已排队,输出到 tests/UI/Editor/integration/shell/panel_content_host_basic/captures/。";
"已请求截图,输出到 tests/UI/Editor/integration/shell/panel_content_host_basic/captures/。";
return;
}
@@ -507,19 +507,19 @@ private:
UIDrawList& drawList = drawData.EmplaceDrawList("PanelContentHostBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(drawList, m_introRect, "这个测试验证什么功能", "验证 Editor panel content host contract,不做业务逻辑。");
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 72.0f), "1. 验证 HostedContent panel 会正式接管 DockHost body而不是继续画 placeholder", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 94.0f), "2. 验证 tab 切换时,旧 body unmount,新 body mount", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 116.0f), "3. 验证切到 Console 这种 placeholder panel 后,external host 会退出", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 138.0f), "4. 验证 standalone HostedContent close/open 时,会发生 unmount / remount", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 164.0f), "建议操作Doc A -> Doc B -> Console -> Open/Close Inspector观察 Mounted Panels 和 Events", kTextWeak, 11.0f);
DrawCard(drawList, m_introRect, "这个测试验证什么功能", "验证面板内容宿主在 DockHost 中的挂载、卸载和占位回退契约,不做业务逻辑。");
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 72.0f), "1. HostedContent panel 应挂到 DockHost 外部 body不再走占位内容", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 94.0f), "2. 切换 tab 时,旧 body 应卸载,新 body 应挂载", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 116.0f), "3. Console 仍然是占位面板,不应该进入 external host。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 138.0f), "4. 关闭再打开 inspector应看到卸载和重新挂载事件", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 164.0f), "建议顺序Doc A -> Doc B -> Console -> Close/Open Inspector", kTextWeak, 11.0f);
DrawCard(drawList, m_controlsRect, "操作", "只保留内容承载 contract 必要按钮。");
DrawCard(drawList, m_controlsRect, "操作", "这里只保留当前契约验证需要的按钮。");
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, m_stateRect, "状态", "重点检查 mounted panel 集合和本帧 mount events");
DrawCard(drawList, m_stateRect, "状态", "重点检查已挂载面板集合和本帧挂载事件");
float stateY = m_stateRect.y + 66.0f;
auto addStateLine = [&](std::string label, std::string value, const UIColor& color, float fontSize = 12.0f) {
drawList.AddText(UIPoint(m_stateRect.x + 18.0f, stateY), std::move(label) + ": " + std::move(value), color, fontSize);
@@ -539,16 +539,16 @@ private:
addStateLine("doc-b", DescribeMountedState(m_composeFrame, "doc-b"), kTextWeak, 11.0f);
addStateLine("inspector", DescribeMountedState(m_composeFrame, "inspector"), kTextWeak, 11.0f);
addStateLine(
"Screenshot",
"截图",
m_autoScreenshot.HasPendingCapture()
? "截图排队中..."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 或 Capture -> captures/")
? std::string("F12 或点按钮 -> captures/")
: m_autoScreenshot.GetLastCaptureSummary()),
kTextWeak,
11.0f);
DrawCard(drawList, m_previewRect, "Preview", "DockHost 壳;蓝色内容块是 external content host 实际挂进去的 body。");
DrawCard(drawList, m_previewRect, "预览", "DockHost 只画外壳;蓝色内容块表示外部内容宿主实际挂的 body。");
AppendUIEditorWorkspaceCompose(drawList, m_composeFrame);
for (const UIEditorPanelContentHostPanelState& panelState : m_composeFrame.contentHostFrame.panelStates) {
if (!panelState.mounted ||

View File

@@ -2,7 +2,7 @@
#define NOMINMAX
#endif
#include <XCEditor/Widgets/UIEditorPanelFrame.h>
#include <XCEditor/Shell/UIEditorPanelFrame.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -375,7 +375,7 @@ private:
{ ActionId::ToggleFooter, "Footer", UIRect(left, top, width, buttonHeight), m_state.showFooter },
{ ActionId::TogglePin, "Pin Button", UIRect(left, top + (buttonHeight + gap), width, buttonHeight), m_state.pinnable },
{ ActionId::ToggleClose, "Close Button", UIRect(left, top + (buttonHeight + gap) * 2.0f, width, buttonHeight), m_state.closable },
{ ActionId::Reset, "Reset", UIRect(left, top + (buttonHeight + gap) * 3.0f, width, buttonHeight), false }
{ ActionId::Reset, "重置", UIRect(left, top + (buttonHeight + gap) * 3.0f, width, buttonHeight), false }
};
}
@@ -405,25 +405,30 @@ private:
DrawCard(
drawList,
introRect,
"测试功能PanelFrame 基础壳层",
"重点检查Header / Body / Footer 布局Active / Focus 边框Pin / Close 命中。");
"这个测试验证什么功能?",
"验证 PanelFrame 的 header/body/footer 分区,以及 active、focus、pin、close 命中。");
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 66.0f),
"操作:移动鼠标观察 hover点 Header 切 Active点 Body/Footer 取 Focus点 Pin/Close 看 Result按 F12 截图",
"1. hover 各区域,检查命中 part 是否正确",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 90.0f),
"这个场景只验证 Editor 基础层 PanelFrame不包含任何业务面板",
"2. 点击 Header、Body、Footer、Pin、Close检查状态变化",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 114.0f),
"3. 右侧只放一个 PanelFrame不接业务面板。",
kTextWeak,
12.0f);
DrawCard(drawList, controlsRect, "开关", "这里只保留基础状态开关,避免试验面板过杂");
DrawCard(drawList, controlsRect, "操作", "只保留影响 PanelFrame contract 的开关");
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, stateRect, "状态", "右侧面板的命中结果和状态变化会同步显示在这里");
DrawCard(drawList, stateRect, "状态摘要", "重点看 hover、active、focus、pinned 和结果");
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 66.0f),
"Hover: " + DescribePart(m_hoveredPart),

View File

@@ -1,10 +1,10 @@
#ifndef NOMINMAX
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorPropertyGridInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorPropertyGrid.h>
#include <XCEditor/Fields/UIEditorPropertyGridInteraction.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorPropertyGrid.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -57,7 +57,6 @@ using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorPropertyGridBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | PropertyGrid Basic";
@@ -91,11 +90,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -543,15 +537,6 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/property_grid_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
}
@@ -579,7 +564,7 @@ private:
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
void ResetScenario() {
@@ -606,7 +591,7 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme);
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics();
m_gridFrame =
UpdateUIEditorPropertyGridInteraction(
m_interactionState,
@@ -741,7 +726,7 @@ private:
UIEditorPropertyGridInteractionResult PumpGridEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme);
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics();
m_gridFrame =
UpdateUIEditorPropertyGridInteraction(
m_interactionState,
@@ -855,17 +840,17 @@ private:
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshGridFrame();
const UIEditorPropertyGridHitTarget currentHit =
HitTestUIEditorPropertyGrid(m_gridFrame.layout, m_mousePosition);
const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
const auto popupPalette = XCEngine::UI::Editor::ResolveUIEditorMenuPopupPalette(m_theme);
const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics();
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette();
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics();
const auto popupPalette = XCEngine::UI::Editor::ResolveUIEditorMenuPopupPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorPropertyGridBasic");
@@ -987,7 +972,7 @@ private:
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 310.0f),
"Theme: " + m_themeStatus,
"Style: fixed",
shellPalette.textWeak,
shellMetrics.bodyFontSize);
@@ -1037,14 +1022,11 @@ private:
UIExpansionModel m_expansionModel = {};
UIPropertyEditModel m_propertyEditModel = {};
UIEditorPropertyGridInteractionState m_interactionState = {};
UIEditorPropertyGridInteractionFrame m_gridFrame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
UIEditorPropertyGridInteractionFrame m_gridFrame = {}; UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
std::string m_lastCommit = {};
std::string m_themeStatus = "fallback";
};
} // namespace

View File

@@ -2,8 +2,8 @@
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorScrollViewInteraction.h>
#include <XCEditor/Widgets/UIEditorScrollView.h>
#include <XCEditor/Collections/UIEditorScrollViewInteraction.h>
#include <XCEditor/Collections/UIEditorScrollView.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -194,7 +194,7 @@ std::string DescribeHitTarget(const UIEditorScrollViewHitTarget& hitTarget) {
return "scrollbar-thumb";
case UIEditorScrollViewHitTargetKind::None:
default:
return "";
return "none";
}
}
@@ -423,7 +423,7 @@ private:
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "已重置到 " + std::to_string(m_logLines.size()) + " 行默认滚动位置";
m_lastResult = "已重置到 " + std::to_string(m_logLines.size()) + " 行默认滚动内容";
RefreshScrollFrame();
}
@@ -478,7 +478,7 @@ private:
const UIEditorScrollViewInteractionResult result =
PumpScrollEvents({ MakeWheelEvent(m_mousePosition, wheelDelta) });
if (result.offsetChanged) {
m_lastResult = "滚轮滚动: offset 已更新";
m_lastResult = "滚轮滚动offset 已更新";
}
InvalidateRect(m_hwnd, nullptr, FALSE);
}
@@ -568,23 +568,23 @@ private:
}
if (result.offsetChanged) {
m_lastResult = "滚动位置变化";
m_lastResult = "滚动位置变化";
return;
}
if (result.focusChanged) {
m_lastResult = m_interactionState.scrollViewState.focused
? "ScrollView 获得 focus"
? "ScrollView 获得 focus"
: "ScrollView focus 已清除";
return;
}
if (insideScrollView) {
m_lastResult = "点击内容区域: 只验证 focus / hover / scrollbar";
m_lastResult = "点击内容区只验证 focus / hover / scrollbar";
return;
}
m_lastResult = "等待交互";
m_lastResult = "无变化";
}
void ExecuteAction(ActionId action) {
@@ -622,31 +622,31 @@ private:
DrawCard(
drawList,
layout.introRect,
"这个测试验证什么功能",
"验证 Editor ScrollView 基础控件,不涉及任何业务面板");
"这个测试验证什么功能",
"验证滚动视图的滚轮滚动、thumb 拖拽、focus 切换和 offset clamp");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 把鼠标放到右侧日志区内滚动滚轮:内容应上下移动,offset 应变化",
"1. 在内容区滚轮滚动,检查 offset 是否连续更新",
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 继续滚到边界后 offset 被 clamp,不能越界",
"2. 滚到底部后继续滚动,offset 必须被 clamp。",
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 拖拽 scrollbar thumb内容位置应同步变化",
"3. 拖拽 scrollbar thumb检查 offset 与 thumb 位置是否同步",
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 点击内容区只改变 focus点击外部空白后 focus 应清除",
"4. 点击内容区只更新 focus / hover;点击外部应能清掉 focus。",
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 按 F12 手动截图;设置 XCUI_AUTO_CAPTURE_ON_STARTUP=1 可自动截图。",
"5. 按 F12 设置 XCUI_AUTO_CAPTURE_ON_STARTUP=1 触发截图。",
kTextPrimary,
12.0f);
@@ -666,7 +666,7 @@ private:
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.scrollViewState.focused ? "" : ""),
std::string("Focused: ") + (m_interactionState.scrollViewState.focused ? "" : ""),
kTextPrimary,
12.0f);
drawList.AddText(

View File

@@ -2,7 +2,7 @@
#define NOMINMAX
#endif
#include <XCEditor/Widgets/UIEditorStatusBar.h>
#include <XCEditor/Shell/UIEditorStatusBar.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -313,10 +313,10 @@ private:
if (m_hoverTarget.kind == UIEditorStatusBarHitTargetKind::Segment) {
m_state.activeIndex = m_hoverTarget.index;
m_state.focused = true;
m_lastResult = "激活 segment: " + m_segments[m_hoverTarget.index].segmentId;
m_lastResult = "命中 segment: " + m_segments[m_hoverTarget.index].segmentId;
} else if (m_hoverTarget.kind == UIEditorStatusBarHitTargetKind::Background) {
m_state.activeIndex = UIEditorStatusBarInvalidIndex;
m_lastResult = "点击 status bar background";
m_lastResult = "命中 status bar background";
} else {
m_lastResult = "命中 " + DescribeHitTarget(m_hoverTarget);
}
@@ -330,22 +330,22 @@ private:
m_segments[1].tone == UIEditorStatusBarTextTone::Accent
? UIEditorStatusBarTextTone::Primary
: UIEditorStatusBarTextTone::Accent;
m_lastResult = "切换 Selection 文本强调";
m_lastResult = "Selection 强调已切换";
break;
case ActionId::ToggleSeparator:
m_segments[0].showSeparator = !m_segments[0].showSeparator;
m_lastResult = m_segments[0].showSeparator ? "开启 Leading separator" : "关闭 Leading separator";
m_lastResult = m_segments[0].showSeparator ? "Leading separator 已开启" : "Leading separator 已关闭";
break;
case ActionId::MoveToTrailing:
m_segments[1].slot =
m_segments[1].slot == UIEditorStatusBarSlot::Leading
? UIEditorStatusBarSlot::Trailing
: UIEditorStatusBarSlot::Leading;
m_lastResult = "切换 Selection slot -> " + DescribeSlot(m_segments[1].slot);
m_lastResult = "Selection slot -> " + DescribeSlot(m_segments[1].slot);
break;
case ActionId::Reset:
ResetState();
m_lastResult = "状态重置";
m_lastResult = "状态重置";
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
@@ -368,7 +368,7 @@ private:
m_state.focused = true;
m_state.activeIndex = 1u;
m_hoverTarget = {};
m_lastResult = "Ready";
m_lastResult = "就绪";
}
void UpdateHover() {
@@ -383,11 +383,11 @@ private:
const float buttonHeight = 34.0f;
const float gap = 10.0f;
m_buttons = {
{ ActionId::ToggleAccent, "强调文本", UIRect(left, top, width, buttonHeight), m_segments[1].tone == UIEditorStatusBarTextTone::Accent },
{ ActionId::ToggleSeparator, "Leading 分隔线", UIRect(left, top + (buttonHeight + gap), width, buttonHeight), m_segments[0].showSeparator },
{ ActionId::ToggleAccent, "切换强调", UIRect(left, top, width, buttonHeight), m_segments[1].tone == UIEditorStatusBarTextTone::Accent },
{ ActionId::ToggleSeparator, "Leading separator", UIRect(left, top + (buttonHeight + gap), width, buttonHeight), m_segments[0].showSeparator },
{ ActionId::MoveToTrailing, "切换 Selection Slot", UIRect(left, top + (buttonHeight + gap) * 2.0f, width, buttonHeight), m_segments[1].slot == UIEditorStatusBarSlot::Trailing },
{ ActionId::Reset, "Reset", UIRect(left, top + (buttonHeight + gap) * 3.0f, width, buttonHeight), false },
{ ActionId::Capture, "截图", UIRect(left, top + (buttonHeight + gap) * 4.0f, width, buttonHeight), false }
{ ActionId::Reset, "重置", UIRect(left, top + (buttonHeight + gap) * 3.0f, width, buttonHeight), false },
{ ActionId::Capture, "截图(F12)", UIRect(left, top + (buttonHeight + gap) * 4.0f, width, buttonHeight), false }
};
}
@@ -429,25 +429,30 @@ private:
DrawCard(
drawList,
introRect,
"测试功能StatusBar 基础壳层",
"重点检查Leading / Trailing slot 对齐文本强调separator 开关,hover / active 命中。");
"这个测试验证什么功能?",
"验证 StatusBar 的 leading/trailing 布局、separator、强调文本以及 hover/active 命中。");
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 66.0f),
"操作:hover 观察 segment 高亮;点击 segment 切 active切换左侧按钮检查 slot / separator / emphasis按 F12 或点“截图”",
"1. hover 不同 segment / separator检查命中是否正确",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 90.0f),
"这个场景只验证 Editor 基础层 StatusBar不混入任何业务面板",
"2. 点击 segment检查 active 是否切到对应项",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 114.0f),
"3. 切换强调、separator 和 Selection slot检查布局是否稳定。",
kTextWeak,
12.0f);
DrawCard(drawList, controlsRect, "开关", "只保留和当前 StatusBar contract 直接相关的操作");
DrawCard(drawList, controlsRect, "操作", "只保留 StatusBar contract 直接相关的开关");
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, stateRect, "状态", "命中、active、slot 和结果统一回显在这里");
DrawCard(drawList, stateRect, "状态摘要", "重点看 hover、active、Selection slot、separator 和结果");
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 66.0f),
"Hover: " + DescribeHitTarget(m_hoverTarget),
@@ -489,12 +494,12 @@ private:
drawList.AddRectOutline(viewportRect, kCardBorder, 1.0f, 10.0f);
drawList.AddText(
UIPoint(viewportRect.x + 18.0f, viewportRect.y + 18.0f),
"Preview Host",
"预览宿主",
kTextPrimary,
18.0f);
drawList.AddText(
UIPoint(viewportRect.x + 18.0f, viewportRect.y + 48.0f),
"这里只保留一个空白宿主区域,专门看底部 StatusBar 的对齐和交互",
"这里只放一个宿主区域,用来观察底部 StatusBar 的布局和状态变化",
kTextMuted,
12.0f);

View File

@@ -2,10 +2,10 @@
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorTabStripInteraction.h>
#include <XCEditor/Core/UIEditorPanelRegistry.h>
#include <XCEditor/Core/UIEditorWorkspaceController.h>
#include <XCEditor/Widgets/UIEditorTabStrip.h>
#include <XCEditor/Collections/UIEditorTabStripInteraction.h>
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
#include <XCEditor/Collections/UIEditorTabStrip.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -79,7 +79,6 @@ constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f);
constexpr UIColor kTextMuted(0.73f, 0.73f, 0.73f, 1.0f);
constexpr UIColor kTextWeak(0.58f, 0.58f, 0.58f, 1.0f);
constexpr UIColor kSuccess(0.62f, 0.74f, 0.62f, 1.0f);
constexpr UIColor kWarning(0.78f, 0.70f, 0.46f, 1.0f);
constexpr UIColor kDanger(0.82f, 0.48f, 0.48f, 1.0f);
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
constexpr UIColor kButtonHoveredBg(0.31f, 0.31f, 0.31f, 1.0f);
@@ -402,7 +401,7 @@ private:
m_tabStripFrame = {};
m_tabItems.clear();
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_lastResult = "等待操作";
m_lastResult = "已重置到默认标签状态";
}
UIRect GetTabStripRect() const {
@@ -524,7 +523,7 @@ private:
if (result.hitTarget.kind == UIEditorTabStripHitTargetKind::HeaderBackground ||
result.hitTarget.kind == UIEditorTabStripHitTargetKind::Content) {
m_lastResult = "TabStrip 获得 focus";
m_lastResult = "命中 TabStrip 背景,保留 focus";
return;
}
@@ -577,7 +576,6 @@ private:
m_tabItems.push_back(std::move(item));
}
}
}
void RenderFrame() {
@@ -610,30 +608,30 @@ private:
DrawCard(
drawList,
introRect,
"测试功能Editor TabStrip 基础层",
"验证 TabStrip header / hit-test / selection / close / keyboard不包含任何业务面板。");
"这个测试验证什么功能?",
"验证 TabStrip header 命中、选中切换、关闭请求和键盘导航,不接业务面板。");
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 68.0f),
"重点检查tab 布局是否整齐selected / hover / focus 是否正确Close 后 active panel 是否回退正确",
"1. 点击 tab检查 selected / active panel 是否同步",
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 92.0f),
"操作:点击 tab 切换;点击 X 关闭Document C 没有 X点击内容区后按 Left / Right / Home / EndReset 恢复F12 截图",
"2. 点击 X只验证关闭请求Document C 没有关闭按钮",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 116.0f),
"预期Close 命中优先于 tab键盘导航只在 focused 时生效Document C 始终保留",
"3. Left / Right / Home / End 验证键盘导航;按重置恢复初始状态",
kTextWeak,
12.0f);
DrawCard(
drawList,
stateRect,
"状态回显",
"这里直接回显 hover / focus / active panel / result方便人工检查");
DrawButton(drawList, m_resetButtonRect, "Reset", m_resetButtonHovered);
"状态摘要",
"持续显示 hoverfocusactive panel、tabs、result 和校验结果");
DrawButton(drawList, m_resetButtonRect, "重置", m_resetButtonHovered);
const std::size_t selectedIndex =
ResolveUIEditorTabStripSelectedIndex(m_tabItems, m_controller.GetWorkspace().activePanelId);
@@ -676,7 +674,7 @@ private:
12.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 276.0f),
validation.IsValid() ? "Validation: OK" : "Validation: " + validation.message,
validation.IsValid() ? "校验: OK" : "校验: " + validation.message,
validation.IsValid() ? kSuccess : kDanger,
12.0f);
@@ -696,7 +694,7 @@ private:
drawList,
previewCardRect,
"预览区",
"这里只放一个 TabStrip 和一个 content placeholder避免试验面板过杂");
"这里只放一个 TabStrip 和一个 content placeholder用来观察 header 与 content frame");
AppendUIEditorTabStripBackground(drawList, m_layout, m_tabStripState);
AppendUIEditorTabStripForeground(drawList, m_layout, m_tabItems, m_tabStripState);
@@ -716,12 +714,12 @@ private:
13.0f);
drawList.AddText(
UIPoint(m_layout.contentRect.x + 20.0f, m_layout.contentRect.y + 76.0f),
"个区域只用于验证 TabStrip content frame 与 focus,不承载业务内容。",
"里只验证 TabStrip content frame 与 focus 同步,不接业务内容。",
kTextWeak,
12.0f);
drawList.AddText(
UIPoint(m_layout.contentRect.x + 20.0f, m_layout.contentRect.y + 100.0f),
"检查点:关闭 Document B 后,应自动回退到相邻 tabDocument C 无法关闭",
"可点击 Document B 切换,或点击 Document C 验证不可关闭 tab",
kTextWeak,
12.0f);

View File

@@ -1,10 +1,11 @@
#ifndef NOMINMAX
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorTextFieldInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorTextField.h>
#include <XCEditor/Fields/UIEditorTextFieldInteraction.h>
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Fields/UIEditorTextField.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -25,6 +26,8 @@
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
// 场景说明:验证固定样式下 TextField 的基础编辑、提交、取消与焦点切换。
namespace {
using XCEngine::Input::KeyCode;
@@ -46,7 +49,6 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorTextFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | TextField Basic";
@@ -83,10 +85,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
@@ -157,18 +155,16 @@ ScenarioLayout BuildScenarioLayout(
return layout;
}
XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics ResolveHostedTextFieldMetrics(
const Style::UITheme& theme) {
const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(theme);
const auto textMetrics = XCEngine::UI::Editor::ResolveUIEditorTextFieldMetrics(theme);
return XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(propertyMetrics, textMetrics);
XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics ResolvePropertyGridTextFieldMetrics() {
return XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldMetrics(
XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(),
XCEngine::UI::Editor::ResolveUIEditorTextFieldMetrics());
}
XCEngine::UI::Editor::Widgets::UIEditorTextFieldPalette ResolveHostedTextFieldPalette(
const Style::UITheme& theme) {
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(theme);
const auto textPalette = XCEngine::UI::Editor::ResolveUIEditorTextFieldPalette(theme);
return XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette(propertyPalette, textPalette);
XCEngine::UI::Editor::Widgets::UIEditorTextFieldPalette ResolvePropertyGridTextFieldPalette() {
return XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldPalette(
XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(),
XCEngine::UI::Editor::ResolveUIEditorTextFieldPalette());
}
void DrawCard(
@@ -427,14 +423,6 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/text_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
@@ -463,7 +451,7 @@ private:
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
void ResetScenario() {
@@ -487,7 +475,7 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolveHostedTextFieldMetrics(m_theme);
const auto metrics = ResolvePropertyGridTextFieldMetrics();
m_frame = UpdateUIEditorTextFieldInteraction(
m_interactionState,
m_spec,
@@ -605,7 +593,7 @@ private:
UIEditorTextFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolveHostedTextFieldMetrics(m_theme);
const auto metrics = ResolvePropertyGridTextFieldMetrics();
m_frame = UpdateUIEditorTextFieldInteraction(
m_interactionState,
m_spec,
@@ -661,16 +649,16 @@ private:
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
const UIEditorTextFieldHitTarget currentHit =
HitTestUIEditorTextField(m_frame.layout, m_mousePosition);
const auto textMetrics = ResolveHostedTextFieldMetrics(m_theme);
const auto textPalette = ResolveHostedTextFieldPalette(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
const auto textMetrics = ResolvePropertyGridTextFieldMetrics();
const auto textPalette = ResolvePropertyGridTextFieldPalette();
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorTextFieldBasic");
@@ -681,8 +669,8 @@ private:
layout.introRect,
shellPalette,
shellMetrics,
"这个测试验证什么功能",
"验证 UIEditorTextField 的基础编辑交互契约,不涉及 PropertyGrid 或业务 Inspector。");
"这个测试验证什么功能",
"验证 UIEditorTextField 的基础编辑契约,不涉及 PropertyGrid 或任何 Inspector 业务逻辑");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 value box检查是否进入编辑态。",
@@ -775,7 +763,7 @@ private:
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
"Theme: " + m_themeStatus,
"Style: fixed",
shellPalette.textWeak,
shellMetrics.bodyFontSize);
@@ -785,7 +773,7 @@ private:
shellPalette,
shellMetrics,
"TextField 预览",
"这里只放一个 Editor TextField用来验证基础字段行为。");
"这里只放一个固定样式 TextField用来验证基础字段行为。");
drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor);
drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f);
drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground);
@@ -827,12 +815,10 @@ private:
UIEditorTextFieldSpec m_spec = {};
UIEditorTextFieldInteractionState m_interactionState = {};
UIEditorTextFieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
std::string m_themeStatus = "fallback";
};
} // namespace

View File

@@ -684,8 +684,8 @@ private:
DrawCard(
drawList,
layout.introRect,
"这个测试验证什么功能?",
"只验证 Editor TreeView 的单选、层级展开/折叠和键盘导航契约,不涉及任何业务面板。");
"这个测试验证什么功能?",
"只验证 Editor TreeView 的单选、层级展开/折叠和键盘导航不涉及任何业务面板。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 row只切换 selectionhover / selected / focused 必须能明确区分。",
@@ -708,7 +708,7 @@ private:
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 点击树外空白清除 focusF12 手动截图XCUI_AUTO_CAPTURE_ON_STARTUP=1 自动截图。",
"5. 点击树外空白清除 focusF12 手动截图XCUI_AUTO_CAPTURE_ON_STARTUP=1 可启动自动截图。",
kTextPrimary,
12.0f);

View File

@@ -5,6 +5,7 @@
#include <XCEditor/Collections/UIEditorInlineRenameSession.h>
#include <XCEditor/Collections/UIEditorTreeView.h>
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Fields/UIEditorTextField.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include "EditorValidationTheme.h"
@@ -13,7 +14,6 @@
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Style/Theme.h>
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h>
@@ -32,6 +32,7 @@
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
// 场景说明:验证固定样式下 TreeView 的 inline rename 启动、提交、取消与外部点击提交。
namespace {
using XCEngine::Input::KeyCode;
@@ -46,8 +47,8 @@ using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Widgets::UIExpansionModel;
using XCEngine::UI::Widgets::UISelectionModel;
using XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics;
using XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette;
using XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldMetrics;
using XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldPalette;
using XCEngine::UI::Editor::BuildUIEditorInlineRenameTextFieldMetrics;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
@@ -75,8 +76,6 @@ using XCEngine::UI::Editor::Widgets::UIEditorTreeViewInvalidIndex;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewItem;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldPalette;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorTreeViewInlineRenameValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | TreeView Inline Rename";
@@ -93,7 +92,6 @@ struct ScenarioLayout {
};
std::filesystem::path ResolveRepoRootPath();
std::filesystem::path ResolveValidationThemePath();
bool ContainsPoint(const UIRect& rect, float x, float y);
std::int32_t MapTreeKey(UINT keyCode);
ScenarioLayout BuildScenarioLayout(float width, float height, const EditorValidationShellMetrics& shellMetrics);
@@ -143,8 +141,6 @@ private:
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
Style::UITheme m_theme = {};
std::string m_themeStatus = "fallback";
std::vector<UIEditorTreeViewItem> m_items = {};
UISelectionModel m_selectionModel = {};
UIExpansionModel m_expansionModel = {};
@@ -168,10 +164,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme").lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
}
@@ -345,13 +337,6 @@ bool ScenarioApp::Initialize(HINSTANCE hInstance, int nCmdShow) {
}
m_captureRoot = ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/tree_view_inline_rename/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad = XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
}
@@ -374,7 +359,7 @@ ScenarioLayout ScenarioApp::GetLayout() const {
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
return BuildScenarioLayout(width, height, XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
return BuildScenarioLayout(width, height, XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
void ScenarioApp::ResetScenario() {
@@ -718,9 +703,9 @@ UIEditorInlineRenameSessionRequest ScenarioApp::BuildRenameRequest(bool beginSes
}
UIEditorTextFieldMetrics ScenarioApp::ResolveHostedTextFieldMetrics() const {
const auto propertyMetrics = ResolveUIEditorPropertyGridMetrics(m_theme);
const auto textMetrics = ResolveUIEditorTextFieldMetrics(m_theme);
return BuildUIEditorHostedTextFieldMetrics(propertyMetrics, textMetrics);
const auto propertyMetrics = ResolveUIEditorPropertyGridMetrics();
const auto textMetrics = ResolveUIEditorTextFieldMetrics();
return BuildUIEditorPropertyGridTextFieldMetrics(propertyMetrics, textMetrics);
}
UIEditorTextFieldMetrics ScenarioApp::ResolveInlineRenameMetrics(const UIRect& bounds) const {
@@ -728,9 +713,9 @@ UIEditorTextFieldMetrics ScenarioApp::ResolveInlineRenameMetrics(const UIRect& b
}
UIEditorTextFieldPalette ScenarioApp::ResolveInlineRenamePalette() const {
const auto propertyPalette = ResolveUIEditorPropertyGridPalette(m_theme);
const auto textPalette = ResolveUIEditorTextFieldPalette(m_theme);
return BuildUIEditorHostedTextFieldPalette(propertyPalette, textPalette);
const auto propertyPalette = ResolveUIEditorPropertyGridPalette();
const auto textPalette = ResolveUIEditorTextFieldPalette();
return BuildUIEditorPropertyGridTextFieldPalette(propertyPalette, textPalette);
}
void ScenarioApp::ExecuteAction(ActionId action) {
@@ -755,9 +740,9 @@ void ScenarioApp::RenderFrame() {
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const auto shellMetrics =
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette =
XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshTreeFrame();
@@ -781,8 +766,8 @@ void ScenarioApp::RenderFrame() {
layout.introRect,
shellPalette,
shellMetrics,
"这个测试验证什么功能?",
"只验证 Editor TreeView 的 inline rename 契约默认进入编辑、字符编辑、Enter 提交、Esc 取消、点击外部提交。");
"这个测试验证什么功能?",
"只验证 Editor TreeView 的 inline rename默认进入编辑、字符编辑、Enter 提交、Esc 取消、点击外部提交。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 启动后默认进入 hero-mesh rename输入框只能覆盖 label 区。",
@@ -790,7 +775,7 @@ void ScenarioApp::RenderFrame() {
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 输入字符后Draft 必须实时变化原标签不能和 overlay 叠字。",
"2. 输入字符后Draft 必须实时变化原标签不能和 overlay 叠字。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
@@ -805,7 +790,7 @@ void ScenarioApp::RenderFrame() {
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. rename 中点击输入框外部:提交当前草稿并退出编辑态。",
"5. rename 中点击输入框外部:提交当前草稿并退出编辑态。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
@@ -903,10 +888,9 @@ void ScenarioApp::RenderFrame() {
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 334.0f),
"Theme: " + m_themeStatus,
"Style: fixed",
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,

View File

@@ -810,7 +810,7 @@ private:
DrawCard(
drawList,
layout.introRect,
"这个测试验证什么功能?",
"这个测试验证什么功能?",
"只验证 Editor TreeView 的多选契约,不混入 Hierarchy / Inspector 业务面板。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
@@ -819,12 +819,12 @@ private:
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. Ctrl+单击 / Shift+单击:只验证 visible tree 范围内的多选切换与连续扩选。",
"2. Ctrl+单击 / Shift+单击:只验证 visible tree 范围内的多选切换与连续扩选。",
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 点击 disclosure只切换 expanded不应无故打散 selection。",
"3. 点击 disclosure只切换 expanded不应无故打散 selection。",
kTextPrimary,
12.0f);
drawList.AddText(
@@ -853,7 +853,7 @@ private:
DrawButton(drawList, button, m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(drawList, layout.stateRect, "状态摘要", "重点检查 multi-select 与 expanded / visible contract 是否稳定。");
DrawCard(drawList, layout.stateRect, "状态摘要", "重点检查 multi-select 与 expanded / visible 契约是否稳定。");
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hit: " + DescribeHitTarget(currentHit, m_items),

View File

@@ -1,10 +1,10 @@
#ifndef NOMINMAX
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Core/UIEditorVector2FieldInteraction.h>
#include <XCEditor/Widgets/UIEditorVector2Field.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorVector2FieldInteraction.h>
#include <XCEditor/Fields/UIEditorVector2Field.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -48,8 +48,6 @@ using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldInvalidComponentIndex;
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector2FieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector2Field Basic";
@@ -85,11 +83,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -437,14 +430,6 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/vector2_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
@@ -473,7 +458,7 @@ private:
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
void ResetScenario() {
@@ -503,7 +488,7 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme);
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics();
m_frame = UpdateUIEditorVector2FieldInteraction(
m_interactionState,
m_spec,
@@ -623,7 +608,7 @@ private:
UIEditorVector2FieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme);
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics();
m_frame = UpdateUIEditorVector2FieldInteraction(
m_interactionState,
m_spec,
@@ -635,12 +620,12 @@ private:
void UpdateResultText(const UIEditorVector2FieldInteractionResult& result) {
if (result.editCommitRejected) {
m_lastResult = "提交失败,当前文本不是合法数字";
m_lastResult = "提交被拒绝:当前输入不是合法数字";
return;
}
if (result.editCommitted) {
m_lastResult =
std::string("已提交 ") +
std::string("已提交编辑: ") +
DescribeSelectedComponent(result.changedComponentIndex) +
" = " + result.committedText;
return;
@@ -657,7 +642,7 @@ private:
}
if (result.stepApplied || result.valueChanged) {
m_lastResult =
std::string("值已更新,当前 component = ") +
std::string("值已更新,当前 component = ") +
DescribeSelectedComponent(result.changedComponentIndex);
return;
}
@@ -669,15 +654,15 @@ private:
}
if (result.focusChanged) {
m_lastResult =
std::string("焦点变化: ") +
std::string("焦点状态: ") +
(m_interactionState.vector2FieldState.focused ? "focused" : "lost");
return;
}
if (result.consumed) {
m_lastResult = "控件已消费输入";
m_lastResult = "输入已处理,但没有额外状态变化";
return;
}
m_lastResult = "等待交互";
m_lastResult = "无变化";
}
void ExecuteAction(ActionId action) {
@@ -702,16 +687,16 @@ private:
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
const UIEditorVector2FieldHitTarget currentHit =
HitTestUIEditorVector2Field(m_frame.layout, m_mousePosition);
const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme);
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector2FieldPalette(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics();
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector2FieldPalette();
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector2FieldBasic");
@@ -722,36 +707,36 @@ private:
layout.introRect,
shellPalette,
shellMetrics,
"这个测试验证什么功能",
"验证 UIEditorVector2Field 的双通道数值编辑契约,不涉及 PropertyGrid 或业务 Inspector。");
"这个测试验证什么功能",
"验证 UIEditorVector2Field 的选择切换、键盘步进、编辑提交/取消,以及固定 Inspector 风格承载");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 X / Y 对应的 value box,检查 selected component 是否切换,并且应进入编辑态",
"1. 点击 X / Y value box 可切换 selected component,并检查 hover 与高亮",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 获得 focus 后按 Tab,检查 selected component 在 X / Y 之间切换",
"2. 获得 focus 后按 Tab 在 X / Y 之间切换 selected component。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 非编辑态按 Up / Down / Home / End,检查当前 component step / 边界行为",
"3. 不进入编辑时,Up / Down / Home / End 会对当前 component step 或边界跳转",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 按 Enter 开始编辑直接输入字符也应开始编辑Enter commitEscape cancel",
"4. 按 Enter 开始编辑,输入后再按 Enter 提交,按 Escape 取消",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 编辑态按 F6 模拟 FocusLost应提交暂存文本并退出编辑态",
"5. 按 F6 模拟 FocusLost检查未提交编辑是否按约定结束",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. 检查 Hover / Selected / Editing / Values / Result 是否同步更新",
"6. 观察 Hover / Selected / Editing / Values / Result 是否同步。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
@@ -826,19 +811,13 @@ private:
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"Vector2Field 预览",
"这里只放一个 Unity 风格的双通道 Vector2 字段");
"这里只放一个固定样式的 Vector2 输入项");
drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor);
drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f);
drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground);
@@ -880,12 +859,10 @@ private:
UIEditorVector2FieldSpec m_spec = {};
UIEditorVector2FieldInteractionState m_interactionState = {};
UIEditorVector2FieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = "等待交互";
std::string m_themeStatus = "fallback";
std::string m_lastResult = "无变化";
};
} // namespace

View File

@@ -1,10 +1,10 @@
#ifndef NOMINMAX
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Core/UIEditorVector3FieldInteraction.h>
#include <XCEditor/Widgets/UIEditorVector3Field.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorVector3FieldInteraction.h>
#include <XCEditor/Fields/UIEditorVector3Field.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -48,8 +48,6 @@ using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldInvalidComponentIndex;
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector3FieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector3Field Basic";
@@ -85,11 +83,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -443,14 +436,6 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/vector3_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
@@ -479,7 +464,7 @@ private:
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
}
void ResetScenario() {
@@ -509,7 +494,7 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme);
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics();
m_frame = UpdateUIEditorVector3FieldInteraction(
m_interactionState,
m_spec,
@@ -629,7 +614,7 @@ private:
UIEditorVector3FieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme);
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics();
m_frame = UpdateUIEditorVector3FieldInteraction(
m_interactionState,
m_spec,
@@ -641,12 +626,12 @@ private:
void UpdateResultText(const UIEditorVector3FieldInteractionResult& result) {
if (result.editCommitRejected) {
m_lastResult = "提交失败,当前文本不是合法数字";
m_lastResult = "提交被拒绝:当前输入不是合法数字";
return;
}
if (result.editCommitted) {
m_lastResult =
std::string("已提交 ") +
std::string("已提交编辑: ") +
DescribeSelectedComponent(result.changedComponentIndex) +
" = " + result.committedText;
return;
@@ -663,7 +648,7 @@ private:
}
if (result.stepApplied || result.valueChanged) {
m_lastResult =
std::string("值已更新,当前 component = ") +
std::string("值已更新,当前 component = ") +
DescribeSelectedComponent(result.changedComponentIndex);
return;
}
@@ -675,15 +660,15 @@ private:
}
if (result.focusChanged) {
m_lastResult =
std::string("焦点变化: ") +
std::string("焦点状态: ") +
(m_interactionState.vector3FieldState.focused ? "focused" : "lost");
return;
}
if (result.consumed) {
m_lastResult = "控件已消费输入";
m_lastResult = "输入已处理,但没有额外状态变化";
return;
}
m_lastResult = "等待交互";
m_lastResult = "无变化";
}
void ExecuteAction(ActionId action) {
@@ -708,16 +693,16 @@ private:
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
const UIEditorVector3FieldHitTarget currentHit =
HitTestUIEditorVector3Field(m_frame.layout, m_mousePosition);
const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme);
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector3FieldPalette(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics();
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector3FieldPalette();
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector3FieldBasic");
@@ -728,36 +713,36 @@ private:
layout.introRect,
shellPalette,
shellMetrics,
"这个测试验证什么功能",
"验证 UIEditorVector3Field 的三通道数值编辑契约,不涉及 PropertyGrid 或业务 Inspector。");
"这个测试验证什么功能",
"验证 UIEditorVector3Field 的选择切换、键盘步进、编辑提交/取消,以及固定 Inspector 风格承载");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 X / Y / Z 对应的 value box,检查 selected component 是否切换,并应进入编辑态",
"1. 点击 X / Y / Z value box 可切换 selected component,并检查 hover 与高亮",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 获得 focus 后按 Tab,检查 selected component 在 X / Y / Z 之间切换",
"2. 获得 focus 后按 Tab 在 X / Y / Z 之间切换 selected component。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 非编辑态按 Up / Down / Home / End,检查当前 component step / 边界行为",
"3. 不进入编辑时,Up / Down / Home / End 会对当前 component step 或边界跳转",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 按 Enter 开始编辑直接输入字符也应开始编辑Enter commitEscape cancel",
"4. 按 Enter 开始编辑,输入后再按 Enter 提交,按 Escape 取消",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 编辑态按 F6 模拟 FocusLost应提交暂存文本并退出编辑态",
"5. 按 F6 模拟 FocusLost检查未提交编辑是否按约定结束",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. 检查 Hover / Selected / Editing / Values / Result 是否同步更新",
"6. 观察 Hover / Selected / Editing / Values / Result 是否同步。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
@@ -834,19 +819,13 @@ private:
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"Vector3Field 预览",
"这里只放一个 Unity 风格的三通道 Vector3 字段");
"这里只放一个固定样式的 Vector3 输入项");
drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor);
drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f);
drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground);
@@ -888,12 +867,10 @@ private:
UIEditorVector3FieldSpec m_spec = {};
UIEditorVector3FieldInteractionState m_interactionState = {};
UIEditorVector3FieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = "等待交互";
std::string m_themeStatus = "fallback";
std::string m_lastResult = "无变化";
};
} // namespace

View File

@@ -1,10 +1,10 @@
#ifndef NOMINMAX
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Core/UIEditorVector4FieldInteraction.h>
#include <XCEditor/Widgets/UIEditorVector4Field.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorVector4FieldInteraction.h>
#include <XCEditor/Fields/UIEditorVector4Field.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -46,8 +46,6 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector4Field;
using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldSpec;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector4FieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector4Field Basic";
@@ -69,11 +67,6 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
std::int32_t MapVectorKey(UINT keyCode) {
switch (keyCode) {
case VK_LEFT: return static_cast<std::int32_t>(KeyCode::Left);
@@ -304,9 +297,6 @@ private:
UpdateWindow(m_hwnd);
m_captureRoot = ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/vector4_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad = XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
m_theme = themeLoad.theme;
m_themeStatus = themeLoad.succeeded ? "loaded" : (themeLoad.error.empty() ? "fallback" : themeLoad.error);
ResetScenario();
return true;
}
@@ -344,8 +334,8 @@ private:
const auto layout = BuildScenarioLayout(
static_cast<float>((std::max)(1L, clientRect.right - clientRect.left)),
static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top)),
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector4FieldMetrics(m_theme);
XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics());
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector4FieldMetrics();
m_frame = UpdateUIEditorVector4FieldInteraction(
m_interactionState,
m_spec,
@@ -357,21 +347,21 @@ private:
void UpdateResultText(const UIEditorVector4FieldInteractionResult& result) {
if (result.editCommitRejected) {
m_lastResult = "提交失败,当前文本不是合法数字";
m_lastResult = "提交被拒绝:当前输入不是合法数字";
} else if (result.editCommitted) {
m_lastResult = "已提交 " + DescribeSelectedComponent(result.changedComponentIndex) + " = " + result.committedText;
m_lastResult = "已提交编辑: " + DescribeSelectedComponent(result.changedComponentIndex) + " = " + result.committedText;
} else if (result.editCanceled) {
m_lastResult = "已取消编辑";
} else if (result.editStarted) {
m_lastResult = "开始编辑 component " + DescribeSelectedComponent(result.selectedComponentIndex);
} else if (result.stepApplied || result.valueChanged) {
m_lastResult = "值已更新,当前 component = " + DescribeSelectedComponent(result.changedComponentIndex);
m_lastResult = "值已更新,当前 component = " + DescribeSelectedComponent(result.changedComponentIndex);
} else if (result.selectionChanged) {
m_lastResult = "已切换选中 component: " + DescribeSelectedComponent(result.selectedComponentIndex);
} else if (result.focusChanged) {
m_lastResult = std::string("焦点变化: ") + (m_interactionState.vector4FieldState.focused ? "focused" : "lost");
m_lastResult = std::string("焦点状态: ") + (m_interactionState.vector4FieldState.focused ? "focused" : "lost");
} else if (result.consumed) {
m_lastResult = "控件已消费输入";
m_lastResult = "输入已处理,但没有额外状态变化";
}
}
@@ -380,14 +370,14 @@ private:
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const auto shellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const auto layout = BuildScenarioLayout(width, height, shellMetrics);
PumpEvents({});
const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector4FieldMetrics(m_theme);
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector4FieldPalette(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector4FieldMetrics();
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector4FieldPalette();
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette();
const auto currentHit = HitTestUIEditorVector4Field(m_frame.layout, m_mousePosition);
UIDrawData drawData = {};
@@ -396,12 +386,12 @@ private:
drawList.AddFilledRect(layout.introRect, shellPalette.cardBackground, shellMetrics.cardRadius);
drawList.AddRectOutline(layout.introRect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 14.0f), "这个测试验证什么功能?", shellPalette.textPrimary, shellMetrics.titleFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 40.0f), "验证 UIEditorVector4Field 的四通道编辑契约", shellPalette.textMuted, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f), "1. 点击 X/Y/Z/W 的 value box检查 selected component 和 editing", shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), "2. 按 Tab/Shift+Tab 切换 componentUp/Down/Home/End 检查 step 与边界。", shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), "3. Enter 开始编辑直接输入字符也应开始编辑Enter commitEscape cancel", shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), "4. F6 模拟 FocusLostF12 截图R 重置当前测试。", shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 14.0f), "这个测试验证什么功能?", shellPalette.textPrimary, shellMetrics.titleFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 40.0f), "验证 UIEditorVector4Field 的四分量切换、键盘步进、编辑提交/取消,以及固定 Inspector 风格展示", shellPalette.textMuted, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f), "1. 点击 X / Y / Z / W 的 value box可切换 selected component。", shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), "2. 按 Tab / Shift+Tab 切换 componentUp / Down / Home / End 检查 step 与边界。", shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), "3. Enter 开始编辑直接输入字符也应开始编辑Enter 提交Escape 取消", shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), "4. F6 模拟 FocusLost,按 F12 截图,按 R 重置当前测试。", shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f), "5. 重点检查 Hover / Selected / Editing / Values / Result 是否同步。", shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddFilledRect(layout.stateRect, shellPalette.cardBackground, shellMetrics.cardRadius);
@@ -414,13 +404,12 @@ private:
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f), "Values: X=" + FormatUIEditorVector4FieldComponentValue(m_spec, 0u) + " Y=" + FormatUIEditorVector4FieldComponentValue(m_spec, 1u) + " Z=" + FormatUIEditorVector4FieldComponentValue(m_spec, 2u) + " W=" + FormatUIEditorVector4FieldComponentValue(m_spec, 3u), shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f), "Display: X=" + m_interactionState.vector4FieldState.displayTexts[0] + " Y=" + m_interactionState.vector4FieldState.displayTexts[1] + " Z=" + m_interactionState.vector4FieldState.displayTexts[2] + " W=" + m_interactionState.vector4FieldState.displayTexts[3], shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f), "Result: " + m_lastResult, shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f), "Capture: " + (m_autoScreenshot.GetLastCaptureSummary().empty() ? std::string("F12 -> captures/") : m_autoScreenshot.GetLastCaptureSummary()), shellPalette.textWeak, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f), "Theme: " + m_themeStatus, shellPalette.textWeak, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f), "截图: " + (m_autoScreenshot.GetLastCaptureSummary().empty() ? std::string("F12 -> captures/") : m_autoScreenshot.GetLastCaptureSummary()), shellPalette.textWeak, shellMetrics.bodyFontSize);
drawList.AddFilledRect(layout.previewRect, shellPalette.cardBackground, shellMetrics.cardRadius);
drawList.AddRectOutline(layout.previewRect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
drawList.AddText(UIPoint(layout.previewRect.x + 16.0f, layout.previewRect.y + 14.0f), "Vector4Field 预览", shellPalette.textPrimary, shellMetrics.titleFontSize);
drawList.AddText(UIPoint(layout.previewRect.x + 16.0f, layout.previewRect.y + 40.0f), "这里只放一个 Unity 风格的四通道字段", shellPalette.textMuted, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.previewRect.x + 16.0f, layout.previewRect.y + 40.0f), "这里只放一个固定样式的 Vector4 输入项", shellPalette.textMuted, shellMetrics.bodyFontSize);
drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor);
drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f);
drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground);
@@ -443,10 +432,8 @@ private:
UIEditorVector4FieldSpec m_spec = {};
UIEditorVector4FieldInteractionState m_interactionState = {};
UIEditorVector4FieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
std::string m_lastResult = "等待交互";
std::string m_themeStatus = "fallback";
std::string m_lastResult = "无变化";
};
} // namespace

View File

@@ -2,8 +2,8 @@
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorViewportShell.h>
#include <XCEditor/Widgets/UIEditorViewportSlot.h>
#include <XCEditor/Shell/UIEditorViewportShell.h>
#include <XCEditor/Shell/UIEditorViewportSlot.h>
#include "Host/AutoScreenshot.h"
#include "Host/InputModifierTracker.h"
#include "Host/NativeRenderer.h"
@@ -506,7 +506,7 @@ private:
UIEditorViewportShellSpec BuildShellSpec() const {
UIEditorViewportShellSpec spec = {};
spec.chrome.title = "Scene View";
spec.chrome.subtitle = "ViewportShell 基础层";
spec.chrome.subtitle = "ViewportShell 基础";
spec.chrome.showTopBar = m_showTopBar;
spec.chrome.showBottomBar = m_showBottomBar;
spec.chrome.topBarHeight = 40.0f;
@@ -565,25 +565,25 @@ private:
m_buttons = {
{
ActionId::ToggleTopBar,
std::string("TopBar: ") + (m_showTopBar ? "" : ""),
std::string("TopBar: ") + (m_showTopBar ? "" : ""),
UIRect(left, top, widthAvailable, buttonHeight),
m_showTopBar
},
{
ActionId::ToggleBottomBar,
std::string("BottomBar: ") + (m_showBottomBar ? "" : ""),
std::string("BottomBar: ") + (m_showBottomBar ? "" : ""),
UIRect(left, top + (buttonHeight + gap), widthAvailable, buttonHeight),
m_showBottomBar
},
{
ActionId::ToggleTexture,
std::string("Texture: ") + (m_textureEnabled ? "" : ""),
std::string("Texture: ") + (m_textureEnabled ? "" : ""),
UIRect(left, top + (buttonHeight + gap) * 2.0f, widthAvailable, buttonHeight),
m_textureEnabled
},
{
ActionId::Reset,
"Reset",
"重置",
UIRect(left, top + (buttonHeight + gap) * 3.0f, widthAvailable, buttonHeight),
false
},
@@ -687,41 +687,41 @@ private:
DrawCard(
drawList,
m_introRect,
"测试功能ViewportShell 基础 contract",
"只验证 Resolve + Update不接 Scene/Game 业务面板");
"这个测试验证什么功能?",
"只验证 Resolve + Update 的 ViewportShell contract,不接 Scene/Game 业务。");
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 66.0f),
"重点检查:切换 TopBar / BottomBar 后,Request Size Input Rect 要同步变化",
"1. 验证 TopBar / BottomBar Request Size Input Rect 的影响",
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 88.0f),
"重点检查:Hover / Focus / Capture 要和右侧 surface 边框状态一致",
"2. 验证 Hover / Focus / Capture 是否与 surface 输入桥同步",
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 110.0f),
"重点检查:Texture Fallback 只影响 frame 分支,不应破坏 shell 输入边界",
"3. 验证 Texture / Fallback 分支切换后frame 输出仍由 shell 承接",
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 132.0f),
"操作:hover Surfaceclick 获取 Focus按住拖出再松开检查 Capture。",
"4. 验证 hover Surfaceclick Surface 后的 Focus / Capture 变化",
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 154.0f),
"操作:点击左侧 TopBar / BottomBar / Texture,观察左侧状态与右侧布局",
"5. 验证 TopBar / BottomBar / Texture 三个开关切换后的整体表现",
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 176.0f),
"操作:需要保留当前画面时点“截图”,或直接按 F12",
"6. 验证截图链路,支持按钮触发和 F12 手动截图",
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 204.0f),
"结果判定:左侧 Request / Input / HoverHit / Hover / Focus / Capture 必须一致",
"7. 左侧持续展示 Request / Input / HoverHit / Hover / Focus / Capture 摘要",
kTextWeak,
11.0f);
@@ -745,18 +745,18 @@ private:
addStateLine("Hover Hit: " + DescribeHitTarget(m_hoverHit), kTextPrimary);
addStateLine("Hover: " + BoolText(m_shellFrame.inputFrame.hovered), kTextPrimary);
addStateLine("Focus: " + BoolText(m_shellFrame.inputFrame.focused), kTextPrimary);
addStateLine("Capture: " + BoolText(m_shellFrame.inputFrame.captured), kTextPrimary);
addStateLine("捕获: " + BoolText(m_shellFrame.inputFrame.captured), kTextPrimary);
addStateLine("Result: " + m_lastResult, kTextMuted);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队中..."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("截图F12 或按钮 -> viewport_shell_basic/captures/")
? std::string("截图: F12 或按钮 -> viewport_shell_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
addStateLine(captureSummary, kTextWeak, 11.0f);
DrawCard(drawList, m_previewRect, "Preview", "这里只有一个 ViewportShell用来检 Editor 基础层 compose。");
DrawCard(drawList, m_previewRect, "Preview", "这里只有一个 ViewportShell用来检 Editor 基础层 compose。");
drawList.AddFilledRect(
UIRect(
m_previewRect.x + 12.0f,

View File

@@ -2,7 +2,7 @@
#define NOMINMAX
#endif
#include <XCEditor/Widgets/UIEditorViewportSlot.h>
#include <XCEditor/Shell/UIEditorViewportSlot.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -322,7 +322,7 @@ private:
UIEditorViewportSlotChrome BuildChrome() const {
UIEditorViewportSlotChrome chrome = {};
chrome.title = "Scene View";
chrome.subtitle = "ViewportSlot 基础壳层";
chrome.subtitle = "ViewportSlot 基础布局验证";
chrome.showTopBar = m_showTopBar;
chrome.showBottomBar = m_showBottomBar;
chrome.topBarHeight = 40.0f;
@@ -341,7 +341,7 @@ private:
frame.statusText = "Fake viewport frame";
} else {
frame.hasTexture = false;
frame.statusText = "当前无纹理,检查 fallback message 与 input 边界";
frame.statusText = "当前无纹理,检查 fallback message 与 input 区域";
}
return frame;
}
@@ -414,10 +414,10 @@ private:
const float width = m_controlsRect.width - 32.0f;
m_buttons = {
{ ActionId::ToggleTopBar, "TopBar", UIRect(left, top, width, buttonHeight), m_showTopBar },
{ ActionId::ToggleBottomBar, "状态条", UIRect(left, top + (buttonHeight + gap), width, buttonHeight), m_showBottomBar },
{ ActionId::ToggleBottomBar, "BottomBar", UIRect(left, top + (buttonHeight + gap), width, buttonHeight), m_showBottomBar },
{ ActionId::ToggleTexture, "Texture", UIRect(left, top + (buttonHeight + gap) * 2.0f, width, buttonHeight), m_textureEnabled },
{ ActionId::ToggleSquareAspect, "方形比例", UIRect(left, top + (buttonHeight + gap) * 3.0f, width, buttonHeight), m_squareAspect },
{ ActionId::Reset, "Reset", UIRect(left, top + (buttonHeight + gap) * 4.0f, width, buttonHeight), false },
{ ActionId::Reset, "重置", UIRect(left, top + (buttonHeight + gap) * 4.0f, width, buttonHeight), false },
{ ActionId::Capture, "截图", UIRect(left, top + (buttonHeight + gap) * 5.0f, width, buttonHeight), false }
};
}
@@ -480,7 +480,7 @@ private:
m_slotState.focused = true;
m_slotState.surfaceActive = true;
m_slotState.inputCaptured = true;
m_lastResult = "Surface 按下focus + active + capture";
m_lastResult = "Surface 按下,进入 focus + active + capture";
}
InvalidateScenario();
}
@@ -493,7 +493,7 @@ private:
break;
case ActionId::ToggleBottomBar:
m_showBottomBar = !m_showBottomBar;
m_lastResult = m_showBottomBar ? "状态条已打开" : "状态条已关闭";
m_lastResult = m_showBottomBar ? "BottomBar 已打开" : "BottomBar 已关闭";
break;
case ActionId::ToggleTexture:
m_textureEnabled = !m_textureEnabled;
@@ -537,11 +537,11 @@ private:
switch (hit.kind) {
case UIEditorViewportSlotHitTargetKind::ToolItem:
m_slotState.activeToolIndex = hit.index;
m_lastResult = "ToolItem 命中" + std::to_string(hit.index);
m_lastResult = "ToolItem 命中: " + std::to_string(hit.index);
break;
case UIEditorViewportSlotHitTargetKind::StatusSegment:
m_slotState.statusBarState.activeIndex = hit.index;
m_lastResult = "StatusSegment 命中" + std::to_string(hit.index);
m_lastResult = "StatusSegment 命中: " + std::to_string(hit.index);
break;
case UIEditorViewportSlotHitTargetKind::Surface:
m_slotState.focused = true;
@@ -584,40 +584,40 @@ private:
DrawCard(
drawList,
m_introRect,
"测试功能ViewportSlot 基础壳层",
"只验证 Editor 基础层。");
"这个测试验证什么功能?",
"只验证 Editor ViewportSlot 的基础层。");
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 66.0f),
"检查1TopBar / Surface / 状态条布局",
"1. 验证 TopBar / Surface / BottomBar 的区域划分",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 88.0f),
"检查2hover / focus / active / capture。",
"2. 验证 hover / focus / active / capture 状态",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 110.0f),
"操作1hover toolbar / surface / status bar。",
"3. 验证 hover toolbar / surface / status bar。",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 132.0f),
"操作2click surface;按住左键观察 capture。",
"4. 验证点击 surface 后进入 capture。",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 154.0f),
"操作3切换 TopBar / 状态条 / Texture / 比例F12 或“截图”",
"5. 验证切换 TopBar / BottomBar / Texture / 方形比例四个开关",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 180.0f),
"结果chrome 开关后 surface 扩张;Texture/Fallback 正确",
"6. Chrome 固定,surface Texture / Fallback 之间切换",
kTextWeak,
12.0f);
DrawCard(drawList, m_controlsRect, "开关", "只保留和 ViewportSlot contract 直接相关的操作。");
DrawCard(drawList, m_controlsRect, "操作", "只保留和 ViewportSlot contract 直接相关的操作。");
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
@@ -640,12 +640,12 @@ private:
13.0f);
drawList.AddText(
UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 144.0f),
"Capture: " + BoolText(m_slotState.inputCaptured),
"捕获: " + BoolText(m_slotState.inputCaptured),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 170.0f),
"Chrome: TopBar " + BoolText(m_showTopBar) + " | 状态条 " + BoolText(m_showBottomBar),
"Chrome: TopBar " + BoolText(m_showTopBar) + " | BottomBar " + BoolText(m_showBottomBar),
kTextPrimary,
13.0f);
drawList.AddText(
@@ -662,7 +662,7 @@ private:
m_autoScreenshot.HasPendingCapture()
? "截图排队中..."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("截图F12 或按钮 -> viewport_slot_basic/captures/")
? std::string("截图: F12 或按钮 -> viewport_slot_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 248.0f),

View File

@@ -2,7 +2,9 @@
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorWorkspaceInteraction.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Shell/UIEditorWorkspaceInteraction.h>
#include "../../shared/src/EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -12,6 +14,8 @@
#include <windowsx.h>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <sstream>
#include <string>
@@ -41,14 +45,20 @@ using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
using XCEngine::UI::Editor::FindUIEditorWorkspaceViewportPresentationFrame;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::ResolveUIEditorDockHostMetrics;
using XCEngine::UI::Editor::ResolveUIEditorDockHostPalette;
using XCEngine::UI::Editor::ResolveUIEditorViewportSlotMetrics;
using XCEngine::UI::Editor::ResolveUIEditorViewportSlotPalette;
using XCEngine::UI::Editor::UIEditorPanelPresentationKind;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
using XCEngine::UI::Editor::UIEditorViewportInputBridgeFrame;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
using XCEngine::UI::Editor::UIEditorWorkspaceController;
using XCEngine::UI::Editor::UIEditorWorkspaceInteractionFrame;
using XCEngine::UI::Editor::UIEditorWorkspaceInteractionModel;
using XCEngine::UI::Editor::UIEditorWorkspaceInteractionResult;
using XCEngine::UI::Editor::UIEditorWorkspaceInteractionState;
using XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationStatus;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
@@ -56,18 +66,12 @@ using XCEngine::UI::Editor::UpdateUIEditorWorkspaceInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorWorkspaceInteractionBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Workspace Interaction";
const auto kShellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const auto kShellMetrics = XCEngine::Tests::EditorUI::GetEditorValidationShellMetrics();
constexpr const wchar_t* kWindowClassName = L"XCUIEditorWorkspaceInteractionBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Workspace Interaction Basic";
constexpr UIColor kWindowBg(0.11f, 0.11f, 0.11f, 1.0f);
constexpr UIColor kCardBg(0.17f, 0.17f, 0.17f, 1.0f);
constexpr UIColor kCardBorder(0.28f, 0.28f, 0.28f, 1.0f);
constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 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 kButtonBg(0.24f, 0.24f, 0.24f, 1.0f);
constexpr UIColor kButtonHover(0.32f, 0.32f, 0.32f, 1.0f);
constexpr UIColor kButtonBorder(0.47f, 0.47f, 0.47f, 1.0f);
constexpr UIColor kSuccess(0.48f, 0.72f, 0.52f, 1.0f);
constexpr UIColor kWarning(0.82f, 0.67f, 0.35f, 1.0f);
@@ -91,6 +95,23 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
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";
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -112,14 +133,22 @@ std::string FormatFloat(float value, int precision = 2) {
std::string DescribeHitTarget(const UIEditorDockHostHitTarget& target) {
switch (target.kind) {
case UIEditorDockHostHitTargetKind::SplitterHandle: return "Splitter: " + target.nodeId;
case UIEditorDockHostHitTargetKind::TabStripBackground: return "TabStripBackground: " + target.nodeId;
case UIEditorDockHostHitTargetKind::Tab: return "Tab: " + target.panelId;
case UIEditorDockHostHitTargetKind::TabCloseButton: return "TabClose: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelHeader: return "PanelHeader: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelBody: return "PanelBody: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelFooter: return "PanelFooter: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelCloseButton: return "PanelClose: " + target.panelId;
case UIEditorDockHostHitTargetKind::SplitterHandle:
return "分割条: " + target.nodeId;
case UIEditorDockHostHitTargetKind::TabStripBackground:
return "标签栏背景: " + target.nodeId;
case UIEditorDockHostHitTargetKind::Tab:
return "标签: " + target.panelId;
case UIEditorDockHostHitTargetKind::TabCloseButton:
return "标签关闭: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelHeader:
return "面板标题: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelBody:
return "面板内容: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelFooter:
return "面板底栏: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelCloseButton:
return "面板关闭: " + target.panelId;
case UIEditorDockHostHitTargetKind::None:
default:
return "None";
@@ -146,52 +175,85 @@ std::string JoinVisiblePanelIds(const UIEditorWorkspaceController& controller) {
std::string DescribeViewportEvent(const UIEditorViewportInputBridgeFrame& frame) {
if (frame.captureStarted) {
return "Viewport CaptureStarted";
return "视口开始捕获";
}
if (frame.captureEnded) {
return "Viewport CaptureEnded";
return "视口结束捕获";
}
if (frame.focusGained) {
return "Viewport FocusGained";
return "视口获得焦点";
}
if (frame.focusLost) {
return "Viewport FocusLost";
return "视口失去焦点";
}
if (frame.pointerPressedInside) {
return "Viewport PointerDownInside";
return "视口内按下";
}
if (frame.pointerReleasedInside) {
return "Viewport PointerUpInside";
return "视口内抬起";
}
if (frame.pointerMoved) {
return "Viewport PointerMove";
return "视口指针移动";
}
if (frame.wheelDelta != 0.0f) {
return "Viewport Wheel " + FormatFloat(frame.wheelDelta);
return "视口滚轮 " + FormatFloat(frame.wheelDelta);
}
return "Viewport Input";
return "视口输入";
}
void DrawCard(
UIDrawList& drawList,
const UIRect& rect,
std::string_view title,
std::string_view subtitle = {}) {
drawList.AddFilledRect(rect, kCardBg, 10.0f);
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
if (!subtitle.empty()) {
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 38.0f), std::string(subtitle), kTextMuted, 12.0f);
std::string DescribeValidationHitTarget(const UIEditorDockHostHitTarget& target) {
switch (target.kind) {
case UIEditorDockHostHitTargetKind::SplitterHandle:
return "Splitter: " + target.nodeId;
case UIEditorDockHostHitTargetKind::TabStripBackground:
return "Tab strip background: " + target.nodeId;
case UIEditorDockHostHitTargetKind::Tab:
return "Tab: " + target.panelId;
case UIEditorDockHostHitTargetKind::TabCloseButton:
return "Tab close: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelHeader:
return "Panel header: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelBody:
return "Panel body: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelFooter:
return "Panel footer: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelCloseButton:
return "Panel close: " + target.panelId;
case UIEditorDockHostHitTargetKind::None:
default:
return "None";
}
}
void DrawButton(UIDrawList& drawList, const ButtonState& button) {
drawList.AddFilledRect(button.rect, button.hovered ? kButtonHover : kButtonBg, 8.0f);
drawList.AddRectOutline(button.rect, kButtonBorder, 1.0f, 8.0f);
drawList.AddText(UIPoint(button.rect.x + 14.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f);
std::string DescribeValidationViewportEvent(const UIEditorViewportInputBridgeFrame& frame) {
if (frame.captureStarted) {
return "Viewport capture started";
}
if (frame.captureEnded) {
return "Viewport capture ended";
}
if (frame.focusGained) {
return "Viewport focus gained";
}
if (frame.focusLost) {
return "Viewport focus lost";
}
if (frame.pointerPressedInside) {
return "Viewport pointer down";
}
if (frame.pointerReleasedInside) {
return "Viewport pointer up";
}
if (frame.pointerMoved) {
return "Viewport pointer move";
}
if (frame.wheelDelta != 0.0f) {
return "Viewport wheel " + FormatFloat(frame.wheelDelta);
}
return "Viewport input";
}
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry BuildValidationPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "viewport", "Viewport", UIEditorPanelPresentationKind::ViewportShell, false, true, true },
@@ -201,7 +263,7 @@ UIEditorPanelRegistry BuildPanelRegistry() {
return registry;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel BuildValidationWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-split",
@@ -219,17 +281,99 @@ UIEditorWorkspaceModel BuildWorkspace() {
return workspace;
}
UIEditorWorkspaceInteractionModel BuildInteractionModel() {
UIEditorWorkspaceInteractionModel BuildValidationInteractionModel() {
UIEditorWorkspaceInteractionModel model = {};
UIEditorWorkspacePanelPresentationModel presentation = {};
presentation.panelId = "viewport";
presentation.kind = UIEditorPanelPresentationKind::ViewportShell;
presentation.viewportShellModel.spec.chrome.title = "Viewport";
presentation.viewportShellModel.spec.chrome.subtitle = "Workspace Interaction";
presentation.viewportShellModel.spec.chrome.subtitle = "Workspace interaction contract";
presentation.viewportShellModel.spec.chrome.showTopBar = true;
presentation.viewportShellModel.spec.chrome.showBottomBar = true;
presentation.viewportShellModel.frame.statusText =
"这里只验证 WorkspaceInteraction contract不接 Scene/Game 业务。";
"Check splitter drag, tab switch, panel activation, and viewport input.";
model.workspacePresentations = { presentation };
return model;
}
void DrawCard(
UIDrawList& drawList,
const UIRect& rect,
std::string_view title,
std::string_view subtitle = {}) {
drawList.AddFilledRect(rect, kShellPalette.cardBackground, kShellMetrics.cardRadius);
drawList.AddRectOutline(rect, kShellPalette.cardBorder, 1.0f, kShellMetrics.cardRadius);
drawList.AddText(
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
std::string(title),
kShellPalette.textPrimary,
kShellMetrics.titleFontSize);
if (!subtitle.empty()) {
drawList.AddText(
UIPoint(rect.x + 16.0f, rect.y + 38.0f),
std::string(subtitle),
kShellPalette.textMuted,
kShellMetrics.bodyFontSize);
}
}
void DrawButton(UIDrawList& drawList, const ButtonState& button) {
drawList.AddFilledRect(
button.rect,
button.hovered ? kShellPalette.buttonHoverBackground : kShellPalette.buttonBackground,
kShellMetrics.buttonRadius);
drawList.AddRectOutline(
button.rect,
kShellPalette.cardBorder,
1.0f,
kShellMetrics.buttonRadius);
drawList.AddText(
UIPoint(button.rect.x + 14.0f, button.rect.y + 10.0f),
button.label,
kShellPalette.textPrimary,
kShellMetrics.bodyFontSize);
}
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "viewport", "视口", UIEditorPanelPresentationKind::ViewportShell, false, true, true },
{ "doc", "文档", UIEditorPanelPresentationKind::Placeholder, true, true, true },
{ "details", "详情", UIEditorPanelPresentationKind::Placeholder, true, true, true }
};
return registry;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-split",
UIEditorWorkspaceSplitAxis::Horizontal,
0.7f,
BuildUIEditorWorkspaceTabStack(
"tab-stack",
{
BuildUIEditorWorkspacePanel("viewport-node", "viewport", "视口"),
BuildUIEditorWorkspacePanel("doc-node", "doc", "文档", true)
},
0u),
BuildUIEditorWorkspacePanel("details-node", "details", "详情", true));
workspace.activePanelId = "viewport";
return workspace;
}
UIEditorWorkspaceInteractionModel BuildInteractionModel() {
UIEditorWorkspaceInteractionModel model = {};
UIEditorWorkspacePanelPresentationModel presentation = {};
presentation.panelId = "viewport";
presentation.kind = UIEditorPanelPresentationKind::ViewportShell;
presentation.viewportShellModel.spec.chrome.title = "视口";
presentation.viewportShellModel.spec.chrome.subtitle =
"验证 Workspace 与 Viewport 的统一交互";
presentation.viewportShellModel.spec.chrome.showTopBar = true;
presentation.viewportShellModel.spec.chrome.showBottomBar = true;
presentation.viewportShellModel.frame.statusText =
"验证分割、标签切换、面板激活与视口输入桥接";
model.workspacePresentations = { presentation };
return model;
}
@@ -271,7 +415,9 @@ private:
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->m_renderer.Resize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
app->m_renderer.Resize(
static_cast<UINT>(LOWORD(lParam)),
static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_PAINT:
@@ -373,7 +519,8 @@ private:
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/workspace_interaction_basic/captures";
ResolveRepoRootPath() /
"tests/UI/Editor/integration/shell/workspace_interaction_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
WNDCLASSEXW windowClass = {};
@@ -411,6 +558,11 @@ private:
}
ResetScenario();
if (IsAutoCaptureOnStartupEnabled()) {
m_lastStatus = "Capture";
m_lastMessage = "Startup capture queued. Verify the first frame before interacting.";
m_lastColor = kWarning;
}
return true;
}
@@ -432,13 +584,15 @@ private:
if (GetCapture() == m_hwnd) {
ReleaseCapture();
}
m_controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
m_controller = BuildDefaultUIEditorWorkspaceController(
BuildValidationPanelRegistry(),
BuildValidationWorkspace());
m_interactionState = {};
m_interactionModel = BuildInteractionModel();
m_interactionModel = BuildValidationInteractionModel();
m_cachedFrame = {};
m_pendingInputEvents.clear();
m_lastStatus = "Ready";
m_lastMessage = "等待交互。这里只验证 WorkspaceInteraction 统一输入路由 contract。";
m_lastMessage = "Use the workspace on the right to exercise splitter drag, tab switch, and viewport input. Set XCUI_AUTO_CAPTURE_ON_STARTUP=1 to capture the startup frame.";
m_lastColor = kWarning;
}
@@ -468,8 +622,8 @@ private:
const float left = m_controlsRect.x + 16.0f;
const float top = m_controlsRect.y + 62.0f;
m_buttons = {
{ ActionId::Reset, "重置", UIRect(left, top, buttonWidth, 36.0f), false },
{ ActionId::Capture, "截图(F12)", UIRect(left + buttonWidth + 12.0f, top, buttonWidth, 36.0f), false }
{ ActionId::Reset, "Reset", UIRect(left, top, buttonWidth, 36.0f), false },
{ ActionId::Capture, "Capture (F12)", UIRect(left + buttonWidth + 12.0f, top, buttonWidth, 36.0f), false }
};
}
@@ -513,14 +667,14 @@ private:
if (action == ActionId::Reset) {
ResetScenario();
m_lastStatus = "Ready";
m_lastMessage = "场景状态已重置。请重新检查 viewport body / tab switch / splitter drag。";
m_lastMessage = "Scenario reset. Re-check splitter drag, tab switch, and viewport input.";
m_lastColor = kWarning;
return;
}
m_autoScreenshot.RequestCapture("manual_button");
m_lastStatus = "Ready";
m_lastMessage = "截图已排队,输出到 tests/UI/Editor/integration/shell/workspace_interaction_basic/captures/。";
m_lastStatus = "Capture";
m_lastMessage = "Capture queued. Check the Output/latest.png path shown below.";
m_lastColor = kWarning;
}
@@ -556,16 +710,16 @@ private:
void SetInteractionResult(const UIEditorWorkspaceInteractionResult& result) {
if (result.dockHostResult.layoutResult.status !=
XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationStatus::Rejected) {
m_lastStatus = "DockHostLayout";
UIEditorWorkspaceLayoutOperationStatus::Rejected) {
m_lastStatus = "Layout";
m_lastMessage = result.dockHostResult.layoutResult.message;
m_lastColor = kSuccess;
return;
}
if (result.dockHostResult.commandResult.status !=
XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus::Rejected) {
m_lastStatus = "DockHostCommand";
UIEditorWorkspaceCommandStatus::Rejected) {
m_lastStatus = "Command";
m_lastMessage = result.dockHostResult.commandResult.message;
m_lastColor = kSuccess;
return;
@@ -573,7 +727,7 @@ private:
if (!result.viewportPanelId.empty()) {
m_lastStatus = result.viewportPanelId;
m_lastMessage = DescribeViewportEvent(result.viewportInputFrame);
m_lastMessage = DescribeValidationViewportEvent(result.viewportInputFrame);
m_lastColor =
result.viewportInputFrame.captureStarted || result.viewportInputFrame.focusGained
? kSuccess
@@ -583,33 +737,165 @@ private:
if (result.requestPointerCapture) {
m_lastStatus = "Capture";
m_lastMessage = "宿主已收到 WorkspaceInteraction pointer capture 请求。";
m_lastMessage = "Workspace interaction requested host pointer capture.";
m_lastColor = kSuccess;
return;
}
if (result.releasePointerCapture) {
m_lastStatus = "Release";
m_lastMessage = "宿主已执行 WorkspaceInteraction pointer release。";
m_lastMessage = "Workspace interaction released host pointer capture.";
m_lastColor = kWarning;
return;
}
if (result.consumed) {
m_lastStatus = "Consumed";
m_lastMessage = "这次输入被 WorkspaceInteraction 层消费。";
m_lastMessage = "The workspace consumed the input, but no higher-level state changed.";
m_lastColor = kWarning;
}
}
void RenderValidationUI(float width, float height) {
const auto* viewportFrame =
FindUIEditorWorkspaceViewportPresentationFrame(m_cachedFrame.composeFrame, "viewport");
const std::string selectedPresentation =
viewportFrame != nullptr ? "ViewportShell" : "Placeholder";
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("WorkspaceInteractionBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kShellPalette.windowBackground);
DrawCard(
drawList,
m_introRect,
"What This Validates",
"Verify that workspace interaction and viewport input share one stable contract.");
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 72.0f),
"1. Splitter drag should update the right-side layout ratio immediately.",
kShellPalette.textPrimary,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f),
"2. Clicking a tab, panel, or viewport should surface the hit target and result on the left.",
kShellPalette.textPrimary,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 116.0f),
"3. Viewport hover, focus, and capture changes should be reflected in the state card.",
kShellPalette.textPrimary,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 138.0f),
"4. This scene validates editor interaction plumbing only, not business-specific panels.",
kShellPalette.textPrimary,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 162.0f),
"Use XCUI_AUTO_CAPTURE_ON_STARTUP=1 for a startup shot; otherwise press F12 after exercising the scene.",
kShellPalette.textWeak,
11.0f);
DrawCard(drawList, m_controlsRect, "Actions", "Only reset and capture are exposed here.");
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, m_stateRect, "State", "Observe workspace interaction and viewport input in real time.");
float stateY = m_stateRect.y + 66.0f;
auto addStateLine = [&](std::string text,
const UIColor& color = kShellPalette.textPrimary,
float fontSize = 12.0f) {
drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY), std::move(text), color, fontSize);
stateY += 20.0f;
};
addStateLine(
"Hit target: " +
DescribeValidationHitTarget(m_interactionState.dockHostInteractionState.dockHostState.hoveredTarget),
kShellPalette.textPrimary,
11.0f);
addStateLine("Presentation: " + selectedPresentation, kShellPalette.textPrimary);
addStateLine(
"Active panel: " +
(m_controller.GetWorkspace().activePanelId.empty()
? std::string("(none)")
: m_controller.GetWorkspace().activePanelId));
addStateLine("Selected tab: " + GetSelectedTabId(), kShellPalette.textPrimary);
addStateLine("Visible panels: " + JoinVisiblePanelIds(m_controller), kShellPalette.textWeak, 11.0f);
addStateLine("Result: " + m_lastStatus, m_lastColor);
drawList.AddText(
UIPoint(m_stateRect.x + 16.0f, stateY + 4.0f),
m_lastMessage,
kShellPalette.textMuted,
11.0f);
stateY += 34.0f;
addStateLine(
"Host capture: " + FormatBool(GetCapture() == m_hwnd),
GetCapture() == m_hwnd ? kSuccess : kShellPalette.textMuted);
addStateLine(
"Root split ratio: " + FormatFloat(m_controller.GetWorkspace().root.splitRatio),
kShellPalette.textWeak,
11.0f);
if (viewportFrame != nullptr) {
addStateLine(
"Viewport hover: " + FormatBool(viewportFrame->viewportShellFrame.inputFrame.hovered),
kShellPalette.textWeak,
11.0f);
addStateLine(
"Viewport focus: " + FormatBool(viewportFrame->viewportShellFrame.inputFrame.focused),
kShellPalette.textWeak,
11.0f);
addStateLine(
"Viewport capture: " + FormatBool(viewportFrame->viewportShellFrame.inputFrame.captured),
kShellPalette.textWeak,
11.0f);
}
addStateLine(
"Capture: " +
(m_autoScreenshot.HasPendingCapture()
? std::string("capture queued...")
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("Set XCUI_AUTO_CAPTURE_ON_STARTUP=1 or press F12.")
: m_autoScreenshot.GetLastCaptureSummary())),
kShellPalette.textWeak,
11.0f);
DrawCard(drawList, m_previewRect, "Preview", "The right side renders the real workspace interaction frame.");
const auto dockPalette = ResolveUIEditorDockHostPalette();
const auto viewportPalette = ResolveUIEditorViewportSlotPalette();
const auto dockMetrics = ResolveUIEditorDockHostMetrics();
const auto viewportMetrics = ResolveUIEditorViewportSlotMetrics();
AppendUIEditorWorkspaceCompose(
drawList,
m_cachedFrame.composeFrame,
dockPalette,
dockMetrics,
viewportPalette,
viewportMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
void RenderFrame() {
UpdateLayout();
const auto dockMetrics = ResolveUIEditorDockHostMetrics();
const auto viewportMetrics = ResolveUIEditorViewportSlotMetrics();
m_cachedFrame = UpdateUIEditorWorkspaceInteraction(
m_interactionState,
m_controller,
m_workspaceRect,
m_interactionModel,
m_pendingInputEvents);
m_pendingInputEvents,
dockMetrics,
viewportMetrics);
m_pendingInputEvents.clear();
ApplyHostCaptureRequests(m_cachedFrame.result);
SetInteractionResult(m_cachedFrame.result);
@@ -618,63 +904,126 @@ private:
FindUIEditorWorkspaceViewportPresentationFrame(m_cachedFrame.composeFrame, "viewport");
const bool viewportVisible = viewportFrame != nullptr;
const std::string selectedPresentation =
viewportVisible ? "ViewportShell" : "DockHost Placeholder";
viewportVisible ? "视口呈现中" : "停用或未生成占位";
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(clientRect.right - clientRect.left, 1L));
const float height = static_cast<float>((std::max)(clientRect.bottom - clientRect.top, 1L));
RenderValidationUI(width, height);
return;
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("WorkspaceInteractionBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kShellPalette.windowBackground);
DrawCard(drawList, m_introRect, "这个测试验证什么功能", "只验证 WorkspaceInteraction 统一路由 contract不做 editor 业务。");
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 72.0f), "1. 验证 DockHost splitter drag 与 ViewportShell input 可以被同一层统一收口。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f), "2. 验证点击 viewport body 时focus/capture 请求会直接冒泡给宿主。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 116.0f), "3. 验证点击 Document tab 后body 会立即从 ViewportShell 切回 DockHost placeholder。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 138.0f), "4. 验证 splitter 改尺寸后viewport body bounds 与 workspace layout 同步变化。", kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 162.0f), "建议操作: 先点中间 viewport再切到 Document最后拖右侧 splitter。", kTextWeak, 11.0f);
DrawCard(
drawList,
m_introRect,
"这个测试验证什么",
"验证 Workspace 基础交互与 Viewport 输入桥接是否统一。");
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 72.0f),
"1. 拖拽分割条后,右侧工作区布局比例应实时变化。",
kShellPalette.textPrimary,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f),
"2. 点击标签、面板或视口时,左侧状态应显示命中目标与结果。",
kShellPalette.textPrimary,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 116.0f),
"3. 视口 hover、focus、capture 的变化应反馈到状态面板。",
kShellPalette.textPrimary,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 138.0f),
"4. 该场景只验证 Editor 基础交互,不验证具体业务面板。",
kShellPalette.textPrimary,
12.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 162.0f),
"重点观察:命中目标、当前激活面板、宿主 capture 与视口输入状态。",
kShellPalette.textWeak,
11.0f);
DrawCard(drawList, m_controlsRect, "操作", "这里只保留 Reset / Capture");
DrawCard(drawList, m_controlsRect, "操作", "这里只保留重置和截图两个辅助动作");
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, m_stateRect, "状态", "重点检查 WorkspaceInteraction 当前状态。");
DrawCard(drawList, m_stateRect, "状态", "观察当前工作区交互与视口输入状态。");
float stateY = m_stateRect.y + 66.0f;
auto addStateLine = [&](std::string text, const UIColor& color = kTextPrimary, float fontSize = 12.0f) {
auto addStateLine = [&](std::string text,
const UIColor& color = kShellPalette.textPrimary,
float fontSize = 12.0f) {
drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY), std::move(text), color, fontSize);
stateY += 20.0f;
};
addStateLine("DockHost Hover: " + DescribeHitTarget(m_interactionState.dockHostInteractionState.dockHostState.hoveredTarget), kTextPrimary, 11.0f);
addStateLine("Selected Presentation: " + selectedPresentation, kTextPrimary);
addStateLine("Active Panel: " + (m_controller.GetWorkspace().activePanelId.empty() ? std::string("(none)") : m_controller.GetWorkspace().activePanelId));
addStateLine("Selected Tab: " + GetSelectedTabId(), kTextPrimary);
addStateLine("Visible Panels: " + JoinVisiblePanelIds(m_controller), kTextWeak, 11.0f);
addStateLine("Result: " + m_lastStatus, m_lastColor);
drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY + 4.0f), m_lastMessage, kTextMuted, 11.0f);
addStateLine(
"命中目标: " +
DescribeHitTarget(m_interactionState.dockHostInteractionState.dockHostState.hoveredTarget),
kShellPalette.textPrimary,
11.0f);
addStateLine("当前呈现: " + selectedPresentation, kShellPalette.textPrimary);
addStateLine(
"当前激活: " +
(m_controller.GetWorkspace().activePanelId.empty()
? std::string("(none)")
: m_controller.GetWorkspace().activePanelId));
addStateLine("当前标签: " + GetSelectedTabId(), kShellPalette.textPrimary);
addStateLine("可见面板: " + JoinVisiblePanelIds(m_controller), kShellPalette.textWeak, 11.0f);
addStateLine("结果: " + m_lastStatus, m_lastColor);
drawList.AddText(
UIPoint(m_stateRect.x + 16.0f, stateY + 4.0f),
m_lastMessage,
kShellPalette.textMuted,
11.0f);
stateY += 34.0f;
addStateLine("Host Capture: " + FormatBool(GetCapture() == m_hwnd), GetCapture() == m_hwnd ? kSuccess : kTextMuted);
addStateLine("root-split ratio: " + FormatFloat(m_controller.GetWorkspace().root.splitRatio), kTextWeak, 11.0f);
addStateLine(
"宿主 capture: " + FormatBool(GetCapture() == m_hwnd),
GetCapture() == m_hwnd ? kSuccess : kShellPalette.textMuted);
addStateLine(
"根分割比例: " + FormatFloat(m_controller.GetWorkspace().root.splitRatio),
kShellPalette.textWeak,
11.0f);
if (viewportFrame != nullptr) {
addStateLine("Viewport Hover: " + FormatBool(viewportFrame->viewportShellFrame.inputFrame.hovered), kTextWeak, 11.0f);
addStateLine("Viewport Focus: " + FormatBool(viewportFrame->viewportShellFrame.inputFrame.focused), kTextWeak, 11.0f);
addStateLine("Viewport Capture: " + FormatBool(viewportFrame->viewportShellFrame.inputFrame.captured), kTextWeak, 11.0f);
addStateLine(
"视口 hover: " + FormatBool(viewportFrame->viewportShellFrame.inputFrame.hovered),
kShellPalette.textWeak,
11.0f);
addStateLine(
"视口 focus: " + FormatBool(viewportFrame->viewportShellFrame.inputFrame.focused),
kShellPalette.textWeak,
11.0f);
addStateLine(
"视口 capture: " + FormatBool(viewportFrame->viewportShellFrame.inputFrame.captured),
kShellPalette.textWeak,
11.0f);
}
addStateLine(
"截图: " +
(m_autoScreenshot.HasPendingCapture()
? std::string("截图排队中...")
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 或 按钮 -> captures/")
: m_autoScreenshot.GetLastCaptureSummary())),
kTextWeak,
? std::string("截图排队中...")
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 或按钮截图 -> captures/")
: m_autoScreenshot.GetLastCaptureSummary())),
kShellPalette.textWeak,
11.0f);
DrawCard(drawList, m_previewRect, "Preview", "真实 WorkspaceInteraction 预览,不再在 exe 宿主里手写拼装输入路由");
AppendUIEditorWorkspaceCompose(drawList, m_cachedFrame.composeFrame);
DrawCard(drawList, m_previewRect, "预览", "右侧是真实的工作区交互预览");
const auto dockPalette = ResolveUIEditorDockHostPalette();
const auto viewportPalette = ResolveUIEditorViewportSlotPalette();
AppendUIEditorWorkspaceCompose(
drawList,
m_cachedFrame.composeFrame,
dockPalette,
dockMetrics,
viewportPalette,
viewportMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
@@ -704,7 +1053,7 @@ private:
bool m_trackingMouseLeave = false;
std::string m_lastStatus = {};
std::string m_lastMessage = {};
UIColor m_lastColor = kTextMuted;
UIColor m_lastColor = kShellPalette.textMuted;
};
} // namespace

View File

@@ -1,6 +1,5 @@
<View
name="EditorWorkspaceShellComposeValidation"
theme="../shared/themes/editor_validation.xctheme">
name="EditorWorkspaceShellComposeValidation">
<Column width="fill" height="fill" padding="20" gap="12">
<Card
title="测试内容Editor Shell 基础壳层组合"

View File

@@ -2,11 +2,11 @@
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorDockHostInteraction.h>
#include <XCEditor/Core/UIEditorPanelRegistry.h>
#include <XCEditor/Core/UIEditorWorkspaceController.h>
#include <XCEditor/Core/UIEditorWorkspaceModel.h>
#include <XCEditor/Widgets/UIEditorDockHost.h>
#include <XCEditor/Shell/UIEditorDockHostInteraction.h>
#include <XCEditor/Shell/UIEditorPanelRegistry.h>
#include <XCEditor/Shell/UIEditorWorkspaceController.h>
#include <XCEditor/Shell/UIEditorWorkspaceModel.h>
#include <XCEditor/Shell/UIEditorDockHost.h>
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -513,7 +513,7 @@ private:
m_resetPressed = false;
if (resetTriggered) {
ResetScenario();
m_lastResult = "Reset";
m_lastResult = "已重置";
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
@@ -555,22 +555,22 @@ private:
}
if (result.requestPointerCapture) {
m_lastResult = "Capture: begin splitter drag";
m_lastResult = "捕获:开始拖动 splitter";
return;
}
if (result.releasePointerCapture) {
m_lastResult = "Capture: end splitter drag";
m_lastResult = "捕获:结束拖动 splitter";
return;
}
if (result.hitTarget.kind != UIEditorDockHostHitTargetKind::None) {
m_lastResult = "Hover: " + DescribeHitTarget(result.hitTarget);
m_lastResult = "悬停: " + DescribeHitTarget(result.hitTarget);
return;
}
if (result.consumed) {
m_lastResult = "Consumed: input handled by DockHostInteraction";
m_lastResult = "已消费: 输入由 DockHostInteraction 处理";
}
}
@@ -597,7 +597,7 @@ private:
DrawCard(
drawList,
m_introRect,
"这个测试验证什么功能?",
"这个测试验证什么功能?",
"验证 Workspace + DockHost 的组合场景是否完全收口到统一交互层。");
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 68.0f),
@@ -625,12 +625,12 @@ private:
m_stateRect,
"状态回显",
"这里直接展示 hover / focus / dragging / active panel / split ratio。");
DrawButton(drawList, m_resetButtonRect, "Reset", m_resetHovered);
DrawButton(drawList, m_resetButtonRect, "重置", m_resetHovered);
const auto validation = m_controller.ValidateState();
drawList.AddText(
UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(m_interactionState.dockHostState.hoveredTarget),
"悬停: " + DescribeHitTarget(m_interactionState.dockHostState.hoveredTarget),
kTextPrimary,
13.0f);
drawList.AddText(
@@ -640,7 +640,7 @@ private:
13.0f);
drawList.AddText(
UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 122.0f),
"Capture: " + std::string(GetCapture() == m_hwnd ? "On" : "Off"),
"捕获: " + std::string(GetCapture() == m_hwnd ? "开启" : "关闭"),
GetCapture() == m_hwnd ? kSuccess : kTextMuted,
13.0f);
drawList.AddText(
@@ -653,12 +653,12 @@ private:
13.0f);
drawList.AddText(
UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 174.0f),
"Active Panel: " + m_controller.GetWorkspace().activePanelId,
"当前激活面板: " + m_controller.GetWorkspace().activePanelId,
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 198.0f),
"Visible Panels: " + JoinVisiblePanelIds(m_controller),
"可见面板: " + JoinVisiblePanelIds(m_controller),
kTextMuted,
12.0f);
drawList.AddText(
@@ -683,7 +683,7 @@ private:
12.0f);
drawList.AddText(
UIPoint(m_stateRect.x + 16.0f, m_stateRect.y + 382.0f),
validation.IsValid() ? "Validation: OK" : "Validation: " + validation.message,
validation.IsValid() ? "验证: OK" : "验证: " + validation.message,
validation.IsValid() ? kSuccess : kDanger,
12.0f);