Refactor new editor boundaries and test ownership

This commit is contained in:
2026-04-19 15:52:28 +08:00
parent dc13b56cf3
commit 93f06e84ed
279 changed files with 6349 additions and 3238 deletions

View File

@@ -0,0 +1,91 @@
add_subdirectory(workspace_shell_compose)
add_subdirectory(panel_frame_basic)
add_subdirectory(tab_strip_basic)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/editor_shell_compose/CMakeLists.txt")
add_subdirectory(editor_shell_compose)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/editor_shell_interaction/CMakeLists.txt")
add_subdirectory(editor_shell_interaction editor_shell_interaction_build)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/menu_bar_basic/CMakeLists.txt")
add_subdirectory(menu_bar_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/dock_host_basic/CMakeLists.txt")
add_subdirectory(dock_host_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/panel_content_host_basic/CMakeLists.txt")
add_subdirectory(panel_content_host_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/property_grid_basic/CMakeLists.txt")
add_subdirectory(property_grid_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/bool_field_basic/CMakeLists.txt")
add_subdirectory(bool_field_basic)
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()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vector2_field_basic/CMakeLists.txt")
add_subdirectory(vector2_field_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vector3_field_basic/CMakeLists.txt")
add_subdirectory(vector3_field_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vector4_field_basic/CMakeLists.txt")
add_subdirectory(vector4_field_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/enum_field_basic/CMakeLists.txt")
add_subdirectory(enum_field_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/color_field_basic/CMakeLists.txt")
add_subdirectory(color_field_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tree_view_basic/CMakeLists.txt")
add_subdirectory(tree_view_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tree_view_multiselect/CMakeLists.txt")
add_subdirectory(tree_view_multiselect)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tree_view_inline_rename/CMakeLists.txt"
AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tree_view_inline_rename/main.cpp")
add_subdirectory(tree_view_inline_rename)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/list_view_basic/CMakeLists.txt")
add_subdirectory(list_view_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/list_view_multiselect/CMakeLists.txt")
add_subdirectory(list_view_multiselect)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/list_view_inline_rename/CMakeLists.txt")
add_subdirectory(list_view_inline_rename)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/scroll_view_basic/CMakeLists.txt")
add_subdirectory(scroll_view_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/status_bar_basic/CMakeLists.txt")
add_subdirectory(status_bar_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/context_menu_basic/CMakeLists.txt")
add_subdirectory(context_menu_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/viewport_slot_basic/CMakeLists.txt")
add_subdirectory(viewport_slot_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/viewport_shell_basic/CMakeLists.txt")
add_subdirectory(viewport_shell_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/workspace_viewport_compose/CMakeLists.txt")
add_subdirectory(workspace_viewport_compose)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/workspace_interaction_basic/CMakeLists.txt")
add_subdirectory(workspace_interaction_basic)
endif()

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_asset_field_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_asset_field_basic_validation
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 "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/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, "é‡<EFBFBD>ç½®", 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/manual_validation/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 = "å·²é‡<EFBFBD>置到默认 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,
"这个æµè¯•在验è¯<EFBFBD>什么功能?",
"验è¯<EFBFBD> Editor 基础 AssetField çš„åºå®šé£Žæ ¼ã€<C3A3>清æ™?hit target,以å<C2A5>?activate / picker / clear 三类最å°<C3A5>请æ±ã€?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 74.0f),
"1. 字段视觉固定ä¸?Unity 风格对象槽:标签ã€<C3A3>预览å<CB86>—ã€<C3A3>å€¼æ‡æœ¬ã€<C3A3>状æ€<C3A6>标记ã€<C3A3>é€‰æ©æŒ‰é®ã€<C3A3>清空按é®ã€?,
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 98.0f),
"2. ç¹å‡»å€¼åŒºåŸŸå<C5B8>ªäº§ç”Ÿ activateRequestedï¼åŸºç¡€å±ä¸<C3A4>决定 pingã€<C3A3>打开é<E282AC>¢æ<C2A2>¿æˆä¸šåŠ¡è·³è½¬ã€?,
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 122.0f),
"3. 点击 oï¼Œæˆ focused å<>ŽæŒ‰ Enter / Space,å<C592>ªäº§ç”Ÿ 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,清空当å‰<C3A5>引用ã€?,
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 170.0f),
"5. 本壳ç¨åº<C3A5>å<EFBFBD>ªç”¨ä¸¤ä¸ªåºå®šæ ·æœ¬èµ„äº§æ¨¡æŸ picker å¤éƒ¨å“<C3A5>应,ä¸<C3A4>引入 object picker 弹窗系统ã€?,
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 194.0f),
"6. 左侧状æ€<C3A6>区è§å¯Ÿ hover / focus / assetId / displayName / status / resultï¼F12 截图ã€?,
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, m_shellPalette, m_shellMetrics, "æ“<EFBFBD>作");
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,
"状æ€<EFBFBD>æ˜è¦?,
"é‡<EFBFBD>ç¹çœ?hitã€<C3A3>focusã€<C3A3>值是å<C2AF>¦å<C2A6>˜åŒï¼Œä»¥å<C2A5>Šè¯·æ±æ˜¯å<C2AF>¦ä¿<C3A4>æŒ<C3A6>è„语义ã€?);
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/manual_validation/shell/asset_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary()),
m_shellPalette.textWeak,
m_shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
m_shellPalette,
m_shellMetrics,
"AssetField 预览",
"这里å<EFBFBD>ªæ”¾ä¸€ä¸ªåŸºç¡€ AssetField,ä¸<C3A4>接业务é<C2A1>¢æ<C2A2>¿ï¼Œä¹Ÿä¸<C3A4>æŽ?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,壳ç¨åº<C3A5>已用åºå®šæ ·æœ¬èµ„产模æŸå¤éƒ¨é€‰æ©ç»“æžœ";
refreshNeeded = true;
} else if (result.clearRequested) {
m_lastResult = "收到 clearRequested,当å‰<C3A5>引用已清空";
refreshNeeded = true;
} else if (result.activateRequested) {
++m_activateCount;
m_lastResult = "收到 activateRequestedï¼åŸºç¡€å±å<E2809A>ªå<C2AA>请æ±ï¼Œä¸<C3A4>ç»å®šä¸šåŠ¡åŠ¨ä½?;
} else if (result.focusChanged) {
m_lastResult = std::string("焦ç¹å<EFBFBD>˜åŒ: ") +
(m_interactionState.fieldState.focused ? "focused" : "lost");
} else if (result.consumed) {
m_lastResult = "输入已被当å‰<EFBFBD>字段消费";
}
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

@@ -0,0 +1,8 @@
add_executable(editor_ui_bool_field_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_bool_field_basic_validation
OUTPUT_NAME "XCUIEditorBoolFieldBasicValidation"
)

View File

@@ -0,0 +1,710 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Fields/UIEditorBoolFieldInteraction.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorBoolField.h>
#include "EditorValidationTheme.h"
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/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::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::UIEditorBoolFieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorBoolFieldInteractionResult;
using XCEngine::UI::Editor::UIEditorBoolFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorBoolFieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolField;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorBoolField;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorBoolFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | BoolField 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 MapBoolFieldKey(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);
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 = 430.0f;
const float gap = shellMetrics.gap;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 214.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 + 24.0f,
layout.previewRect.y + 82.0f,
260.0f,
32.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "é‡<EFBFBD>ç½®", 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 UIEditorBoolFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorBoolFieldHitTargetKind::Checkbox:
return "checkbox";
case UIEditorBoolFieldHitTargetKind::Row:
return "row";
case UIEditorBoolFieldHitTargetKind::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;
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "已请求截图,输出�captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
const std::int32_t keyCode = MapBoolFieldKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->HandleKeyDown(keyCode);
return 0;
}
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1480,
920,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/bool_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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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::GetEditorValidationShellMetrics());
}
void ResetScenario() {
m_value = false;
m_spec = {};
m_spec.fieldId = "enabled";
m_spec.label = "Enabled";
m_interactionState = {};
m_interactionState.fieldState.focused = true;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "å·²é‡<EFBFBD>置到默认 BoolField 状æ€?;
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics();
m_frame = UpdateUIEditorBoolFieldInteraction(
m_interactionState,
m_value,
layout.fieldRect,
m_spec,
{},
metrics);
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshFrame();
}
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);
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorBoolFieldInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleKeyDown(std::int32_t keyCode) {
const UIEditorBoolFieldInteractionResult result =
PumpEvents({ MakeKeyEvent(keyCode) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorBoolFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics();
m_frame = UpdateUIEditorBoolFieldInteraction(
m_interactionState,
m_value,
layout.fieldRect,
m_spec,
std::move(events),
metrics);
return m_frame.result;
}
void UpdateResultText(const UIEditorBoolFieldInteractionResult& result) {
if (result.valueChanged) {
m_lastResult = std::string("值已切æ<EFBFBD>¢åˆ? ") + (m_value ? "true" : "false");
return;
}
if (result.consumed) {
m_lastResult = "控件已消费输�;
return;
}
m_lastResult = "等待交互";
}
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 RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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::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();
const auto boolPalette = XCEngine::UI::Editor::ResolveUIEditorBoolFieldPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorBoolFieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个æµè¯•在验è¯<EFBFBD>什么功能?",
"验è¯<EFBFBD> Editor BoolField çš„ç¹å‡»åˆ‡æ<E280A1>¢ã€<C3A3>é”®ç˜åˆ‡æ<E280A1>¢åŒçжæ€<C3A6>å<EFBFBD>Œæ­¥ï¼Œæ ·å¼<C3A5>åºå®šä¸?Editor 字段风格ã€?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 row æˆ?checkbox,检æŸ?true / false 是å<C2AF>¦ç¨³å®šåˆ‡æ<E280A1>¢ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 控件获得 focus å<>ŽæŒ‰ Space / Enter,也必须能够切æ<E280A1>¢å€¼ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 检æŸ?Hover / Focus / Value / Result 是å<C2AF>¦å<C2A6>Œæ­¥æ´æ°ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. �F12 或点击截图按钮,确认自动截图路径正确�,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "æ“<EFBFBD>作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状æ€<EFBFBD>æ˜è¦?,
"é‡<EFBFBD>ç¹æ£€æŸ?hit / focus / value / resultã€?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "æ˜? : "å<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Active: ") + (m_interactionState.fieldState.active ? "æ˜? : "å<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
std::string("Value: ") + (m_value ? "true" : "false"),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队�.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/bool_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"BoolField 预览",
"这里å<EFBFBD>ªæ”¾ä¸€ä¸ªåºå®šæ ·å¼?BoolField,ä¸<C3A4>混入其ä»ä¸šåŠ¡æŽ§ä»¶ã€?);
UIEditorBoolFieldSpec previewSpec = m_spec;
previewSpec.value = m_value;
AppendUIEditorBoolField(
drawList,
layout.fieldRect,
previewSpec,
m_interactionState.fieldState,
boolPalette,
boolMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorBoolFieldSpec m_spec = {};
bool m_value = false;
UIEditorBoolFieldInteractionState m_interactionState = {};
UIEditorBoolFieldInteractionFrame m_frame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_color_field_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_color_field_basic_validation
OUTPUT_NAME "XCUIEditorColorFieldBasicValidation"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -0,0 +1,751 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Fields/UIEditorColorFieldInteraction.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorColorField.h>
#include "EditorValidationTheme.h"
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cstdio>
#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::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::UIEditorColorFieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorColorFieldInteractionResult;
using XCEngine::UI::Editor::UIEditorColorFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorColorFieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorColorField;
using XCEngine::UI::Editor::Widgets::FormatUIEditorColorFieldHexText;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorColorField;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldSpec;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorColorFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ColorField 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 IsTruthyEnvironmentFlag(const char* name) {
const char* value = std::getenv(name);
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 &&
y >= rect.y &&
y <= rect.y + rect.height;
}
ScenarioLayout BuildScenarioLayout(
float width,
float height,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
const float margin = shellMetrics.margin;
constexpr float leftWidth = 456.0f;
const float gap = shellMetrics.gap;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 248.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)(250.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
layout.previewRect = UIRect(
leftWidth + margin * 2.0f,
margin,
(std::max)(520.0f, width - leftWidth - margin * 3.0f),
height - margin * 2.0f);
layout.fieldRect = UIRect(
layout.previewRect.x + 28.0f,
layout.previewRect.y + 72.0f,
360.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, "é‡<EFBFBD>ç½®", 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 UIEditorColorFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorColorFieldHitTargetKind::Swatch:
return "swatch";
case UIEditorColorFieldHitTargetKind::PopupCloseButton:
return "popup_close";
case UIEditorColorFieldHitTargetKind::HueWheel:
return "popup_hue_wheel";
case UIEditorColorFieldHitTargetKind::SaturationValue:
return "popup_sv_square";
case UIEditorColorFieldHitTargetKind::RedChannel:
return "popup_red_channel";
case UIEditorColorFieldHitTargetKind::GreenChannel:
return "popup_green_channel";
case UIEditorColorFieldHitTargetKind::BlueChannel:
return "popup_blue_channel";
case UIEditorColorFieldHitTargetKind::AlphaChannel:
return "popup_alpha_channel";
case UIEditorColorFieldHitTargetKind::PopupSurface:
return "popup_surface";
case UIEditorColorFieldHitTargetKind::Row:
return "row";
case UIEditorColorFieldHitTargetKind::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;
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr && wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "已请求截图,输出�captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1540,
940,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/color_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
ResetScenario();
if (IsTruthyEnvironmentFlag("XCUI_COLOR_FIELD_OPEN_POPUP_ON_STARTUP")) {
const UIPoint swatchPoint(
m_frame.layout.swatchRect.x + 4.0f,
m_frame.layout.swatchRect.y + 4.0f);
PumpEvents({
MakePointerEvent(
UIInputEventType::PointerButtonDown,
swatchPoint,
UIPointerButton::Left),
MakePointerEvent(
UIInputEventType::PointerButtonUp,
swatchPoint,
UIPointerButton::Left)
});
m_lastResult = "已自动打开 ColorField 弹窗";
}
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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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::GetEditorValidationShellMetrics());
}
UIRect GetViewportRect() const {
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
return UIRect(
0.0f,
0.0f,
static_cast<float>((std::max)(1L, clientRect.right - clientRect.left)),
static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top)));
}
void ResetScenario() {
m_spec = {};
m_spec.fieldId = "tint";
m_spec.label = "Tint";
m_spec.value = XCEngine::UI::UIColor(0.84f, 0.42f, 0.28f, 0.65f);
m_spec.showAlpha = true;
m_interactionState = {};
m_interactionState.colorFieldState.focused = true;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "å·²é‡<EFBFBD>置到默认 ColorField 状æ€?;
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics();
m_frame = UpdateUIEditorColorFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
{},
metrics,
GetViewportRect());
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshFrame();
}
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);
UpdateResultText(PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) }));
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
UpdateResultText(
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) }));
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
UpdateResultText(
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) }));
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorColorFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics();
m_frame = UpdateUIEditorColorFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
std::move(events),
metrics,
GetViewportRect());
return m_frame.result;
}
void UpdateResultText(const UIEditorColorFieldInteractionResult& result) {
if (result.colorChanged) {
m_lastResult = "颜色值已更新";
return;
}
if (result.popupOpened) {
m_lastResult = "已打开拾色弹窗";
return;
}
if (result.popupClosed) {
m_lastResult = "已关闭拾色弹�;
return;
}
if (result.consumed) {
m_lastResult = "控件已消费输�;
return;
}
m_lastResult = "等待交互";
}
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;
}
}
std::string BuildColorSummary() const {
return XCEngine::UI::Editor::Widgets::FormatUIEditorColorFieldRgbaText(m_spec);
}
void RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
const UIRect viewportRect = GetViewportRect();
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();
const auto fieldPalette = XCEngine::UI::Editor::ResolveUIEditorColorFieldPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorColorFieldBasic");
drawList.AddFilledRect(viewportRect, shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个æµè¯•在验è¯<EFBFBD>什么功能?",
"验è¯<EFBFBD> Editor ColorField çš?swatchã€<C3A3>popupã€<C3A3>SV squareã€<C3A3>hue wheelã€<C3A3>RGBA slider 和关闭行为ã€?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 swatch,打开 popup�,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 拖动 SV square,检查颜色实时å<C2B6>˜åŒã€?,
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,检查结果å<C593>Œæ­¥ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 点击 close 或外部区域,关闭 popup�,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 左侧é‡<C3A9>ç¹çœ?Hexã€<C3A3>RGBAã€<C3A3>Popup å’?Resultã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "æ“<EFBFBD>作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状æ€<EFBFBD>æ˜è¦?,
"é‡<EFBFBD>ç¹çœ?hoverã€<C3A3>popupã€<C3A3>hexã€<C3A3>rgba 和结果ã€?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Popup: ") + (m_interactionState.colorFieldState.popupOpen ? "open" : "closed"),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
"Hex: " + FormatUIEditorColorFieldHexText(m_spec),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
BuildColorSummary(),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队�.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/color_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"ColorField 预览",
"这里å<EFBFBD>ªæ”¾ä¸€ä¸?ColorField,用æ<C2A8>¥éªŒè¯?Editor 基础字段çš?popup 交互ã€?);
AppendUIEditorColorField(
drawList,
layout.fieldRect,
m_spec,
m_interactionState.colorFieldState,
fieldPalette,
fieldMetrics,
viewportRect);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(viewportRect.width),
static_cast<unsigned int>(viewportRect.height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorColorFieldSpec m_spec = {};
UIEditorColorFieldInteractionState m_interactionState = {};
UIEditorColorFieldInteractionFrame m_frame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_context_menu_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_context_menu_basic_validation
OUTPUT_NAME "XCUIEditorContextMenuBasicValidation"
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_dock_host_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_dock_host_basic_validation
OUTPUT_NAME "XCUIEditorDockHostBasicValidation"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -0,0 +1,676 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Docking/UIEditorDockHostInteraction.h>
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
#include <XCEditor/Docking/UIEditorDockHost.h>
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <sstream>
#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::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::BuildDefaultUIEditorWorkspaceController;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::CollectUIEditorWorkspaceVisiblePanels;
using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName;
using XCEngine::UI::Editor::GetUIEditorWorkspaceLayoutOperationStatusName;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::UIEditorDockHostInteractionFrame;
using XCEngine::UI::Editor::UIEditorDockHostInteractionResult;
using XCEngine::UI::Editor::UIEditorDockHostInteractionState;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
using XCEngine::UI::Editor::UIEditorWorkspaceController;
using XCEngine::UI::Editor::UIEditorWorkspaceLayoutOperationStatus;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandStatus;
using XCEngine::UI::Editor::UpdateUIEditorDockHostInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorDockHostHitTargetKind;
using XCEngine::UI::Editor::Widgets::AppendUIEditorDockHostBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorDockHostForeground;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorDockHostBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | DockHost 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 kPreviewBg(0.13f, 0.13f, 0.13f, 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);
constexpr UIColor kDanger(0.78f, 0.36f, 0.36f, 1.0f);
enum class ActionId : unsigned char {
Reset = 0,
Capture
};
struct ButtonState {
ActionId action = ActionId::Reset;
std::string label = {};
UIRect rect = {};
bool hovered = false;
};
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::string FormatBool(bool value) {
return value ? "true" : "false";
}
std::string FormatFloat(float value, int precision = 2) {
std::ostringstream stream = {};
stream.setf(std::ios::fixed, std::ios::floatfield);
stream.precision(precision);
stream << value;
return stream.str();
}
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::PanelHeader:
return "PanelHeader: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelBody:
return "PanelBody: " + target.panelId;
case UIEditorDockHostHitTargetKind::PanelFooter:
return "PanelFooter: " + target.panelId;
case UIEditorDockHostHitTargetKind::None:
default:
return "None";
}
}
std::string JoinVisiblePanelIds(const UIEditorWorkspaceController& controller) {
const auto panels = CollectUIEditorWorkspaceVisiblePanels(
controller.GetWorkspace(),
controller.GetSession());
if (panels.empty()) {
return "(none)";
}
std::ostringstream stream = {};
for (std::size_t index = 0; index < panels.size(); ++index) {
if (index > 0u) {
stream << ", ";
}
stream << panels[index].panelId;
}
return stream.str();
}
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);
}
}
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);
}
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "doc-a", "Document A", {}, true, true, true },
{ "doc-b", "Document B", {}, true, true, true },
{ "details", "Details", {}, true, true, true },
{ "console", "Console", {}, true, true, true }
};
return registry;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-split",
UIEditorWorkspaceSplitAxis::Horizontal,
0.5f,
BuildUIEditorWorkspaceTabStack(
"document-tabs",
{
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
},
1u),
BuildUIEditorWorkspaceSplit(
"right-split",
UIEditorWorkspaceSplitAxis::Vertical,
0.6f,
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true),
BuildUIEditorWorkspacePanel("console-node", "console", "Console", true)));
workspace.activePanelId = "doc-b";
return workspace;
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
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)));
}
return 0;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_MOUSEMOVE:
if (app != nullptr) {
if (!app->m_trackingMouseLeave) {
TRACKMOUSEEVENT trackMouseEvent = {};
trackMouseEvent.cbSize = sizeof(trackMouseEvent);
trackMouseEvent.dwFlags = TME_LEAVE;
trackMouseEvent.hwndTrack = hwnd;
if (TrackMouseEvent(&trackMouseEvent)) {
app->m_trackingMouseLeave = true;
}
}
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->m_trackingMouseLeave = false;
UIInputEvent event = {};
event.type = UIInputEventType::PointerLeave;
app->m_pendingInputEvents.push_back(event);
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
SetFocus(hwnd);
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_SETFOCUS:
if (app != nullptr) {
UIInputEvent event = {};
event.type = UIInputEventType::FocusGained;
app->m_pendingInputEvents.push_back(event);
return 0;
}
break;
case WM_KILLFOCUS:
if (app != nullptr) {
UIInputEvent event = {};
event.type = UIInputEventType::FocusLost;
app->m_pendingInputEvents.push_back(event);
return 0;
}
break;
case WM_CAPTURECHANGED:
if (app != nullptr &&
app->m_interactionState.splitterDragState.active &&
reinterpret_cast<HWND>(lParam) != hwnd) {
UIInputEvent event = {};
event.type = UIInputEventType::FocusLost;
app->m_pendingInputEvents.push_back(event);
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
return 0;
}
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/dock_host_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
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,
1540,
940,
nullptr,
nullptr,
hInstance,
this);
if (m_hwnd == nullptr) {
return false;
}
ShowWindow(m_hwnd, nCmdShow);
if (!m_renderer.Initialize(m_hwnd)) {
return false;
}
ResetScenario();
return true;
}
void Shutdown() {
if (GetCapture() == m_hwnd) {
ReleaseCapture();
}
m_autoScreenshot.Shutdown();
m_renderer.Shutdown();
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
DestroyWindow(m_hwnd);
}
if (m_windowClassAtom != 0) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
}
}
void ResetScenario() {
if (GetCapture() == m_hwnd) {
ReleaseCapture();
}
m_controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
m_interactionState = {};
m_cachedFrame = {};
m_pendingInputEvents.clear();
m_lastStatus = "Ready";
m_lastMessage = "等待交äºã€è¿™é‡Œå<EFBFBD>ªéªŒè¯<EFBFBD> DockHost 基础交互 contract,ä¸<C3A4>接旧 editor 业务ã€?;
m_lastColor = kWarning;
}
void UpdateLayout() {
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));
constexpr float padding = 20.0f;
constexpr float leftWidth = 430.0f;
m_introRect = UIRect(padding, padding, leftWidth, 236.0f);
m_controlsRect = UIRect(padding, 272.0f, leftWidth, 116.0f);
m_stateRect = UIRect(padding, 404.0f, leftWidth, height - 424.0f);
m_previewRect = UIRect(
leftWidth + padding * 2.0f,
padding,
width - leftWidth - padding * 3.0f,
height - padding * 2.0f);
m_dockHostRect = UIRect(
m_previewRect.x + 18.0f,
m_previewRect.y + 54.0f,
m_previewRect.width - 36.0f,
m_previewRect.height - 72.0f);
const float buttonWidth = (m_controlsRect.width - 32.0f - 12.0f) * 0.5f;
const float left = m_controlsRect.x + 16.0f;
const float top = m_controlsRect.y + 62.0f;
m_buttons = {
{ ActionId::Reset, "é‡<EFBFBD>ç½®", UIRect(left, top, buttonWidth, 36.0f), false },
{ ActionId::Capture, "截图(F12)", UIRect(left + buttonWidth + 12.0f, top, buttonWidth, 36.0f), false }
};
}
void HandleMouseMove(float x, float y) {
UpdateLayout();
for (ButtonState& button : m_buttons) {
button.hovered = ContainsPoint(button.rect, x, y);
}
UIInputEvent event = {};
event.type = UIInputEventType::PointerMove;
event.position = UIPoint(x, y);
m_pendingInputEvents.push_back(event);
}
void HandleLeftButtonDown(float x, float y) {
UpdateLayout();
for (const ButtonState& button : m_buttons) {
if (ContainsPoint(button.rect, x, y)) {
ExecuteAction(button.action);
return;
}
}
UIInputEvent event = {};
event.type = UIInputEventType::PointerButtonDown;
event.position = UIPoint(x, y);
event.pointerButton = UIPointerButton::Left;
m_pendingInputEvents.push_back(event);
}
void HandleLeftButtonUp(float x, float y) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerButtonUp;
event.position = UIPoint(x, y);
event.pointerButton = UIPointerButton::Left;
m_pendingInputEvents.push_back(event);
}
void ExecuteAction(ActionId action) {
if (action == ActionId::Reset) {
ResetScenario();
m_lastStatus = "Ready";
m_lastMessage = "场景状æ€<EFBFBD>å·²é‡<EFBFBD>ç½®ã€è¯·é‡<EFBFBD>æ°æ£€æŸ?splitter drag / tab activate / panel close / active panel syncã€?;
m_lastColor = kWarning;
return;
}
m_autoScreenshot.RequestCapture("manual_button");
m_lastStatus = "Ready";
m_lastMessage = "截图已排队,输出�tests/UI/Editor/manual_validation/shell/dock_host_basic/captures/�;
m_lastColor = kWarning;
}
void ApplyHostCaptureRequests(const UIEditorDockHostInteractionResult& result) {
if (result.requestPointerCapture && GetCapture() != m_hwnd) {
SetCapture(m_hwnd);
}
if (result.releasePointerCapture && GetCapture() == m_hwnd) {
ReleaseCapture();
}
}
void SetInteractionResult(const UIEditorDockHostInteractionResult& result) {
if (result.layoutResult.status != UIEditorWorkspaceLayoutOperationStatus::Rejected) {
m_lastStatus = std::string(GetUIEditorWorkspaceLayoutOperationStatusName(result.layoutResult.status));
m_lastMessage = result.layoutResult.message.empty()
? std::string("Layout æ“<C3A6>作已完æˆ<C3A6>ã€?)
: result.layoutResult.message;
m_lastColor =
result.layoutResult.status == UIEditorWorkspaceLayoutOperationStatus::Changed
? kSuccess
: kWarning;
return;
}
if (result.commandResult.status != UIEditorWorkspaceCommandStatus::Rejected) {
m_lastStatus = std::string(GetUIEditorWorkspaceCommandStatusName(result.commandResult.status));
m_lastMessage = result.commandResult.message.empty()
? std::string("Workspace å½ä»¤å·²å®Œæˆ<C3A6>ã€?)
: result.commandResult.message;
m_lastColor =
result.commandResult.status == UIEditorWorkspaceCommandStatus::Changed
? kSuccess
: kWarning;
return;
}
if (result.requestPointerCapture) {
m_lastStatus = "Capture";
m_lastMessage = "Splitter drag 已开始,宿主已收�pointer capture 请求�;
m_lastColor = kSuccess;
return;
}
if (result.releasePointerCapture) {
m_lastStatus = "Release";
m_lastMessage = "Splitter drag 已结æ<E2809C>Ÿï¼Œå®¿ä¸»å·²æ‰§è¡?pointer releaseã€?;
m_lastColor = kWarning;
return;
}
if (result.hitTarget.kind != UIEditorDockHostHitTargetKind::None) {
m_lastStatus = "Hover";
m_lastMessage = "当å‰<EFBFBD> hover 命中: " + DescribeHitTarget(result.hitTarget);
m_lastColor = kTextMuted;
return;
}
if (result.consumed) {
m_lastStatus = "Consumed";
m_lastMessage = "这次输入�DockHost 基础交互层消费�;
m_lastColor = kWarning;
}
}
void RenderFrame() {
UpdateLayout();
m_cachedFrame = UpdateUIEditorDockHostInteraction(
m_interactionState,
m_controller,
m_dockHostRect,
m_pendingInputEvents);
m_pendingInputEvents.clear();
ApplyHostCaptureRequests(m_cachedFrame.result);
SetInteractionResult(m_cachedFrame.result);
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));
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("DockHostBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(drawList, m_introRect, "这个æµè¯•验è¯<EFBFBD>什么功能?", "å<EFBFBD>ªéªŒè¯?DockHost 基础交互 contract,ä¸<C3A4>å<EFBFBD>?editor 业务é<C2A1>¢æ<C2A2>¿ã€?);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 72.0f), "1. 验è¯<C3A8> splitter drag 是å<C2AF>¦å<C2A6>ªé€šè¿‡ DockHostInteraction + WorkspaceController 完æˆ<C3A6>ã€?, kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 94.0f), "2. 验è¯<C3A8> unified dock:tab activate / single-tab body activate / panel closeã€?, kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 116.0f), "3. 验è¯<C3A8> active panelã€<C3A3>visible panelsã€<C3A3>split ratio 是å<C2AF>¦ç»Ÿä¸€æ”¶å<C2B6>£åˆ?controllerã€?, kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 138.0f), "4. 验è¯<C3A8> pointer capture / release è¯·æ±æ˜¯å<C2AF>¦é€šè¿‡ contract 明确返回ã€?, kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 162.0f), "建议æ“<EFBFBD>作:先æä¸­é—?splitter,å†<C3A5>ç?Document Aã€?, kTextWeak, 11.0f);
drawList.AddText(UIPoint(m_introRect.x + 16.0f, m_introRect.y + 180.0f), "ç„¶å<EFBFBD>Žåˆ‡æ<EFBFBD>¢ Document A / B,最å<E282AC>Žç¹ Details æˆ?Console çš?Xã€?, kTextWeak, 11.0f);
DrawCard(drawList, m_controlsRect, "æ“<EFBFBD>作", "这里å<EFBFBD>ªä¿<EFBFBD>留当å‰<EFBFBD>场景必è¦<EFBFBD>按é®ã€?);
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, m_stateRect, "状æ€?, "é<EFBFBD>ç¹æ£æŸ?DockHost 基ç¡äº¤äºå½å<EFBFBD>çŠæ<EFBFBD>ã?);
float stateY = m_stateRect.y + 66.0f;
auto addStateLine = [&](std::string text, const UIColor& color = kTextPrimary, float fontSize = 12.0f) {
drawList.AddText(UIPoint(m_stateRect.x + 16.0f, stateY), std::move(text), color, fontSize);
stateY += 20.0f;
};
addStateLine("Hover: " + DescribeHitTarget(m_interactionState.dockHostState.hoveredTarget), kTextPrimary, 11.0f);
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("当å‰<EFBFBD>é<EFBFBD>¢æ<EFBFBD>¿: " + (m_controller.GetWorkspace().activePanelId.empty() ? std::string("(none)") : m_controller.GetWorkspace().activePanelId));
addStateLine("å<EFBFBD>¯è§<EFBFBD>é<EFBFBD>¢æ<EFBFBD>¿: " + JoinVisiblePanelIds(m_controller), kTextWeak, 11.0f);
addStateLine("焦点: " + FormatBool(m_cachedFrame.focused), m_cachedFrame.focused ? kSuccess : kTextMuted);
addStateLine("æ<EFBFBD>•获: " + FormatBool(GetCapture() == m_hwnd), GetCapture() == m_hwnd ? kSuccess : kTextMuted);
addStateLine(
"拖拽 Splitter: " +
(m_interactionState.dockHostState.activeSplitterNodeId.empty()
? std::string("(none)")
: m_interactionState.dockHostState.activeSplitterNodeId),
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(
"å<EFBFBD>³ä¾§åˆ†å‰²æ¯”ä¾: " +
FormatFloat(m_controller.GetWorkspace().root.children[1].splitRatio),
kTextWeak,
11.0f);
}
addStateLine(
"截图: " +
(m_autoScreenshot.HasPendingCapture()
? std::string("截图排队�..")
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 �按钮 -> captures/")
: m_autoScreenshot.GetLastCaptureSummary())),
kTextWeak,
11.0f);
DrawCard(drawList, m_previewRect, "预览", "真实 DockHost 交äºé¢„览,å<C592>ªçœåŸºç¡€å±ï¼Œä¸<C3A4>接æ—?editorã€?);
drawList.AddFilledRect(m_dockHostRect, kPreviewBg, 8.0f);
AppendUIEditorDockHostBackground(drawList, m_cachedFrame.layout);
AppendUIEditorDockHostForeground(drawList, m_cachedFrame.layout);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorWorkspaceController m_controller = {};
UIEditorDockHostInteractionState m_interactionState = {};
UIEditorDockHostInteractionFrame m_cachedFrame = {};
std::vector<UIInputEvent> m_pendingInputEvents = {};
std::vector<ButtonState> m_buttons = {};
UIRect m_introRect = {};
UIRect m_controlsRect = {};
UIRect m_stateRect = {};
UIRect m_previewRect = {};
UIRect m_dockHostRect = {};
bool m_trackingMouseLeave = false;
std::string m_lastStatus = {};
std::string m_lastMessage = {};
UIColor m_lastColor = kTextMuted;
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_editor_shell_compose_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_editor_shell_compose_validation
OUTPUT_NAME "XCUIEditorShellComposeValidation"
)

View File

@@ -0,0 +1,585 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Panels/UIEditorPanelRegistry.h>
#include <XCEditor/Shell/UIEditorShellCompose.h>
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
#include <XCEditor/Workspace/UIEditorWorkspaceModel.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include "../../shared/src/EditorValidationTheme.h"
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
namespace {
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::UISize;
using XCEngine::UI::Editor::AppendUIEditorShellCompose;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
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;
using XCEngine::UI::Editor::UIEditorShellComposeFrame;
using XCEngine::UI::Editor::UIEditorShellComposeModel;
using XCEngine::UI::Editor::UIEditorShellComposeRequest;
using XCEngine::UI::Editor::UIEditorShellComposeState;
using XCEngine::UI::Editor::UIEditorWorkspaceCommand;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandResult;
using XCEngine::UI::Editor::UIEditorWorkspaceController;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
using XCEngine::UI::Editor::UpdateUIEditorShellCompose;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
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 锟洁辑??| 锟斤拷锟斤拷锟斤拷锟?;
constexpr UIColor kSuccess(0.46f, 0.72f, 0.50f, 1.0f);
constexpr UIColor kDanger(0.78f, 0.35f, 0.35f, 1.0f);
enum class ActionId : unsigned char {
ActivateScene = 0,
ActivateDocument,
ToggleTopBar,
ToggleBottomBar,
ToggleTexture,
Reset,
Capture
};
struct ButtonState {
ActionId action = ActionId::ActivateScene;
std::string label = {};
UIRect rect = {};
bool selected = false;
};
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::string FormatFloat(float value) {
std::ostringstream stream = {};
stream.setf(std::ios::fixed, std::ios::floatfield);
stream.precision(1);
stream << value;
return stream.str();
}
std::string FormatRect(const UIRect& rect) {
return "x=" + FormatFloat(rect.x) +
" y=" + FormatFloat(rect.y) +
" w=" + FormatFloat(rect.width) +
" h=" + FormatFloat(rect.height);
}
std::string FormatSize(const UISize& size) {
return FormatFloat(size.width) + " x " + FormatFloat(size.height);
}
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.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", "灞傜骇", 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;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-left-main",
UIEditorWorkspaceSplitAxis::Horizontal,
0.23f,
BuildUIEditorWorkspacePanel("hierarchy-node", "hierarchy", "灞傜骇", true),
BuildUIEditorWorkspaceSplit(
"main-center-right",
UIEditorWorkspaceSplitAxis::Horizontal,
0.72f,
BuildUIEditorWorkspaceTabStack(
"center-tabs",
{
BuildUIEditorWorkspacePanel("scene-node", "scene", "鍦烘櫙"),
BuildUIEditorWorkspacePanel("document-node", "document", "鏂囨。", true)
},
0u),
BuildUIEditorWorkspacePanel("inspector-node", "inspector", "妫€瑙嗗櫒", true)));
workspace.activePanelId = "scene";
return workspace;
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
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)));
}
return 0;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleClick(static_cast<float>(GET_X_LPARAM(lParam)), static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr && wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
InvalidateRect(hwnd, nullptr, FALSE);
UpdateWindow(hwnd);
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/editor_shell_compose/captures";
m_autoScreenshot.Initialize(m_captureRoot);
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,
1520,
940,
nullptr,
nullptr,
hInstance,
this);
if (m_hwnd == nullptr) {
return false;
}
ShowWindow(m_hwnd, nCmdShow);
if (!m_renderer.Initialize(m_hwnd)) {
return false;
}
ResetScenario();
return true;
}
void Shutdown() {
m_autoScreenshot.Shutdown();
m_renderer.Shutdown();
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
DestroyWindow(m_hwnd);
}
if (m_windowClassAtom != 0) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
}
}
void ResetScenario() {
m_controller = BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
m_shellState = {};
m_showTopBar = true;
m_showBottomBar = true;
m_textureEnabled = true;
m_lastResult = "灏辩华";
}
void UpdateLayoutForCurrentWindow() {
RECT clientRect = {};
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));
constexpr float outerPadding = 20.0f;
constexpr float leftColumnWidth = 460.0f;
m_introRect = UIRect(outerPadding, outerPadding, leftColumnWidth, 188.0f);
m_controlsRect = UIRect(outerPadding, 224.0f, leftColumnWidth, 340.0f);
m_stateRect = UIRect(outerPadding, 580.0f, leftColumnWidth, height - 600.0f);
m_previewRect = UIRect(
leftColumnWidth + outerPadding * 2.0f,
outerPadding,
width - leftColumnWidth - outerPadding * 3.0f,
height - outerPadding * 2.0f);
m_shellRect = UIRect(
m_previewRect.x + 18.0f,
m_previewRect.y + 54.0f,
m_previewRect.width - 36.0f,
m_previewRect.height - 72.0f);
const float buttonHeight = 32.0f;
const float gap = 8.0f;
const float left = m_controlsRect.x + 16.0f;
const float top = m_controlsRect.y + 64.0f;
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("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, "Capture", UIRect(left, top + (buttonHeight + gap) * 6.0f, widthAvailable, buttonHeight), false }
};
}
UIEditorShellComposeModel BuildShellModel() const {
UIEditorShellComposeModel model = {};
model.menuBarItems = {
UIEditorMenuBarItem{ "file", "File", true, 0.0f },
UIEditorMenuBarItem{ "window", "Window", true, 0.0f },
UIEditorMenuBarItem{ "layout", "Layout", true, 0.0f }
};
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{ "branch", m_textureEnabled ? "Texture" : "Fallback", UIEditorStatusBarSlot::Trailing, {}, true, true, 96.0f }
};
UIEditorWorkspacePanelPresentationModel presentation = {};
presentation.panelId = "scene";
presentation.kind = UIEditorPanelPresentationKind::ViewportShell;
presentation.viewportShellModel.spec.chrome.title = "Scene";
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;
presentation.viewportShellModel.spec.chrome.bottomBarHeight = 28.0f;
presentation.viewportShellModel.spec.toolItems = {
{ "mode", "Perspective", XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot::Leading, true, true, 98.0f }
};
presentation.viewportShellModel.spec.statusSegments = {
{ "view", "Shell", UIEditorStatusBarSlot::Leading, {}, true, true, 64.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 = "Simulated viewport frame";
} else {
presentation.viewportShellModel.frame.hasTexture = false;
presentation.viewportShellModel.frame.statusText = "Simulated viewport frame";
}
model.workspacePresentations = { presentation };
return model;
}
std::string GetSelectedTabId() const {
if (!m_shellFrame.workspaceFrame.dockHostLayout.tabStacks.empty()) {
return m_shellFrame.workspaceFrame.dockHostLayout.tabStacks.front().selectedPanelId;
}
return m_controller.GetWorkspace().activePanelId;
}
void UpdateShellFrame() {
const UIEditorShellComposeModel model = BuildShellModel();
const auto metrics = ResolveUIEditorShellComposeMetrics();
m_shellRequest = ResolveUIEditorShellComposeRequest(
m_shellRect,
m_controller.GetPanelRegistry(),
m_controller.GetWorkspace(),
m_controller.GetSession(),
model,
{},
m_shellState,
metrics);
m_shellFrame = UpdateUIEditorShellCompose(
m_shellState,
m_shellRect,
m_controller.GetPanelRegistry(),
m_controller.GetWorkspace(),
m_controller.GetSession(),
model,
{},
{},
metrics);
m_cachedModel = model;
}
void HandleClick(float x, float y) {
for (const ButtonState& button : m_buttons) {
if (!ContainsPoint(button.rect, x, y)) {
continue;
}
ExecuteAction(button.action);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::ActivateScene:
DispatchWorkspaceCommand(UIEditorWorkspaceCommandKind::ActivatePanel, "scene", "鍒囧埌鍦烘櫙");
break;
case ActionId::ActivateDocument:
DispatchWorkspaceCommand(UIEditorWorkspaceCommandKind::ActivatePanel, "document", "鍒囧埌鏂囨。");
break;
case ActionId::ToggleTopBar:
m_showTopBar = !m_showTopBar;
m_lastResult = m_showTopBar ? "Top bar enabled" : "Top bar disabled";
break;
case ActionId::ToggleBottomBar:
m_showBottomBar = !m_showBottomBar;
m_lastResult = m_showBottomBar ? "Bottom bar enabled" : "Bottom bar disabled";
break;
case ActionId::ToggleTexture:
m_textureEnabled = !m_textureEnabled;
m_lastResult = m_textureEnabled ? "Texture branch enabled" : "Fallback branch enabled";
break;
case ActionId::Reset:
ResetScenario();
m_lastResult = "Scenario reset";
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "Capture queued";
InvalidateRect(m_hwnd, nullptr, FALSE);
UpdateWindow(m_hwnd);
break;
}
}
void DispatchWorkspaceCommand(UIEditorWorkspaceCommandKind kind, std::string_view panelId, std::string_view label) {
UIEditorWorkspaceCommand command = {};
command.kind = kind;
command.panelId = std::string(panelId);
const UIEditorWorkspaceCommandResult result = m_controller.Dispatch(command);
m_lastResult =
std::string(label) + " -> " +
std::string(GetUIEditorWorkspaceCommandStatusName(result.status)) +
" | " + result.message;
}
void RenderFrame() {
UpdateLayoutForCurrentWindow();
UpdateShellFrame();
UpdateLayoutForCurrentWindow();
RECT clientRect = {};
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* viewportRequest =
FindUIEditorWorkspaceViewportPresentationRequest(m_shellRequest.workspaceRequest, "scene");
const std::string selectedPresentation =
GetSelectedTabId() == "scene" && viewportRequest != nullptr ? "ViewportShell" : "Placeholder";
const auto validation = m_controller.ValidateState();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorShellCompose");
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, "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("Requested viewport size: " + (viewportRequest != nullptr ? FormatSize(viewportRequest->viewportShellRequest.requestedViewportSize) : std::string("n/a")), kShellPalette.textPrimary);
addStateLine("Result: " + m_lastResult, kShellPalette.textMuted);
addStateLine(
validation.IsValid() ? "宸ヤ綔鍖烘牎楠岋細姝e父" : "宸ヤ綔鍖烘牎楠岋細" + validation.message,
validation.IsValid() ? kSuccess : kDanger,
11.0f);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "Capture queued..."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 or the Capture button -> editor_shell_compose/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
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(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorWorkspaceController m_controller = {};
UIEditorShellComposeState m_shellState = {};
UIEditorShellComposeRequest m_shellRequest = {};
UIEditorShellComposeFrame m_shellFrame = {};
UIEditorShellComposeModel m_cachedModel = {};
std::vector<ButtonState> m_buttons = {};
UIRect m_introRect = {};
UIRect m_controlsRect = {};
UIRect m_stateRect = {};
UIRect m_previewRect = {};
UIRect m_shellRect = {};
bool m_showTopBar = true;
bool m_showBottomBar = true;
bool m_textureEnabled = true;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_editor_shell_interaction_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_editor_shell_interaction_validation
OUTPUT_NAME "XCUIEditorShellInteractionValidation"
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_enum_field_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_enum_field_basic_validation
OUTPUT_NAME "XCUIEditorEnumFieldBasicValidation"
)

View File

@@ -0,0 +1,776 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Fields/UIEditorEnumFieldInteraction.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorEnumField.h>
#include <XCEditor/Menu/UIEditorMenuPopup.h>
#include "EditorValidationTheme.h"
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/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::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::UIEditorEnumFieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorEnumFieldInteractionResult;
using XCEngine::UI::Editor::UIEditorEnumFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorEnumFieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumField;
using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuPopup;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorEnumField;
using XCEngine::UI::Editor::Widgets::ResolveUIEditorEnumFieldValueText;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupInvalidIndex;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorEnumFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | EnumField 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 MapEnumFieldKey(UINT keyCode) {
switch (keyCode) {
case VK_UP:
return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN:
return static_cast<std::int32_t>(KeyCode::Down);
case VK_HOME:
return static_cast<std::int32_t>(KeyCode::Home);
case VK_END:
return static_cast<std::int32_t>(KeyCode::End);
case VK_RETURN:
return static_cast<std::int32_t>(KeyCode::Enter);
case VK_SPACE:
return static_cast<std::int32_t>(KeyCode::Space);
case VK_ESCAPE:
return static_cast<std::int32_t>(KeyCode::Escape);
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, 240.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)(250.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,
340.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, "é‡<EFBFBD>ç½®", 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 UIEditorEnumFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorEnumFieldHitTargetKind::DropdownArrow:
return "dropdown_arrow";
case UIEditorEnumFieldHitTargetKind::ValueBox:
return "value_box";
case UIEditorEnumFieldHitTargetKind::Row:
return "row";
case UIEditorEnumFieldHitTargetKind::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;
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "已请求截图,输出�captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
const std::int32_t keyCode = MapEnumFieldKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->HandleKeyDown(keyCode);
return 0;
}
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1480,
920,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/enum_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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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::GetEditorValidationShellMetrics());
}
UIRect GetViewportRect() const {
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
return UIRect(
0.0f,
0.0f,
static_cast<float>((std::max)(1L, clientRect.right - clientRect.left)),
static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top)));
}
void ResetScenario() {
m_spec = {};
m_spec.fieldId = "render_mode";
m_spec.label = "Render Mode";
m_spec.options = { "Opaque", "Cutout", "Fade", "Transparent" };
m_selectedIndex = 1u;
m_spec.selectedIndex = m_selectedIndex;
m_interactionState = {};
m_interactionState.fieldState.focused = true;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "å·²é‡<EFBFBD>置到默认 EnumField 状æ€?;
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics();
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics();
m_spec.selectedIndex = m_selectedIndex;
m_frame = UpdateUIEditorEnumFieldInteraction(
m_interactionState,
m_selectedIndex,
layout.fieldRect,
m_spec,
{},
fieldMetrics,
popupMetrics,
GetViewportRect());
m_spec.selectedIndex = m_selectedIndex;
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshFrame();
}
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);
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorEnumFieldInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleKeyDown(std::int32_t keyCode) {
const UIEditorEnumFieldInteractionResult result =
PumpEvents({ MakeKeyEvent(keyCode) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorEnumFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics();
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics();
m_spec.selectedIndex = m_selectedIndex;
m_frame = UpdateUIEditorEnumFieldInteraction(
m_interactionState,
m_selectedIndex,
layout.fieldRect,
m_spec,
std::move(events),
fieldMetrics,
popupMetrics,
GetViewportRect());
m_spec.selectedIndex = m_selectedIndex;
return m_frame.result;
}
void UpdateResultText(const UIEditorEnumFieldInteractionResult& result) {
if (result.selectionChanged && m_selectedIndex < m_spec.options.size()) {
m_lastResult = std::string("已切æ<EFBFBD>¢é€‰é¡¹: ") + m_spec.options[m_selectedIndex];
return;
}
if (result.popupOpened) {
m_lastResult = "䏿‰è<EFBFBD>œå<EFBFBD>•已展开";
return;
}
if (result.popupClosed) {
m_lastResult = "䏿‰è<EFBFBD>œå<EFBFBD>•已关é—?;
return;
}
if (result.consumed) {
m_lastResult = "控件已消费输�;
return;
}
m_lastResult = "等待交互";
}
std::string ResolveHighlightedText() const {
if (!m_frame.popupOpen ||
m_frame.popupState.hoveredIndex == UIEditorMenuPopupInvalidIndex ||
m_frame.popupState.hoveredIndex >= m_spec.options.size()) {
return "(none)";
}
return m_spec.options[m_frame.popupState.hoveredIndex];
}
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 RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
const UIRect viewportRect = GetViewportRect();
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();
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");
drawList.AddFilledRect(viewportRect, shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个æµè¯•在验è¯<EFBFBD>什么功能?",
"验è¯<EFBFBD>æžšä¸¾å­—æ®µçš„ä¸æ‰æ‰“å¼€/关闭ã€<C3A3>é”®ç˜åˆ‡æ<E280A1>¢ã€<C3A3>高亮å<C2AE>Œæ­¥ï¼Œä»¥å<C2A5>Šåºå®š Inspector 风格承载ã€?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 value box æˆ?dropdown arrow,应该打开æˆå…³é—­ä¸æ‰è<E280B0>œå<C593>•ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 打开å<E282AC>Žæ£€æŸ?hover åŒé«˜äº®é¡¹æ˜¯å<C2AF>¦å<C2A6>Œæ­¥æ´æ°ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 获得 focus å<>Žï¼ŒUp / Down / Home / End 切æ<E280A1>¢é«˜äº®ï¼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 是å<C2AF>¦å<C2A6>Œæ­¥ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. �F12 或点击截图按钮,确认自动截图路径正确�,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "æ“<EFBFBD>作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状æ€<EFBFBD>æ˜è¦?,
"é‡<EFBFBD>ç¹æ£€æŸ?hit / popup / highlight / selected / resultã€?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "æ˜? : "å<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Popup: ") + (m_frame.popupOpen ? "open" : "closed"),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
"Highlight: " + ResolveHighlightedText(),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Selected: " + ResolveUIEditorEnumFieldValueText(m_spec),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队�.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/enum_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"EnumField 预览",
"这里å<EFBFBD>ªæ”¾ä¸€ä¸ªåºå®šæ ·å¼<EFBFBD>的枚举字段预览ã€?);
AppendUIEditorEnumField(
drawList,
layout.fieldRect,
m_spec,
m_interactionState.fieldState,
enumPalette,
enumMetrics);
if (m_frame.popupOpen) {
AppendUIEditorMenuPopup(
drawList,
m_frame.popupLayout.popupRect,
m_frame.popupItems,
m_frame.popupState,
popupPalette,
popupMetrics);
}
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(viewportRect.width),
static_cast<unsigned int>(viewportRect.height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorEnumFieldSpec m_spec = {};
std::size_t m_selectedIndex = 0u;
UIEditorEnumFieldInteractionState m_interactionState = {};
UIEditorEnumFieldInteractionFrame m_frame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_list_view_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_list_view_basic_validation
OUTPUT_NAME "XCUIEditorListViewBasicValidation"
)

View File

@@ -0,0 +1,767 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Collections/UIEditorListView.h>
#include <XCEditor/Collections/UIEditorListViewInteraction.h>
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#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::Widgets::UISelectionModel;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::UIEditorListViewInteractionFrame;
using XCEngine::UI::Editor::UIEditorListViewInteractionResult;
using XCEngine::UI::Editor::UIEditorListViewInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorListViewInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorListViewBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorListViewForeground;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorListView;
using XCEngine::UI::Editor::Widgets::UIEditorListViewHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorListViewHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorListViewItem;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorListViewBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ListView Basic";
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 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 kTextSuccess(0.63f, 0.76f, 0.63f, 1.0f);
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
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 listRect = {};
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 MapListNavigationKey(UINT keyCode) {
switch (keyCode) {
case VK_UP:
return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN:
return static_cast<std::int32_t>(KeyCode::Down);
case VK_HOME:
return static_cast<std::int32_t>(KeyCode::Home);
case VK_END:
return static_cast<std::int32_t>(KeyCode::End);
default:
return static_cast<std::int32_t>(KeyCode::None);
}
}
ScenarioLayout BuildScenarioLayout(float width, float height) {
constexpr float margin = 20.0f;
constexpr float leftWidth = 430.0f;
constexpr float gap = 16.0f;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 214.0f);
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 92.0f);
layout.stateRect = UIRect(
margin,
layout.controlRect.y + layout.controlRect.height + gap,
leftWidth,
(std::max)(200.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.listRect = UIRect(
layout.previewRect.x + 18.0f,
layout.previewRect.y + 64.0f,
layout.previewRect.width - 36.0f,
layout.previewRect.height - 84.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 40.0f;
layout.buttons = {
{ ActionId::Reset, "é‡<EFBFBD>ç½®", 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,
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 + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
}
}
void DrawButton(
UIDrawList& drawList,
const ButtonLayout& button,
bool hovered) {
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
drawList.AddText(
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
button.label,
kTextPrimary,
12.0f);
}
std::vector<UIEditorListViewItem> BuildListItems() {
return {
{ "scene", "Scene.unity", "Scene | 最近修� 1 分钟�, 0.0f },
{ "material", "Metal.mat", "Material | 4 个属�, 0.0f },
{ "script", "PlayerController.cs", "C# Script | 1.8 KB", 0.0f },
{ "texture", "Checker.png", "Texture2D | 1024x1024", 0.0f },
{ "prefab", "Robot.prefab", "Prefab | 7 个组�, 0.0f }
};
}
std::string JoinItems(const std::vector<UIEditorListViewItem>& items) {
std::ostringstream stream = {};
for (std::size_t index = 0u; index < items.size(); ++index) {
if (index > 0u) {
stream << " | ";
}
stream << items[index].primaryText;
}
return stream.str();
}
std::string DescribeHitTarget(
const UIEditorListViewHitTarget& hitTarget,
const std::vector<UIEditorListViewItem>& items) {
if (hitTarget.itemIndex >= items.size()) {
return "æ—?;
}
return "row: " + items[hitTarget.itemIndex].primaryText;
}
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;
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_RBUTTONDOWN:
if (app != nullptr) {
app->HandleRightButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_RBUTTONUP:
if (app != nullptr) {
app->HandleRightButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "已请求截图,输出�captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
const std::int32_t keyCode = MapListNavigationKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->HandleNavigationKey(keyCode);
return 0;
}
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1480,
920,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/list_view_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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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);
}
void ResetScenario() {
m_items = BuildListItems();
m_selectionModel = {};
m_selectionModel.SetSelection("material");
m_interactionState = {};
m_interactionState.listViewState.focused = true;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "å·²é‡<EFBFBD>置到默认列表状æ€?;
RefreshListFrame();
}
void RefreshListFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
m_listFrame =
UpdateUIEditorListViewInteraction(
m_interactionState,
m_selectionModel,
layout.listRect,
m_items,
{});
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshListFrame();
}
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);
PumpListEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpListEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
PumpListEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const bool wasFocused = m_interactionState.listViewState.focused;
const bool insideList = ContainsPoint(layout.listRect, x, y);
const UIEditorListViewInteractionResult result =
PumpListEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result, wasFocused, insideList);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleRightButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
PumpListEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Right) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleRightButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const UIEditorListViewInteractionResult result =
PumpListEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Right) });
UpdateResultText(result, m_interactionState.listViewState.focused, true);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleNavigationKey(std::int32_t keyCode) {
const UIEditorListViewInteractionResult result =
PumpListEvents({ MakeKeyEvent(keyCode) });
UpdateResultText(result, m_interactionState.listViewState.focused, true);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorListViewInteractionResult PumpListEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
m_listFrame =
UpdateUIEditorListViewInteraction(
m_interactionState,
m_selectionModel,
layout.listRect,
m_items,
events);
return m_listFrame.result;
}
void UpdateResultText(
const UIEditorListViewInteractionResult& result,
bool wasFocused,
bool insideList) {
if (result.keyboardNavigated && !result.selectedItemId.empty()) {
m_lastResult = "键盘导航选择: " + result.selectedItemId;
return;
}
if (result.secondaryClicked && !result.selectedItemId.empty()) {
m_lastResult = "å<EFBFBD>³é”®å½ä¸­è¡? " + result.selectedItemId;
return;
}
if (result.selectionChanged && !result.selectedItemId.empty()) {
m_lastResult = "选中� " + result.selectedItemId;
return;
}
if (!insideList && wasFocused && !m_interactionState.listViewState.focused) {
m_lastResult = "点击列表外空ç™? focus 已清除,selection ä¿<C3A4>ç•™";
return;
}
if (insideList) {
m_lastResult = "点击列表内空ç™? å<>ªæ´æ?focus / hover";
return;
}
m_lastResult = "等待交互";
}
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 RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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 ScenarioLayout layout = BuildScenarioLayout(width, height);
RefreshListFrame();
const UIEditorListViewHitTarget currentHit =
HitTestUIEditorListView(m_listFrame.layout, m_mousePosition);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorListViewBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(
drawList,
layout.introRect,
"这个æµè¯•验è¯<EFBFBD>什么功能?",
"å<EFBFBD>ªéªŒè¯?Editor ListView 基础控件:行布局ã€<C3A3>å<EFBFBD>•选ã€<C3A3>focus åŒé”®ç˜å¯¼èˆªã€ä¸<C3A4>涉å<E280B0>Šä»»ä½•业务é<C2A1>¢æ<C2A2>¿ã€?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 检查列表行åžç´æŽå¸ƒæ˜¯å<C2AF>¦ç¨³å®šï¼šä¸»æ ‡é¢˜åŒæ¬¡æ ‡é¢˜ä¸<C3A4>能äºç¸æŒ¤åŽã€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 点击 row å<>ªåˆ‡æ<E280A1>?selectionï¼hoverã€<C3A3>selectedã€<C3A3>focused 必须能区分ã€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 列表获得 focus å<>Žï¼ŒæŒ?Up / Down / Home / End 应稳定移动当å‰<C3A5>选æ©ã€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. ç¹å‡»åˆ—表å¤ç©ºç™½å<C2BD>Žï¼Œfocus 应清除,ä½?selection å¿…é¡»ä¿<C3A4>ç•™ã€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. æŒ?F12 手动截图;设ç½?XCUI_AUTO_CAPTURE_ON_STARTUP=1 å<>¯å<C2AF>¯åŠ¨è‡ªåŠ¨æˆªå¾ã€?,
kTextPrimary,
12.0f);
DrawCard(drawList, layout.controlRect, "æ“<EFBFBD>作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(drawList, layout.stateRect, "状æ€<EFBFBD>æ˜è¦?, "é<EFBFBD>ç¹æ£æŸ?hit / focus / selection / current / itemsã?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit, m_items),
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.listViewState.focused ? "å¼€" : "å…?),
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
"Selected: " +
(m_selectionModel.HasSelection() ? m_selectionModel.GetSelectedId() : std::string("(none)")),
kTextSuccess,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
std::string("Current: ") +
(m_interactionState.keyboardNavigation.HasCurrentIndex()
? std::to_string(m_interactionState.keyboardNavigation.GetCurrentIndex())
: std::string("(none)")),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Items(" + std::to_string(m_items.size()) + "): " + JoinItems(m_items),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Result: " + m_lastResult,
kTextPrimary,
12.0f);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队�.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/list_view_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 216.0f),
captureSummary,
kTextWeak,
12.0f);
DrawCard(drawList, layout.previewRect, "ListView 预览", "这里å<EFBFBD>ªæ”¾ä¸€ä¸?ListView,ä¸<C3A4>æ··å…¥ Project / Console 等业务内容ã€?);
AppendUIEditorListViewBackground(
drawList,
m_listFrame.layout,
m_items,
m_selectionModel,
m_interactionState.listViewState);
AppendUIEditorListViewForeground(drawList, m_listFrame.layout, m_items);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
std::vector<UIEditorListViewItem> m_items = {};
UISelectionModel m_selectionModel = {};
UIEditorListViewInteractionState m_interactionState = {};
UIEditorListViewInteractionFrame m_listFrame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_list_view_inline_rename_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_list_view_inline_rename_validation
OUTPUT_NAME "XCUIEditorListViewInlineRenameValidation"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_list_view_multiselect_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_list_view_multiselect_validation
OUTPUT_NAME "XCUIEditorListViewMultiSelectValidation"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -0,0 +1,878 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Collections/UIEditorListView.h>
#include <XCEditor/Collections/UIEditorListViewInteraction.h>
#include "EditorValidationTheme.h"
#include "Rendering/Native/AutoScreenshot.h"
#include "Platform/Win32/InputModifierTracker.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::Tests::EditorUI::EditorValidationShellMetrics;
using XCEngine::Tests::EditorUI::EditorValidationShellPalette;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIInputModifiers;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Widgets::UISelectionModel;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::InputModifierTracker;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::UIEditorListViewInteractionFrame;
using XCEngine::UI::Editor::UIEditorListViewInteractionResult;
using XCEngine::UI::Editor::UIEditorListViewInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorListViewInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorListViewBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorListViewForeground;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorListView;
using XCEngine::UI::Editor::Widgets::UIEditorListViewHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorListViewItem;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorListViewMultiSelectValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ListView MultiSelect";
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 listRect = {};
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 MapListNavigationKey(UINT keyCode) {
switch (keyCode) {
case VK_UP:
return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN:
return static_cast<std::int32_t>(KeyCode::Down);
case VK_HOME:
return static_cast<std::int32_t>(KeyCode::Home);
case VK_END:
return static_cast<std::int32_t>(KeyCode::End);
default:
return static_cast<std::int32_t>(KeyCode::None);
}
}
ScenarioLayout BuildScenarioLayout(
float width,
float height,
const 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, 252.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)(260.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
layout.previewRect = UIRect(
leftWidth + margin * 2.0f,
margin,
(std::max)(520.0f, width - leftWidth - margin * 3.0f),
height - margin * 2.0f);
layout.listRect = UIRect(
layout.previewRect.x + 22.0f,
layout.previewRect.y + 72.0f,
layout.previewRect.width - 44.0f,
layout.previewRect.height - 104.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "é‡<EFBFBD>ç½®", 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 EditorValidationShellPalette& shellPalette,
const 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 EditorValidationShellPalette& shellPalette,
const 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::vector<UIEditorListViewItem> BuildListItems() {
return {
{ "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 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 },
{ "audio", "FactoryLoop.wav", "AudioClip | 44.1 kHz", 0.0f },
{ "timeline", "IntroPlayable.playable", "Timeline | 7 tracks", 0.0f },
{ "profile", "GameplayProfile.asset", "Volume Profile | 6 overrides", 0.0f }
};
}
std::string JoinSelectedIds(const UISelectionModel& selectionModel) {
const std::vector<std::string>& selectedIds = selectionModel.GetSelectedIds();
if (selectedIds.empty()) {
return "(none)";
}
std::ostringstream stream = {};
for (std::size_t index = 0u; index < selectedIds.size(); ++index) {
if (index > 0u) {
stream << " | ";
}
stream << selectedIds[index];
}
return stream.str();
}
std::string DescribeHitTarget(
const UIEditorListViewHitTarget& hitTarget,
const std::vector<UIEditorListViewItem>& items) {
if (hitTarget.itemIndex >= items.size()) {
return "(none)";
}
return "row: " + items[hitTarget.itemIndex].primaryText;
}
std::string DescribeModifiers(const UIInputModifiers& modifiers) {
std::ostringstream stream = {};
stream << "Ctrl " << (modifiers.control ? "on" : "off")
<< " | Shift " << (modifiers.shift ? "on" : "off")
<< " | Alt " << (modifiers.alt ? "on" : "off");
return stream.str();
}
UIInputEvent MakePointerEvent(
UIInputEventType type,
const UIPoint& position,
const UIInputModifiers& modifiers,
UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = position;
event.pointerButton = button;
event.modifiers = modifiers;
return event;
}
UIInputEvent MakeKeyEvent(
std::int32_t keyCode,
const UIInputModifiers& modifiers) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = keyCode;
event.modifiers = modifiers;
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)),
static_cast<std::size_t>(wParam));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)),
static_cast<std::size_t>(wParam));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)),
static_cast<std::size_t>(wParam));
return 0;
}
break;
case WM_RBUTTONDOWN:
if (app != nullptr) {
app->HandleRightButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)),
static_cast<std::size_t>(wParam));
return 0;
}
break;
case WM_RBUTTONUP:
if (app != nullptr) {
app->HandleRightButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)),
static_cast<std::size_t>(wParam));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
app->HandleKeyDown(wParam, lParam);
return 0;
}
break;
case WM_KEYUP:
case WM_SYSKEYUP:
if (app != nullptr) {
app->HandleKeyUp(wParam, lParam);
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1540,
940,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/list_view_multiselect/captures";
m_autoScreenshot.Initialize(m_captureRoot);
m_modifierTracker.SyncFromSystemState();
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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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::GetEditorValidationShellMetrics());
}
void ResetScenario() {
m_items = BuildListItems();
m_selectionModel = {};
m_selectionModel.SetSelections({ "material", "script" }, "script");
m_interactionState = {};
m_interactionState.listViewState.focused = true;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
m_hoveredAction = ActionId::Reset;
m_lastModifiers = {};
m_lastResult = "Reset to the default multiselect state.";
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
m_frame = UpdateUIEditorListViewInteraction(
m_interactionState,
m_selectionModel,
layout.listRect,
m_items,
{});
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshFrame();
}
void HandleMouseMove(float x, float y, std::size_t keyState) {
m_modifierTracker.SyncFromSystemState();
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);
const UIInputModifiers modifiers = m_modifierTracker.BuildPointerModifiers(keyState);
m_lastModifiers = modifiers;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition, modifiers) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition, {}) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y, std::size_t keyState) {
m_modifierTracker.SyncFromSystemState();
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIInputModifiers modifiers = m_modifierTracker.BuildPointerModifiers(keyState);
m_lastModifiers = modifiers;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, modifiers, UIPointerButton::Left) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y, std::size_t keyState) {
m_modifierTracker.SyncFromSystemState();
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const bool insideList = ContainsPoint(layout.listRect, x, y);
const bool wasFocused = m_interactionState.listViewState.focused;
const UIInputModifiers modifiers = m_modifierTracker.BuildPointerModifiers(keyState);
m_lastModifiers = modifiers;
const UIEditorListViewInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, modifiers, UIPointerButton::Left) });
UpdateResultText(result, insideList, wasFocused);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleRightButtonDown(float x, float y, std::size_t keyState) {
m_modifierTracker.SyncFromSystemState();
m_mousePosition = UIPoint(x, y);
const UIInputModifiers modifiers = m_modifierTracker.BuildPointerModifiers(keyState);
m_lastModifiers = modifiers;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, modifiers, UIPointerButton::Right) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleRightButtonUp(float x, float y, std::size_t keyState) {
m_modifierTracker.SyncFromSystemState();
m_mousePosition = UIPoint(x, y);
const UIInputModifiers modifiers = m_modifierTracker.BuildPointerModifiers(keyState);
m_lastModifiers = modifiers;
const UIEditorListViewInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, modifiers, UIPointerButton::Right) });
UpdateResultText(result, true, m_interactionState.listViewState.focused);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleKeyDown(WPARAM wParam, LPARAM lParam) {
const UIInputModifiers modifiers = m_modifierTracker.ApplyKeyMessage(UIInputEventType::KeyDown, wParam, lParam);
m_lastModifiers = modifiers;
if (wParam == VK_F12) {
m_autoScreenshot.RequestCapture("manual_f12");
m_lastResult = "�������ͼ�����??captures/latest.png";
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const std::int32_t keyCode = MapListNavigationKey(static_cast<UINT>(wParam));
if (keyCode == static_cast<std::int32_t>(KeyCode::None)) {
return;
}
const UIEditorListViewInteractionResult result =
PumpEvents({ MakeKeyEvent(keyCode, modifiers) });
UpdateResultText(result, true, true);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleKeyUp(WPARAM wParam, LPARAM lParam) {
m_lastModifiers = m_modifierTracker.ApplyKeyMessage(UIInputEventType::KeyUp, wParam, lParam);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorListViewInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
m_frame = UpdateUIEditorListViewInteraction(
m_interactionState,
m_selectionModel,
layout.listRect,
m_items,
std::move(events));
return m_frame.result;
}
void UpdateResultText(
const UIEditorListViewInteractionResult& result,
bool insideList,
bool wasFocused) {
if (result.secondaryClicked && !result.selectedItemId.empty()) {
m_lastResult =
"å<EFBFBD>³é”®å½ä¸­: " + result.selectedItemId +
" | primary=" + m_selectionModel.GetSelectedId() +
" | count=" + std::to_string(m_selectionModel.GetSelectionCount());
return;
}
if (result.keyboardNavigated && result.selectionChanged) {
m_lastResult = "键盘导航更新选集: " + JoinSelectedIds(m_selectionModel);
return;
}
if (result.keyboardNavigated && !result.selectedItemId.empty()) {
m_lastResult = "���̵���?? " + result.selectedItemId;
return;
}
if (result.selectionChanged) {
m_lastResult =
"ѡ���Ѹ�?? primary=" + m_selectionModel.GetSelectedId() +
" | ids=" + JoinSelectedIds(m_selectionModel);
return;
}
if (!insideList && wasFocused && !m_interactionState.listViewState.focused) {
m_lastResult = "ç¹å‡»åˆ—表å¤ç©ºç™½ï¼Œfocus 已清除,selection ä¿<C3A4>ç•™";
return;
}
if (insideList) {
m_lastResult = "����б��ڿհף�ֻ��?hover / focus";
return;
}
m_lastResult = "等待交互";
}
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 RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
const UIEditorListViewHitTarget currentHit =
HitTestUIEditorListView(m_frame.layout, m_mousePosition);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorListViewMultiSelect");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个æµè¯•验è¯<EFBFBD>什么功能?",
"å<EFBFBD>ªéªŒè¯?Editor ListView 多é€?contract:Ctrl/Shift 选é†ã€<C3A3>å<EFBFBD>³é”?primary 切æ<E280A1>¢ã€<C3A3>é”®ç˜èŒƒå´æ‰©å±•,ä¸<C3A4>涉å<E280B0>Šä»»ä½•业务é<C2A1>¢æ<C2A2>¿ã€?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. Ctrl+左键:对å<C2B9>•行å<C592>?add/remove,多选é†å<E280A0>ˆå¿…须稳定ä¿<C3A4>ç•™ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. Shift+左键:以 anchor ä¸ºèµ·ç¹æ‰©å±•范å´ï¼Œprimary 应切到当å‰<C3A5>ç¹å‡»è¡Œã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. å<>³é”®å½ä¸­å·²é€‰é†å<E280A0>ˆï¼šå<C5A1>ªåˆ‡æ<E280A1>?primary/context target,ä¸<C3A4>应破å<C2B4><C3A5>当å‰<C3A5>多选é†å<E280A0>ˆã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 列表获得 focus å<>ŽæŒ‰ Up/Down/Home/Endï¼æŒ‰ä½?Shift 时应扩展范å´ï¼Œä¸<C3A4>æŒ?Shift 时应åžåˆ°å<C2B0>•选ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. ç¹å‡»åˆ—表å¤ç©ºç™½å<C2BD>ªæ¸…除 focusï¼ç¹å‡»åˆ—表内空白å<C2BD>ªæ´æ?hover/focusï¼F12 æˆæŒ‰é®è§¦å<C2A6>截å¾ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "æ“<EFBFBD>作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状æ€<EFBFBD>æ˜è¦?,
"é‡<EFBFBD>ç¹æ£€æŸ?primary / count / ids / anchor / current / hit / resultã€?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit, m_items),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.listViewState.focused ? "on" : "off"),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
"Primary Selected: " +
(m_selectionModel.HasSelection() ? m_selectionModel.GetSelectedId() : std::string("(none)")),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
"Selected Count: " + std::to_string(m_selectionModel.GetSelectionCount()),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Selected IDs: " + JoinSelectedIds(m_selectionModel),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Anchor ID: " + (m_interactionState.selectionAnchorId.empty()
? std::string("(none)")
: m_interactionState.selectionAnchorId),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
std::string("Current: ") +
(m_interactionState.keyboardNavigation.HasCurrentIndex()
? std::to_string(m_interactionState.keyboardNavigation.GetCurrentIndex())
: std::string("(none)")),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
"Modifiers: " + DescribeModifiers(m_lastModifiers),
shellPalette.textMuted,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f),
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队�.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/list_view_multiselect/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 286.0f),
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"ListView 预览",
"这里å<EFBFBD>ªæ”¾ä¸€ä¸?ListViewã€é»˜è®¤çжæ€<C3A6>ä¸å·²é€‰ä¸­ material + script,æ¹ä¾¿ç´æŽ¥æ£€æŸ¥å¤šé€?contractã€?);
AppendUIEditorListViewBackground(
drawList,
m_frame.layout,
m_items,
m_selectionModel,
m_interactionState.listViewState);
AppendUIEditorListViewForeground(drawList, m_frame.layout, m_items);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
InputModifierTracker m_modifierTracker = {};
std::filesystem::path m_captureRoot = {};
std::vector<UIEditorListViewItem> m_items = {};
UISelectionModel m_selectionModel = {};
UIEditorListViewInteractionState m_interactionState = {};
UIEditorListViewInteractionFrame m_frame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
UIInputModifiers m_lastModifiers = {};
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_menu_bar_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_menu_bar_basic_validation
OUTPUT_NAME "XCUIEditorMenuBarBasicValidation"
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_number_field_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_number_field_basic_validation
OUTPUT_NAME "XCUIEditorNumberFieldBasicValidation"
)

View File

@@ -0,0 +1,824 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Fields/UIEditorNumberFieldInteraction.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Fields/UIEditorNumberField.h>
#include "EditorValidationTheme.h"
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/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::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::UIEditorNumberFieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorNumberFieldInteractionResult;
using XCEngine::UI::Editor::UIEditorNumberFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorNumberFieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorNumberField;
using XCEngine::UI::Editor::Widgets::FormatUIEditorNumberFieldValue;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorNumberFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | NumberField 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 inspectorRect = {};
UIRect inspectorHeaderRect = {};
UIRect sectionRect = {};
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 MapNumberFieldKey(UINT keyCode) {
switch (keyCode) {
case VK_LEFT:
return static_cast<std::int32_t>(KeyCode::Left);
case VK_RIGHT:
return static_cast<std::int32_t>(KeyCode::Right);
case VK_UP:
return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN:
return static_cast<std::int32_t>(KeyCode::Down);
case VK_HOME:
return static_cast<std::int32_t>(KeyCode::Home);
case VK_END:
return static_cast<std::int32_t>(KeyCode::End);
case VK_RETURN:
return static_cast<std::int32_t>(KeyCode::Enter);
case VK_ESCAPE:
return static_cast<std::int32_t>(KeyCode::Escape);
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 = 460.0f;
const float gap = shellMetrics.gap;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 250.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)(240.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.inspectorRect = UIRect(
layout.previewRect.x + 18.0f,
layout.previewRect.y + 54.0f,
(std::min)(392.0f, layout.previewRect.width - 36.0f),
150.0f);
layout.inspectorHeaderRect = UIRect(
layout.inspectorRect.x,
layout.inspectorRect.y,
layout.inspectorRect.width,
24.0f);
layout.sectionRect = UIRect(
layout.inspectorRect.x,
layout.inspectorRect.y + layout.inspectorHeaderRect.height,
layout.inspectorRect.width,
24.0f);
layout.fieldRect = UIRect(
layout.inspectorRect.x,
layout.sectionRect.y + layout.sectionRect.height + 2.0f,
layout.inspectorRect.width,
22.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "<EFBFBD>滨蔭", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
{ ActionId::Capture, "<EFBFBD>芸㦛(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
};
return layout;
}
XCEngine::UI::Editor::Widgets::UIEditorNumberFieldMetrics ResolvePropertyGridNumberFieldMetrics() {
return XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldMetrics(
XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(),
XCEngine::UI::Editor::ResolveUIEditorNumberFieldMetrics());
}
XCEngine::UI::Editor::Widgets::UIEditorNumberFieldPalette ResolvePropertyGridNumberFieldPalette() {
return XCEngine::UI::Editor::BuildUIEditorPropertyGridNumberFieldPalette(
XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(),
XCEngine::UI::Editor::ResolveUIEditorNumberFieldPalette());
}
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 UIEditorNumberFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorNumberFieldHitTargetKind::ValueBox:
return "value_box";
case UIEditorNumberFieldHitTargetKind::Row:
return "row";
case UIEditorNumberFieldHitTargetKind::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 MakeCharacterEvent(wchar_t character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = static_cast<std::uint32_t>(character);
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "撌脰窈瘙<EFBFBD><EFBFBD><EFBFBD>颲枏枂<EFBFBD>?captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
const std::int32_t keyCode = MapNumberFieldKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->HandleKeyDown(keyCode);
return 0;
}
}
break;
case WM_CHAR:
if (app != nullptr) {
app->HandleCharacter(static_cast<wchar_t>(wParam));
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1480,
920,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/number_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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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::GetEditorValidationShellMetrics());
}
void ResetScenario() {
m_spec = {};
m_spec.fieldId = "scale";
m_spec.label = "Scale";
m_spec.value = 1.25;
m_spec.step = 0.25;
m_spec.minValue = 0.0;
m_spec.maxValue = 4.0;
m_spec.integerMode = false;
m_spec.readOnly = false;
m_interactionState = {};
m_interactionState.numberFieldState.focused = true;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "撌脤<EFBFBD>蝵桀<EFBFBD>暺䁅恕 NumberField <20><EFBFBD>?;
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolvePropertyGridNumberFieldMetrics();
m_frame = UpdateUIEditorNumberFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
{},
metrics);
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshFrame();
}
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);
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorNumberFieldInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleKeyDown(std::int32_t keyCode) {
const UIEditorNumberFieldInteractionResult result = PumpEvents({ MakeKeyEvent(keyCode) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleCharacter(wchar_t character) {
if (character < 32) {
return;
}
const UIEditorNumberFieldInteractionResult result =
PumpEvents({ MakeCharacterEvent(character) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorNumberFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolvePropertyGridNumberFieldMetrics();
m_frame = UpdateUIEditorNumberFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
std::move(events),
metrics);
return m_frame.result;
}
void UpdateResultText(const UIEditorNumberFieldInteractionResult& result) {
if (result.editCommitRejected) {
m_lastResult = "<EFBFBD>𣂷漱憭梯揖嚗䔶<EFBFBD>靽脲<EFBFBD><EFBFBD><EFBFBD>颲𤑳𠶖<EFBFBD><EFBFBD><EFBFBD>?;
return;
}
if (result.editCommitted) {
m_lastResult = std::string("撌脫<EFBFBD>鈭斗㺭<EFBFBD>? ") + result.committedText;
return;
}
if (result.editCanceled) {
m_lastResult = "撌脣<EFBFBD><EFBFBD><EFBFBD>颲㻫<EFBFBD>?;
return;
}
if (result.editStarted) {
m_lastResult = "撌脰<EFBFBD><EFBFBD><EFBFBD>颲𤑳𠶖<EFBFBD><EFBFBD><EFBFBD>?;
return;
}
if (result.valueChanged || result.stepApplied) {
m_lastResult = std::string("<EFBFBD><EFBFBD>澆歇<EFBFBD>湔鰵: ") + FormatUIEditorNumberFieldValue(m_spec);
return;
}
if (result.consumed) {
m_lastResult = "<EFBFBD>找辣撌脫<EFBFBD>韐寡<EFBFBD><EFBFBD><EFBFBD>?;
return;
}
m_lastResult = "蝑匧<EFBFBD>鈭支<EFBFBD>";
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::Reset:
ResetScenario();
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "撌脰窈瘙<EFBFBD><EFBFBD><EFBFBD>颲枏枂<EFBFBD>?captures/latest.png";
break;
}
}
void RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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::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 = ResolvePropertyGridNumberFieldMetrics();
const auto numberPalette = ResolvePropertyGridNumberFieldPalette();
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorNumberFieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"餈嗘葵瘚贝<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>",
"撉諹<EFBFBD> Inspector 摰蹂蜓銝剔<E98A9D> NumberField 鈭支<E988AD>憟𤑳漲<F0A491B3><EFBFBD>霈文挪銝駁<E98A9D><E9A781><EFBFBD>?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. <20>孵稬 value box嚗峕<E59A97><E5B395>交糓<E4BAA4>西<EFBFBD><E8A5BF><EFBFBD>颲烐<E9A2B2><E78390><EFBFBD>憭𤥁<E686AD>摨娍糓 Unity 憌擧聢<E693A7><EFBFBD><E99697><EFBFBD><E4BAA4>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. <20><EFBFBD> focus <20><EFBFBD> Left / Right / Up / Down / Home / End嚗峕<E59A97><E5B395>仿睸<E4BBBF>䀹郊餈䜘<E9A488>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. <20>?Enter 餈𥕦<E9A488>蝻𤥁<E89DBB><F0A4A581><EFBFBD><EFBFBD><EFBFBD>湔𦻖颲枏<E9A2B2>摮㛖泵嚗𡻈nter commit嚗袏sc cancel<65>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 璉<><E79289>?Hover / Focus / Editing / Value / Result <20>臬炏<E887AC>峕郊<E5B395>湔鰵<E6B994>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. <20>?F12 <20>𣇉<EFBFBD><F0A38789>餅⏛<E9A485><EFBFBD><E69AB9><EFBFBD>蝖株恕<E6A0AA>芸𢆡<E88AB8>芸㦛頝臬<E9A09D><EFBFBD><EFBFBD>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "<EFBFBD><EFBFBD>");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"<EFBFBD><EFBFBD><EFBFBD><EFBFBD>閬?,
"<EFBFBD><EFBFBD><EFBFBD><EFBFBD>?hit / focus / editing / value / result<6C>?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.numberFieldState.focused ? "<EFBFBD>? : "<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Editing: ") + (m_interactionState.numberFieldState.editing ? "<EFBFBD>? : "<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
"Value: " + FormatUIEditorNumberFieldValue(m_spec),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Display: " + m_interactionState.numberFieldState.displayText,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "<EFBFBD>芸㦛<EFBFBD><EFBFBD>銝?.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/number_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
"Style: fixed",
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"NumberField 憸<><E686B8>",
"餈䠷<EFBFBD><EFBFBD><EFBFBD>閫?Inspector 摰蹂蜓銝剔<E98A9D> Unity 憌擧聢 Number 摮埈挾<E59F88>?);
drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor);
drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f);
drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground);
drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f);
drawList.AddText(
UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f),
"Inspector",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor);
drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f);
drawList.AddText(
UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f),
"v Transform",
propertyPalette.sectionTextColor,
shellMetrics.bodyFontSize);
AppendUIEditorNumberField(
drawList,
layout.fieldRect,
m_spec,
m_interactionState.numberFieldState,
numberPalette,
numberMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorNumberFieldSpec m_spec = {};
UIEditorNumberFieldInteractionState m_interactionState = {};
UIEditorNumberFieldInteractionFrame m_frame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_object_field_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_object_field_basic_validation
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 "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/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/manual_validation/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<65>悟ス灘燕蠑慕畑蟾イ貂<EFBDB2>ゥコ";
return;
}
if (result.focusChanged) {
m_lastResult = std::string("辟ヲ轤ケ蟾イ蛻<EFBFBD><EFBFBD>? ") + (m_interactionState.fieldState.focused ? "focused" : "blurred");
return;
}
if (result.consumed) {
m_lastResult = "霎灘<EFBFBD>蟾イ豸郁エ?;
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 = "謌ェ蝗セ蟾イ謗帝弌<EFBFBD>瑚セ灘<EFBFBD>蛻?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,
"霑吩クェ豬玖ッ募惠鬪瑚ッ∽サ€荵亥粥閭ス<EFBFBD><EFBFBD>",
"鬪瑚ッ<EFBFBD> Unity 鬟取<E9AC9F>シ ObjectField 逧<>€シ譯<EFBDBC>€∫アサ蝙区<E89D99><E58CBA>ュセ縲…lear / picker 謖蛾聴<E89BBE>御サ・蜿?focus縲∥ctivate縲…lear 螂醍コヲ縲?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 轤ケ蜃サ value box 謌?picker 謖蛾聴<E89BBE>悟コ碑ァヲ蜿<EFBDA6> activateRequested縲?,
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 轤ケ蜃サ clear 謖蛾聴<E89BBE><EFBFBD> focused 譌カ謖<EFBDB6> Delete / Backspace<63>悟コ疲ク<E796B2>ゥコ蠖灘燕蟇ケ雎。縲?,
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. focused 蜷取潔 Enter / Space<63>悟コ皮サァ扈ュ襍?activate 螂醍コヲ縲?,
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 驥咲せ譽€譟?Hover縲ocused縲as Value縲、ctivate Count縲ヽesult 譏ッ蜷ヲ蜷梧ュ・縲?,
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 謖?F12 謌也せ蜃サ謌ェ蝗セ謖蛾聴<E89BBE>悟庄蟇シ蜃コ蠖灘燕遯怜哨謌ェ蝗セ縲?,
m_shellPalette.textPrimary,
m_shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.controlRect,
m_shellPalette,
m_shellMetrics,
"謫堺ス<EFBFBD>",
"霑咎㈹蜿ェ菫晉蕗蠖灘燕蝨コ譎ッ髴€隕∫噪譛€蟆乗桃菴懊€?);
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縲ocus縲」alue縲∥ctivate縲〉esult縲?);
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/manual_validation/shell/object_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary()),
m_shellPalette.textWeak,
m_shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
m_shellPalette,
m_shellMetrics,
"ObjectField 鬚<><EFBFBD>",
"霑咎㈹蜿ェ謾セ荳€荳?Unity 鬟取<E9AC9F>シ ObjectField<6C>御ク肴キキ蜈・荳壼苅 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 = "謌ェ蝗セ蟾イ謗帝弌<EFBFBD>瑚セ灘<EFBFBD>蛻?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

@@ -0,0 +1,8 @@
add_executable(editor_ui_panel_content_host_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_panel_content_host_basic_validation
OUTPUT_NAME "XCUIEditorPanelContentHostBasicValidation"
)

View File

@@ -0,0 +1,610 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Panels/UIEditorPanelContentHost.h>
#include <XCEditor/Workspace/UIEditorWorkspaceCompose.h>
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
namespace {
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::AppendUIEditorWorkspaceCompose;
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceController;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::CollectUIEditorWorkspaceComposeExternalBodyPanelIds;
using XCEngine::UI::Editor::GetUIEditorPanelContentHostEventKindName;
using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::UIEditorPanelContentHostPanelState;
using XCEngine::UI::Editor::UIEditorPanelPresentationKind;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
using XCEngine::UI::Editor::UIEditorWorkspaceCommand;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
using XCEngine::UI::Editor::UIEditorWorkspaceComposeFrame;
using XCEngine::UI::Editor::UIEditorWorkspaceComposeState;
using XCEngine::UI::Editor::UIEditorWorkspaceController;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspacePanelPresentationModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
using XCEngine::UI::Editor::UpdateUIEditorWorkspaceCompose;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorPanelContentHostBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Panel Content Host";
constexpr UIColor kWindowBg(0.12f, 0.12f, 0.12f, 1.0f);
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 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 kSuccess(0.46f, 0.72f, 0.50f, 1.0f);
constexpr UIColor kWarning(0.82f, 0.68f, 0.36f, 1.0f);
constexpr UIColor kDanger(0.78f, 0.35f, 0.35f, 1.0f);
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
constexpr UIColor kButtonHover(0.33f, 0.33f, 0.33f, 1.0f);
constexpr UIColor kButtonBorder(0.46f, 0.46f, 0.46f, 1.0f);
constexpr UIColor kMountedFill(0.22f, 0.28f, 0.36f, 1.0f);
constexpr UIColor kMountedBorder(0.66f, 0.76f, 0.86f, 1.0f);
enum class ActionId : unsigned char {
ActivateDocA = 0,
ActivateDocB,
ActivateConsole,
ActivateInspector,
CloseInspector,
OpenInspector,
Reset,
Capture
};
struct ButtonState {
ActionId action = ActionId::ActivateDocA;
std::string label = {};
UIRect rect = {};
bool hovered = false;
};
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;
}
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "doc-a", "Document A", UIEditorPanelPresentationKind::HostedContent, false, true, true },
{ "doc-b", "Document B", UIEditorPanelPresentationKind::HostedContent, false, true, true },
{ "console", "Console", UIEditorPanelPresentationKind::Placeholder, true, true, true },
{ "inspector", "Inspector", UIEditorPanelPresentationKind::HostedContent, false, true, true }
};
return registry;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root",
UIEditorWorkspaceSplitAxis::Horizontal,
0.68f,
BuildUIEditorWorkspaceTabStack(
"documents",
{
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A"),
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B"),
BuildUIEditorWorkspacePanel("console-node", "console", "Console", true)
},
0u),
BuildUIEditorWorkspacePanel("inspector-node", "inspector", "Inspector"));
workspace.activePanelId = "doc-a";
return workspace;
}
std::vector<UIEditorWorkspacePanelPresentationModel> BuildPresentationModels() {
std::vector<UIEditorWorkspacePanelPresentationModel> models = {};
for (std::string_view panelId : { "doc-a", "doc-b", "inspector" }) {
UIEditorWorkspacePanelPresentationModel model = {};
model.panelId = std::string(panelId);
model.kind = UIEditorPanelPresentationKind::HostedContent;
models.push_back(std::move(model));
}
return models;
}
std::string JoinExternalBodyPanelIds(const UIEditorWorkspaceComposeFrame& frame) {
const auto panelIds = CollectUIEditorWorkspaceComposeExternalBodyPanelIds(frame);
if (panelIds.empty()) {
return "(none)";
}
std::ostringstream stream = {};
for (std::size_t index = 0; index < panelIds.size(); ++index) {
if (index > 0u) {
stream << ", ";
}
stream << panelIds[index];
}
return stream.str();
}
std::string FormatEvents(const UIEditorWorkspaceComposeFrame& frame) {
if (frame.contentHostFrame.events.empty()) {
return "(none)";
}
std::ostringstream stream = {};
for (std::size_t index = 0; index < frame.contentHostFrame.events.size(); ++index) {
if (index > 0u) {
stream << " | ";
}
stream << GetUIEditorPanelContentHostEventKindName(frame.contentHostFrame.events[index].kind)
<< ":" << frame.contentHostFrame.events[index].panelId;
}
return stream.str();
}
std::string DescribeMountedState(
const UIEditorWorkspaceComposeFrame& frame,
std::string_view panelId) {
for (const UIEditorPanelContentHostPanelState& panelState : frame.contentHostFrame.panelStates) {
if (panelState.panelId != panelId) {
continue;
}
if (!panelState.mounted) {
return std::string(panelId) + ": unmounted";
}
std::ostringstream stream = {};
stream << panelId << ": mounted @ "
<< static_cast<int>(panelState.bounds.width) << "x"
<< static_cast<int>(panelState.bounds.height);
return stream.str();
}
return std::string(panelId) + ": missing";
}
void DrawCard(
UIDrawList& drawList,
const UIRect& rect,
std::string_view title,
std::string_view subtitle = {}) {
drawList.AddFilledRect(rect, kCardBg, 12.0f);
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 12.0f);
drawList.AddText(UIPoint(rect.x + 18.0f, rect.y + 16.0f), std::string(title), kTextPrimary, 17.0f);
if (!subtitle.empty()) {
drawList.AddText(UIPoint(rect.x + 18.0f, rect.y + 42.0f), std::string(subtitle), kTextMuted, 12.0f);
}
}
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 + 12.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f);
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleClick(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr && wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(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,
1460,
920,
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_captureRoot =
ResolveRepoRootPath() /
"tests/UI/Editor/manual_validation/shell/panel_content_host_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
ResetScenario();
return true;
}
void Shutdown() {
m_autoScreenshot.Shutdown();
m_renderer.Shutdown();
if (m_hwnd != nullptr) {
DestroyWindow(m_hwnd);
m_hwnd = nullptr;
}
if (m_windowClassAtom != 0) {
UnregisterClassW(kWindowClassName, m_hInstance);
m_windowClassAtom = 0;
}
}
void ResetScenario() {
m_controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
m_composeState = {};
m_lastStatus = "Ready";
m_lastMessage =
"鮟倩ョ、迥カ諤∽ク<EFBFBD> doc-a 蜥?inspector 莨壽撃蛻ー螟夜Κ<CE9A>ョケ螳ソ荳サ<E88DB3>靴onsole 莉咲┯襍ー蜊<EFBDB0>菴榊<E88FB4>螳ケ霍ッ蠕<EFBDAF>€?;
UpdateComposeFrame();
}
void UpdateLayout() {
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));
constexpr float padding = 20.0f;
constexpr float leftWidth = 430.0f;
m_introRect = UIRect(padding, padding, leftWidth, 206.0f);
m_controlsRect = UIRect(padding, 242.0f, leftWidth, 208.0f);
m_stateRect = UIRect(padding, 466.0f, leftWidth, height - 486.0f);
m_previewRect = UIRect(leftWidth + padding * 2.0f, padding, width - leftWidth - padding * 3.0f, height - padding * 2.0f);
m_workspaceRect = UIRect(m_previewRect.x + 18.0f, m_previewRect.y + 54.0f, m_previewRect.width - 36.0f, m_previewRect.height - 72.0f);
const float buttonWidth = (m_controlsRect.width - 32.0f - 16.0f) * 0.5f;
const float buttonLeft = m_controlsRect.x + 16.0f;
const float buttonTop = m_controlsRect.y + 62.0f;
const float buttonHeight = 34.0f;
const float rowGap = 10.0f;
m_buttons = {
{ ActionId::ActivateDocA, "Activate Doc A", UIRect(buttonLeft, buttonTop, buttonWidth, buttonHeight), false },
{ ActionId::ActivateDocB, "Activate Doc B", UIRect(buttonLeft + buttonWidth + 16.0f, buttonTop, buttonWidth, buttonHeight), false },
{ ActionId::ActivateConsole, "Activate Console", UIRect(buttonLeft, buttonTop + buttonHeight + rowGap, buttonWidth, buttonHeight), false },
{ 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, "驥咲スョ", 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 }
};
}
void OnResize(UINT width, UINT height) {
m_renderer.Resize(width, height);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseMove(float x, float y) {
UpdateLayout();
for (ButtonState& button : m_buttons) {
button.hovered = ContainsPoint(button.rect, x, y);
}
}
void HandleClick(float x, float y) {
UpdateLayout();
for (const ButtonState& button : m_buttons) {
if (ContainsPoint(button.rect, x, y)) {
ExecuteAction(button.action);
return;
}
}
}
void ExecuteAction(ActionId action) {
if (action == ActionId::Reset) {
ResetScenario();
return;
}
if (action == ActionId::Capture) {
m_autoScreenshot.RequestCapture("manual_button");
m_lastStatus = "Ready";
m_lastMessage =
"蟾イ隸キ豎よ穐蝗セ<EFBFBD>瑚セ灘<EFBFBD>蛻?tests/UI/Editor/manual_validation/shell/panel_content_host_basic/captures/縲?;
return;
}
UIEditorWorkspaceCommand command = {};
switch (action) {
case ActionId::ActivateDocA:
command.kind = UIEditorWorkspaceCommandKind::ActivatePanel;
command.panelId = "doc-a";
break;
case ActionId::ActivateDocB:
command.kind = UIEditorWorkspaceCommandKind::ActivatePanel;
command.panelId = "doc-b";
break;
case ActionId::ActivateConsole:
command.kind = UIEditorWorkspaceCommandKind::ActivatePanel;
command.panelId = "console";
break;
case ActionId::ActivateInspector:
command.kind = UIEditorWorkspaceCommandKind::ActivatePanel;
command.panelId = "inspector";
break;
case ActionId::CloseInspector:
command.kind = UIEditorWorkspaceCommandKind::ClosePanel;
command.panelId = "inspector";
break;
case ActionId::OpenInspector:
command.kind = UIEditorWorkspaceCommandKind::OpenPanel;
command.panelId = "inspector";
break;
default:
return;
}
const auto result = m_controller.Dispatch(command);
m_lastStatus = std::string(GetUIEditorWorkspaceCommandStatusName(result.status));
m_lastMessage = result.message;
UpdateComposeFrame();
}
void UpdateComposeFrame() {
UpdateLayout();
m_composeFrame = UpdateUIEditorWorkspaceCompose(
m_composeState,
m_workspaceRect,
m_controller.GetPanelRegistry(),
m_controller.GetWorkspace(),
m_controller.GetSession(),
BuildPresentationModels(),
{});
}
void RenderFrame() {
UpdateComposeFrame();
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));
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("PanelContentHostBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(drawList, m_introRect, "霑吩クェ豬玖ッ暮ェ瑚ッ∽サ€荵亥粥閭ス<EFBFBD><EFBFBD>", "鬪瑚ッ<EFBFBD>擇譚ソ蜀<EFBFBD>ョケ螳ソ荳サ蝨?DockHost 荳ュ逧<EFBDAD>撃霓ス縲∝査霓ス蜥悟頃菴榊屓騾€螂醍コヲ<EFBDBA>御ク榊★荳壼苅騾サ霎代€?);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 72.0f), "1. HostedContent panel 蠎疲撃蛻?DockHost 螟夜Κ body<64>御ク榊<EFBDB8>襍ー蜊<EFBDB0>菴榊<E88FB4>螳ケ縲?, kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 94.0f), "2. 蛻<>困 tab 譌カ<E8AD8C><EFBFBD>?body 蠎泌査霓ス<E99C93><EFBFBD>?body 蠎疲撃霓ス縲?, kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 116.0f), "3. Console 莉咲┯譏ッ蜊<EFBDAF>菴埼擇譚ソ<E8AD9A>御ク榊コ碑ッ・霑帛<E99C91>?external host縲?, kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 138.0f), "4. 蜈ウ髣ュ蜀肴遠蠑€ inspector<6F>悟コ皮恚蛻ー蜊ク霓ス蜥碁㍾譁ー謖りスス莠倶サカ縲?, kTextPrimary, 12.0f);
drawList.AddText(UIPoint(m_introRect.x + 18.0f, m_introRect.y + 164.0f), "蟒コ隶ョ鬘コ蠎擾シ咼oc A -> Doc B -> Console -> Close/Open Inspector縲?, kTextWeak, 11.0f);
DrawCard(drawList, m_controlsRect, "謫堺ス<EFBFBD>", "霑咎㈹蜿ェ菫晉蕗蠖灘燕螂醍コヲ鬪瑚ッ<EFBFBD>怙隕∫噪謖蛾聴縲?);
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, m_stateRect, "迥カ諤?, "€ソ<EFBFBD>?);
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);
stateY += 20.0f;
};
const UIColor resultColor =
m_lastStatus == "Rejected" ? kDanger :
(m_lastStatus == "Ready" ? kWarning : kSuccess);
addStateLine("Active Panel", m_controller.GetWorkspace().activePanelId, kTextPrimary, 11.0f);
addStateLine("Mounted Panels", JoinExternalBodyPanelIds(m_composeFrame), kSuccess, 11.0f);
addStateLine("Events", FormatEvents(m_composeFrame), kWarning, 11.0f);
addStateLine("Result", m_lastStatus, resultColor);
drawList.AddText(UIPoint(m_stateRect.x + 18.0f, stateY + 4.0f), m_lastMessage, kTextMuted, 11.0f);
stateY += 32.0f;
addStateLine("doc-a", DescribeMountedState(m_composeFrame, "doc-a"), kTextWeak, 11.0f);
addStateLine("doc-b", DescribeMountedState(m_composeFrame, "doc-b"), kTextWeak, 11.0f);
addStateLine("inspector", DescribeMountedState(m_composeFrame, "inspector"), kTextWeak, 11.0f);
addStateLine(
"謌ェ蝗セ",
m_autoScreenshot.HasPendingCapture()
? "謌ェ蝗セ謗帝弌荳?.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 謌也せ謖蛾聴 -> captures/")
: m_autoScreenshot.GetLastCaptureSummary()),
kTextWeak,
11.0f);
DrawCard(drawList, m_previewRect, "<EFBFBD><EFBFBD>", "DockHost 蜿ェ逕サ螟門」ウ<EFBDA3>幄統濶イ蜀<EFBDB2>ョケ蝮苓。ィ遉コ螟夜Κ<CE9A>ョケ螳ソ荳サ螳樣刔謖ょ<E8AC96>逧?body縲?);
AppendUIEditorWorkspaceCompose(drawList, m_composeFrame);
for (const UIEditorPanelContentHostPanelState& panelState : m_composeFrame.contentHostFrame.panelStates) {
if (!panelState.mounted ||
panelState.kind != UIEditorPanelPresentationKind::HostedContent) {
continue;
}
drawList.AddFilledRect(panelState.bounds, kMountedFill, 8.0f);
drawList.AddRectOutline(panelState.bounds, kMountedBorder, 2.0f, 8.0f);
drawList.AddText(
UIPoint(panelState.bounds.x + 16.0f, panelState.bounds.y + 16.0f),
panelState.panelId,
kTextPrimary,
18.0f);
drawList.AddText(
UIPoint(panelState.bounds.x + 16.0f, panelState.bounds.y + 44.0f),
"External Content Host",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(panelState.bounds.x + 16.0f, panelState.bounds.y + 66.0f),
"Mounted Body owned outside DockHost placeholder path.",
kTextWeak,
11.0f);
}
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HINSTANCE m_hInstance = nullptr;
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorWorkspaceController m_controller = {};
UIEditorWorkspaceComposeState m_composeState = {};
UIEditorWorkspaceComposeFrame m_composeFrame = {};
std::string m_lastStatus = {};
std::string m_lastMessage = {};
UIRect m_introRect = {};
UIRect m_controlsRect = {};
UIRect m_stateRect = {};
UIRect m_previewRect = {};
UIRect m_workspaceRect = {};
std::vector<ButtonState> m_buttons = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_panel_frame_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_panel_frame_basic_validation
OUTPUT_NAME "XCUIEditorPanelFrameBasicValidation"
)

View File

@@ -0,0 +1,525 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Panels/UIEditorPanelFrame.h>
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.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::UI::UIColor;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::Widgets::AppendUIEditorPanelFrameBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorPanelFrameForeground;
using XCEngine::UI::Editor::Widgets::BuildUIEditorPanelFrameLayout;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorPanelFrame;
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameLayout;
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameState;
using XCEngine::UI::Editor::Widgets::UIEditorPanelFrameText;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorPanelFrameBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | PanelFrame Basic";
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
constexpr UIColor kCardBorder(0.30f, 0.30f, 0.30f, 1.0f);
constexpr UIColor kCardAccent(0.82f, 0.82f, 0.82f, 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.26f, 0.26f, 0.26f, 1.0f);
constexpr UIColor kButtonOnBg(0.42f, 0.42f, 0.42f, 1.0f);
constexpr UIColor kButtonBorder(0.48f, 0.48f, 0.48f, 1.0f);
enum class ActionId : unsigned char {
ToggleFooter = 0,
TogglePin,
ToggleClose,
Reset
};
struct ButtonState {
ActionId action = ActionId::ToggleFooter;
std::string label = {};
UIRect rect = {};
bool selected = false;
};
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::string DescribePart(UIEditorPanelFrameHitTarget part) {
switch (part) {
case UIEditorPanelFrameHitTarget::Header: return "Header";
case UIEditorPanelFrameHitTarget::Body: return "Body";
case UIEditorPanelFrameHitTarget::Footer: return "Footer";
case UIEditorPanelFrameHitTarget::PinButton: return "Pin";
case UIEditorPanelFrameHitTarget::CloseButton: return "Close";
default: return "None";
}
}
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);
}
}
void DrawButton(
UIDrawList& drawList,
const ButtonState& button) {
drawList.AddFilledRect(button.rect, button.selected ? kButtonOnBg : kButtonBg, 8.0f);
drawList.AddRectOutline(button.rect, button.selected ? kCardAccent : kButtonBorder, 1.0f, 8.0f);
drawList.AddText(
UIPoint(button.rect.x + 12.0f, button.rect.y + 10.0f),
button.label,
kTextPrimary,
12.0f);
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleClick(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr && wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/panel_frame_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
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,
920,
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;
}
ResetState();
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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
void OnResize(UINT width, UINT height) {
m_renderer.Resize(width, height);
}
void HandleMouseMove(float x, float y) {
m_mousePosition = UIPoint(x, y);
TRACKMOUSEEVENT event = {};
event.cbSize = sizeof(event);
event.dwFlags = TME_LEAVE;
event.hwndTrack = m_hwnd;
TrackMouseEvent(&event);
UpdateHoveredPart();
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredPart = UIEditorPanelFrameHitTarget::None;
}
void HandleClick(float x, float y) {
for (const ButtonState& button : m_buttons) {
if (ContainsPoint(button.rect, x, y)) {
ExecuteAction(button.action);
return;
}
}
const UIEditorPanelFrameHitTarget part =
HitTestUIEditorPanelFrame(m_layout, m_state, UIPoint(x, y));
switch (part) {
case UIEditorPanelFrameHitTarget::Header:
m_state.active = !m_state.active;
m_lastResult = m_state.active ? "Header click -> Active = On" : "Header click -> Active = Off";
break;
case UIEditorPanelFrameHitTarget::Body:
case UIEditorPanelFrameHitTarget::Footer:
m_state.focused = true;
m_lastResult = "Body/Footer click -> Focus = On";
break;
case UIEditorPanelFrameHitTarget::PinButton:
if (m_state.pinnable) {
m_state.pinned = !m_state.pinned;
m_lastResult = m_state.pinned ? "Pin click -> Pinned = On" : "Pin click -> Pinned = Off";
}
break;
case UIEditorPanelFrameHitTarget::CloseButton:
if (m_state.closable) {
++m_closeDispatchCount;
m_lastResult = "Close click -> CloseRequested #" + std::to_string(m_closeDispatchCount);
}
break;
case UIEditorPanelFrameHitTarget::None:
default:
m_state.focused = false;
m_lastResult = "Outside click -> Focus = Off";
break;
}
UpdateHoveredPart();
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::ToggleFooter:
m_state.showFooter = !m_state.showFooter;
m_lastResult = m_state.showFooter ? "Footer = On" : "Footer = Off";
break;
case ActionId::TogglePin:
m_state.pinnable = !m_state.pinnable;
if (!m_state.pinnable) {
m_state.pinned = false;
}
m_lastResult = m_state.pinnable ? "Pin button = On" : "Pin button = Off";
break;
case ActionId::ToggleClose:
m_state.closable = !m_state.closable;
m_lastResult = m_state.closable ? "Close button = On" : "Close button = Off";
break;
case ActionId::Reset:
ResetState();
m_lastResult = "State reset";
break;
}
}
void ResetState() {
m_state = {};
m_state.active = true;
m_state.showFooter = true;
m_state.pinnable = true;
m_state.closable = true;
m_hoveredPart = UIEditorPanelFrameHitTarget::None;
m_lastResult = "Ready";
m_closeDispatchCount = 0;
}
void UpdateHoveredPart() {
m_hoveredPart = HitTestUIEditorPanelFrame(m_layout, m_state, m_mousePosition);
}
void BuildButtons(float left, float top, float width) {
const float buttonHeight = 34.0f;
const float gap = 10.0f;
m_buttons = {
{ 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, "驥咲スョ", UIRect(left, top + (buttonHeight + gap) * 3.0f, width, buttonHeight), false }
};
}
void RenderFrame() {
RECT clientRect = {};
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 float leftColumnWidth = 340.0f;
const float outerPadding = 20.0f;
const UIRect introRect(outerPadding, outerPadding, leftColumnWidth, 148.0f);
const UIRect controlsRect(outerPadding, 188.0f, leftColumnWidth, 180.0f);
const UIRect stateRect(outerPadding, 388.0f, leftColumnWidth, 220.0f);
const UIRect panelRect(
leftColumnWidth + outerPadding * 2.0f,
outerPadding,
width - leftColumnWidth - outerPadding * 3.0f,
height - outerPadding * 2.0f);
BuildButtons(controlsRect.x + 16.0f, controlsRect.y + 54.0f, controlsRect.width - 32.0f);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("PanelFrameBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(
drawList,
introRect,
"霑吩クェ豬玖ッ暮ェ瑚ッ∽サ€荵亥粥閭ス<EFBFBD><EFBFBD>",
"鬪瑚ッ<EFBFBD> PanelFrame 逧?header/body/footer 蛻<><EFBFBD>御サ・蜿?active縲ocus縲in縲…lose 蜻ス荳ュ縲?);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 66.0f),
"1. hover 蜷<>玄蝓滂シ梧」€譟・蜻ス荳?part 譏ッ蜷ヲ豁」遑ョ縲?,
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 90.0f),
"2. 轤ケ蜃サ Header縲。ody縲ooter縲 ̄in縲lose<73>梧」€譟・迥カ諤∝序蛹悶€?,
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 114.0f),
"3. 蜿ウ萓ァ蜿ェ謾セ荳€荳?PanelFrame<6D>御ク肴磁荳壼苅髱「譚ソ縲?,
kTextWeak,
12.0f);
DrawCard(drawList, controlsRect, "謫堺ス<EFBFBD>", "蜿ェ菫晉蕗蠖ア蜩?PanelFrame contract 逧<>€蜈ウ縲?);
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, stateRect, "迥カ諤∵遭隕?, "?hover縲ctive縲ocus縲inned ?);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 66.0f),
"Hover: " + DescribePart(m_hoveredPart),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 92.0f),
std::string("Active: ") + (m_state.active ? "On" : "Off"),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 118.0f),
std::string("Focused: ") + (m_state.focused ? "On" : "Off"),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 144.0f),
std::string("Pinned: ") + (m_state.pinned ? "On" : "Off"),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 170.0f),
"Result: " + m_lastResult,
kTextMuted,
12.0f);
UIEditorPanelFrameState frameState = m_state;
frameState.hovered = m_hoveredPart != UIEditorPanelFrameHitTarget::None;
frameState.pinHovered = m_hoveredPart == UIEditorPanelFrameHitTarget::PinButton;
frameState.closeHovered = m_hoveredPart == UIEditorPanelFrameHitTarget::CloseButton;
m_layout = BuildUIEditorPanelFrameLayout(panelRect, frameState);
AppendUIEditorPanelFrameBackground(drawList, m_layout, frameState);
AppendUIEditorPanelFrameForeground(
drawList,
m_layout,
frameState,
UIEditorPanelFrameText{
"Inspector Placeholder",
"PanelFrame foundation validation",
m_state.showFooter
? "Footer visible | Active=" + std::string(m_state.active ? "On" : "Off")
: ""
});
drawList.AddText(
UIPoint(m_layout.bodyRect.x, m_layout.bodyRect.y),
"Body content placeholder",
kTextPrimary,
15.0f);
drawList.AddText(
UIPoint(m_layout.bodyRect.x, m_layout.bodyRect.y + 28.0f),
"Hover current region: " + DescribePart(m_hoveredPart),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(m_layout.bodyRect.x, m_layout.bodyRect.y + 50.0f),
"Pin button and Close button use PanelFrame hit rects.",
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(m_layout.bodyRect.x, m_layout.bodyRect.y + 72.0f),
"Close does not destroy the panel here. It only verifies dispatch intent.",
kTextWeak,
12.0f);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
std::vector<ButtonState> m_buttons = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
UIEditorPanelFrameState m_state = {};
UIEditorPanelFrameLayout m_layout = {};
UIEditorPanelFrameHitTarget m_hoveredPart = UIEditorPanelFrameHitTarget::None;
std::string m_lastResult = {};
int m_closeDispatchCount = 0;
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_property_grid_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_property_grid_basic_validation
OUTPUT_NAME "XCUIEditorPropertyGridBasicValidation"
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_scroll_view_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_scroll_view_basic_validation
OUTPUT_NAME "XCUIEditorScrollViewBasicValidation"
)

View File

@@ -0,0 +1,759 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Collections/UIEditorScrollViewInteraction.h>
#include <XCEditor/Collections/UIEditorScrollView.h>
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
namespace {
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::UIEditorScrollViewInteractionFrame;
using XCEngine::UI::Editor::UIEditorScrollViewInteractionResult;
using XCEngine::UI::Editor::UIEditorScrollViewInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorScrollViewInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorScrollViewBackground;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorScrollView;
using XCEngine::UI::Editor::Widgets::ResolveUIEditorScrollViewContentOrigin;
using XCEngine::UI::Editor::Widgets::UIEditorScrollViewHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorScrollViewHitTargetKind;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorScrollViewBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ScrollView Basic";
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 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 kTextSuccess(0.63f, 0.76f, 0.63f, 1.0f);
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
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 scrollRect = {};
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;
}
ScenarioLayout BuildScenarioLayout(float width, float height) {
constexpr float margin = 20.0f;
constexpr float leftWidth = 430.0f;
constexpr float gap = 16.0f;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 214.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)(200.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.scrollRect = UIRect(
layout.previewRect.x + 18.0f,
layout.previewRect.y + 64.0f,
layout.previewRect.width - 36.0f,
layout.previewRect.height - 84.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "é‡<EFBFBD>ç½®", 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,
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 + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
}
}
void DrawButton(
UIDrawList& drawList,
const ButtonLayout& button,
bool hovered) {
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
drawList.AddText(
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
button.label,
kTextPrimary,
12.0f);
}
std::vector<std::string> BuildLogLines() {
std::vector<std::string> lines = {
"Line 01 - ScrollView validation log",
"Line 02 - Hover inside the viewport, then use wheel",
"Line 03 - Offset should grow when wheel scrolls down",
"Line 04 - Offset should clamp at the bottom boundary",
"Line 05 - Drag the thumb to verify direct scrollbar control",
"Line 06 - Clicking empty content should only change focus",
"Line 07 - ScrollView is Editor foundation, not any business panel",
"Line 08 - PropertyGrid and ListView will reuse this viewport contract",
"Line 09 - Line spacing should remain clipped inside viewport",
"Line 10 - The right thumb should stay aligned to overflow",
"Line 11 - Extra row",
"Line 12 - Extra row",
"Line 13 - Extra row",
"Line 14 - Extra row",
"Line 15 - Extra row",
"Line 16 - Extra row",
};
for (int index = 11; index <= 40; ++index) {
lines.push_back(
"Line " +
std::string(index < 10 ? "0" : "") +
std::to_string(index) +
" - Extra row");
}
return lines;
}
std::string DescribeHitTarget(const UIEditorScrollViewHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorScrollViewHitTargetKind::Content:
return "content";
case UIEditorScrollViewHitTargetKind::ScrollbarTrack:
return "scrollbar-track";
case UIEditorScrollViewHitTargetKind::ScrollbarThumb:
return "scrollbar-thumb";
case UIEditorScrollViewHitTargetKind::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 MakeWheelEvent(const UIPoint& position, float wheelDelta) {
UIInputEvent event = {};
event.type = UIInputEventType::PointerWheel;
event.position = position;
event.wheelDelta = wheelDelta;
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_MOUSEWHEEL:
if (app != nullptr) {
POINT point = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
ScreenToClient(hwnd, &point);
app->HandleMouseWheel(
static_cast<float>(point.x),
static_cast<float>(point.y),
static_cast<float>(GET_WHEEL_DELTA_WPARAM(wParam)) / static_cast<float>(WHEEL_DELTA));
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr && wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "已请求截图,输出�captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1480,
920,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/scroll_view_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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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);
}
float ResolveContentHeight() const {
constexpr float lineHeight = 28.0f;
constexpr float topPadding = 12.0f;
return topPadding * 2.0f + static_cast<float>(m_logLines.size()) * lineHeight;
}
void ResetScenario() {
m_logLines = BuildLogLines();
m_verticalOffset = 0.0f;
m_interactionState = {};
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "å·²é‡<EFBFBD>置到 " + std::to_string(m_logLines.size()) + " 行默认滚动内å®?;
RefreshScrollFrame();
}
void RefreshScrollFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
m_scrollFrame =
UpdateUIEditorScrollViewInteraction(
m_interactionState,
m_verticalOffset,
layout.scrollRect,
ResolveContentHeight(),
{});
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshScrollFrame();
}
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);
PumpScrollEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpScrollEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseWheel(float x, float y, float wheelDelta) {
m_mousePosition = UIPoint(x, y);
const UIEditorScrollViewInteractionResult result =
PumpScrollEvents({ MakeWheelEvent(m_mousePosition, wheelDelta) });
if (result.offsetChanged) {
m_lastResult = "滚轮滚动:offset 已更�;
}
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorScrollViewInteractionResult result =
PumpScrollEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
if (result.startedThumbDrag) {
SetCapture(m_hwnd);
}
UpdateResultText(result, ContainsPoint(layout.scrollRect, x, y));
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorScrollViewInteractionResult result =
PumpScrollEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
if (!m_interactionState.scrollViewState.draggingScrollbarThumb && GetCapture() == m_hwnd) {
ReleaseCapture();
}
UpdateResultText(result, ContainsPoint(layout.scrollRect, x, y));
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorScrollViewInteractionResult PumpScrollEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
m_scrollFrame =
UpdateUIEditorScrollViewInteraction(
m_interactionState,
m_verticalOffset,
layout.scrollRect,
ResolveContentHeight(),
std::move(events));
return m_scrollFrame.result;
}
void UpdateResultText(
const UIEditorScrollViewInteractionResult& result,
bool insideScrollView) {
if (result.startedThumbDrag) {
m_lastResult = "开始拖�scrollbar thumb";
return;
}
if (result.endedThumbDrag) {
m_lastResult = "结æ<EFBFBD>Ÿææ½ scrollbar thumb";
return;
}
if (result.offsetChanged) {
m_lastResult = "滚动ä½<EFBFBD>置已å<EFBFBD>˜åŒ?;
return;
}
if (result.focusChanged) {
m_lastResult = m_interactionState.scrollViewState.focused
? "ScrollView 已获�focus"
: "ScrollView focus 已清�;
return;
}
if (insideScrollView) {
m_lastResult = "ç¹å‡»å†…容区:å<EFBFBD>ªéªŒè¯?focus / hover / scrollbar";
return;
}
m_lastResult = "æ— å<EFBFBD>˜åŒ?;
}
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 RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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 ScenarioLayout layout = BuildScenarioLayout(width, height);
RefreshScrollFrame();
const UIEditorScrollViewHitTarget currentHit =
HitTestUIEditorScrollView(m_scrollFrame.layout, m_mousePosition);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorScrollViewBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(
drawList,
layout.introRect,
"这个æµè¯•验è¯<EFBFBD>什么功能?",
"验è¯<EFBFBD>滚动视å¾çš„æ»šè½®æ»šåЍã€<EFBFBD>thumb ææ½ã€<C3A3>focus 切æ<E280A1>¢å?offset clampã€?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 在内容区滚轮滚动,检æŸ?offset 是å<C2AF>¦è¿žç»­æ´æ°ã€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 滚到底部å<C2A8>Žç»§ç»­æ»šåŠ¨ï¼Œoffset å¿…é¡»è¢?clampã€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 拖拽 scrollbar thumb,检æŸ?offset ä¸?thumb ä½<C3A4>置是å<C2AF>¦å<C2A6>Œæ­¥ã€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. ç¹å‡»å†…容区å<C2BA>ªæ´æ° 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 触å<C2A6>截å¾ã€?,
kTextPrimary,
12.0f);
DrawCard(drawList, layout.controlRect, "æ“<EFBFBD>作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(drawList, layout.stateRect, "状æ€<EFBFBD>æ˜è¦?, "é<EFBFBD>ç¹æ£æŸ?hit / focus / thumb-drag / offset / overflowã?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.scrollViewState.focused ? "æ˜? : "å<EFBFBD>?),
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Thumb Dragging: ") +
(m_interactionState.scrollViewState.draggingScrollbarThumb ? "æ˜? : "å<EFBFBD>?),
kTextSuccess,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
"Offset: " + std::to_string(static_cast<int>(m_verticalOffset)) +
" / " + std::to_string(static_cast<int>(m_scrollFrame.layout.maxOffset)),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
std::string("Has Scrollbar: ") + (m_scrollFrame.layout.hasScrollbar ? "æ˜? : "å<EFBFBD>?),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Lines: " + std::to_string(m_logLines.size()),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
"Result: " + m_lastResult,
kTextPrimary,
12.0f);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队�.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/scroll_view_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
captureSummary,
kTextWeak,
12.0f);
DrawCard(drawList, layout.previewRect, "ScrollView 预览", "这里å<EFBFBD>ªæ”¾ä¸€ä¸?ScrollView,ä¸<C3A4>混入任何上å±ä¸šåС内容ã€?);
AppendUIEditorScrollViewBackground(
drawList,
m_scrollFrame.layout,
m_interactionState.scrollViewState);
const UIPoint contentOrigin = ResolveUIEditorScrollViewContentOrigin(m_scrollFrame.layout);
drawList.PushClipRect(m_scrollFrame.layout.contentRect);
for (std::size_t index = 0; index < m_logLines.size(); ++index) {
drawList.AddText(
UIPoint(
contentOrigin.x + 16.0f,
contentOrigin.y + 12.0f + static_cast<float>(index) * 28.0f),
m_logLines[index],
kTextPrimary,
12.0f);
}
drawList.PopClipRect();
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
std::vector<std::string> m_logLines = {};
UIEditorScrollViewInteractionState m_interactionState = {};
UIEditorScrollViewInteractionFrame m_scrollFrame = {};
float m_verticalOffset = 0.0f;
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_status_bar_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_status_bar_basic_validation
OUTPUT_NAME "XCUIEditorStatusBarBasicValidation"
)

View File

@@ -0,0 +1,536 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Shell/UIEditorStatusBar.h>
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.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::UI::UIColor;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::Widgets::AppendUIEditorStatusBarBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorStatusBarForeground;
using XCEngine::UI::Editor::Widgets::BuildUIEditorStatusBarLayout;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorStatusBar;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarInvalidIndex;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarLayout;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarState;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarTextTone;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorStatusBarBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | StatusBar Basic";
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
constexpr UIColor kCardBorder(0.30f, 0.30f, 0.30f, 1.0f);
constexpr UIColor kCardAccent(0.82f, 0.82f, 0.82f, 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.26f, 0.26f, 0.26f, 1.0f);
constexpr UIColor kButtonOnBg(0.40f, 0.40f, 0.40f, 1.0f);
constexpr UIColor kButtonBorder(0.48f, 0.48f, 0.48f, 1.0f);
enum class ActionId : unsigned char {
ToggleAccent = 0,
ToggleSeparator,
MoveToTrailing,
Reset,
Capture
};
struct ButtonState {
ActionId action = ActionId::ToggleAccent;
std::string label = {};
UIRect rect = {};
bool selected = false;
};
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::string DescribeHitTarget(const UIEditorStatusBarHitTarget& hit) {
switch (hit.kind) {
case UIEditorStatusBarHitTargetKind::Segment:
return "Segment[" + std::to_string(hit.index) + "]";
case UIEditorStatusBarHitTargetKind::Separator:
return "Separator[" + std::to_string(hit.index) + "]";
case UIEditorStatusBarHitTargetKind::Background:
return "Background";
case UIEditorStatusBarHitTargetKind::None:
default:
return "None";
}
}
std::string DescribeSlot(UIEditorStatusBarSlot slot) {
return slot == UIEditorStatusBarSlot::Leading ? "Leading" : "Trailing";
}
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);
}
}
void DrawButton(UIDrawList& drawList, const ButtonState& button) {
drawList.AddFilledRect(button.rect, button.selected ? kButtonOnBg : kButtonBg, 8.0f);
drawList.AddRectOutline(button.rect, button.selected ? kCardAccent : kButtonBorder, 1.0f, 8.0f);
drawList.AddText(
UIPoint(button.rect.x + 12.0f, button.rect.y + 10.0f),
button.label,
kTextPrimary,
12.0f);
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleClick(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr && wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
InvalidateRect(hwnd, nullptr, FALSE);
UpdateWindow(hwnd);
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/status_bar_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
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,
920,
nullptr,
nullptr,
hInstance,
this);
if (m_hwnd == nullptr) {
return false;
}
if (!m_renderer.Initialize(m_hwnd)) {
return false;
}
ShowWindow(m_hwnd, nCmdShow);
ResetState();
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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
void OnResize(UINT width, UINT height) {
m_renderer.Resize(width, height);
}
void HandleMouseMove(float x, float y) {
m_mousePosition = UIPoint(x, y);
TRACKMOUSEEVENT event = {};
event.cbSize = sizeof(event);
event.dwFlags = TME_LEAVE;
event.hwndTrack = m_hwnd;
TrackMouseEvent(&event);
UpdateHover();
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_state.hoveredIndex = UIEditorStatusBarInvalidIndex;
m_hoverTarget = {};
}
void HandleClick(float x, float y) {
for (const ButtonState& button : m_buttons) {
if (ContainsPoint(button.rect, x, y)) {
ExecuteAction(button.action);
return;
}
}
m_hoverTarget = HitTestUIEditorStatusBar(m_layout, UIPoint(x, y));
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;
} else if (m_hoverTarget.kind == UIEditorStatusBarHitTargetKind::Background) {
m_state.activeIndex = UIEditorStatusBarInvalidIndex;
m_lastResult = "命中 status bar background";
} else {
m_lastResult = "命中 " + DescribeHitTarget(m_hoverTarget);
}
UpdateHover();
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::ToggleAccent:
m_segments[1].tone =
m_segments[1].tone == UIEditorStatusBarTextTone::Accent
? UIEditorStatusBarTextTone::Primary
: UIEditorStatusBarTextTone::Accent;
m_lastResult = "Selection 强调已切æ<E280A1>?;
break;
case ActionId::ToggleSeparator:
m_segments[0].showSeparator = !m_segments[0].showSeparator;
m_lastResult = m_segments[0].showSeparator ? "Leading separator 已开å<E282AC>? : "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);
break;
case ActionId::Reset:
ResetState();
m_lastResult = "状æ€<EFBFBD>å·²é‡<EFBFBD>ç½®";
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
InvalidateRect(m_hwnd, nullptr, FALSE);
UpdateWindow(m_hwnd);
m_lastResult = "截图已排�;
break;
}
UpdateHover();
}
void ResetState() {
m_segments = {
{ "scene", "Scene: Main", UIEditorStatusBarSlot::Leading, UIEditorStatusBarTextTone::Primary, true, true, 96.0f },
{ "selection", "Selection: Camera", UIEditorStatusBarSlot::Leading, UIEditorStatusBarTextTone::Accent, true, false, 140.0f },
{ "frame", "16.7 ms", UIEditorStatusBarSlot::Trailing, UIEditorStatusBarTextTone::Muted, true, true, 64.0f },
{ "gpu", "GPU Ready", UIEditorStatusBarSlot::Trailing, UIEditorStatusBarTextTone::Primary, true, false, 86.0f }
};
m_state = {};
m_state.focused = true;
m_state.activeIndex = 1u;
m_hoverTarget = {};
m_lastResult = "就绪";
}
void UpdateHover() {
m_hoverTarget = HitTestUIEditorStatusBar(m_layout, m_mousePosition);
m_state.hoveredIndex =
m_hoverTarget.kind == UIEditorStatusBarHitTargetKind::Segment
? m_hoverTarget.index
: UIEditorStatusBarInvalidIndex;
}
void BuildButtons(float left, float top, float width) {
const float buttonHeight = 34.0f;
const float gap = 10.0f;
m_buttons = {
{ ActionId::ToggleAccent, "切æ<EFBFBD>¢å¼ºè°ƒ", 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, "切æ<EFBFBD>¢ Selection Slot", UIRect(left, top + (buttonHeight + gap) * 2.0f, width, buttonHeight), m_segments[1].slot == UIEditorStatusBarSlot::Trailing },
{ ActionId::Reset, "é‡<EFBFBD>ç½®", UIRect(left, top + (buttonHeight + gap) * 3.0f, width, buttonHeight), false },
{ ActionId::Capture, "截图(F12)", UIRect(left, top + (buttonHeight + gap) * 4.0f, width, buttonHeight), false }
};
}
void RenderFrame() {
RECT clientRect = {};
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 float leftColumnWidth = 340.0f;
const float outerPadding = 20.0f;
const UIRect introRect(outerPadding, outerPadding, leftColumnWidth, 156.0f);
const UIRect controlsRect(outerPadding, 196.0f, leftColumnWidth, 276.0f);
const UIRect stateRect(outerPadding, 492.0f, leftColumnWidth, 244.0f);
const UIRect previewRect(
leftColumnWidth + outerPadding * 2.0f,
outerPadding,
width - leftColumnWidth - outerPadding * 3.0f,
height - outerPadding * 2.0f);
const UIRect viewportRect(
previewRect.x,
previewRect.y,
previewRect.width,
previewRect.height - 28.0f);
const UIRect statusBarRect(
previewRect.x,
previewRect.y + previewRect.height - 28.0f,
previewRect.width,
28.0f);
BuildButtons(controlsRect.x + 16.0f, controlsRect.y + 54.0f, controlsRect.width - 32.0f);
m_layout = BuildUIEditorStatusBarLayout(statusBarRect, m_segments);
UpdateHover();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("StatusBarBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(
drawList,
introRect,
"这个æµè¯•验è¯<EFBFBD>什么功能?",
"验è¯<EFBFBD> StatusBar çš?leading/trailing 布局ã€<C3A3>separatorã€<C3A3>å¼ºè°ƒæ‡æœ¬ï¼Œä»¥å<C2A5>Š hover/active 命中ã€?);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 66.0f),
"1. hover ä¸<C3A4>å<EFBFBD>Œ segment / separator,检查å½ä¸­æ˜¯å<C2AF>¦æ­£ç¡®ã€?,
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 90.0f),
"2. 点击 segment,检æŸ?active 是å<C2AF>¦åˆ‡åˆ°å¯¹åº”项ã€?,
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 114.0f),
"3. 切æ<E280A1>¢å¼ºè°ƒã€<C3A3>separator å’?Selection slot,检查布局是å<C2AF>¦ç¨³å®šã€?,
kTextWeak,
12.0f);
DrawCard(drawList, controlsRect, "æ“<EFBFBD>作", "å<EFBFBD>ªä¿<EFBFBD>留与 StatusBar contract 直接相关的开关ã€?);
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, stateRect, "状æ€<EFBFBD>æ˜è¦?, "é<EFBFBD>ç¹çœ?hoverã<EFBFBD>activeã<EFBFBD>Selection slotã<EFBFBD>separator åŒç»æžœã?);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 66.0f),
"Hover: " + DescribeHitTarget(m_hoverTarget),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 92.0f),
"Active: " + (m_state.activeIndex == UIEditorStatusBarInvalidIndex ? std::string("None") : m_segments[m_state.activeIndex].segmentId),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 118.0f),
"Selection Slot: " + DescribeSlot(m_segments[1].slot),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 144.0f),
std::string("Leading Separator: ") + (m_segments[0].showSeparator ? "On" : "Off"),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 170.0f),
"Result: " + m_lastResult,
kTextMuted,
12.0f);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队�.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("截图: F12 或按�-> status_bar_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 196.0f),
captureSummary,
kTextWeak,
12.0f);
drawList.AddFilledRect(viewportRect, UIColor(0.17f, 0.17f, 0.17f, 1.0f), 10.0f);
drawList.AddRectOutline(viewportRect, kCardBorder, 1.0f, 10.0f);
drawList.AddText(
UIPoint(viewportRect.x + 18.0f, viewportRect.y + 18.0f),
"预览宿主",
kTextPrimary,
18.0f);
drawList.AddText(
UIPoint(viewportRect.x + 18.0f, viewportRect.y + 48.0f),
"这里å<EFBFBD>ªæ”¾ä¸€ä¸ªå®¿ä¸»åŒºåŸŸï¼Œç”¨æ<EFBFBD>¥è§å¯Ÿåº•部 StatusBar 的布局åŒçжæ€<C3A6>å<EFBFBD>˜åŒã€?,
kTextMuted,
12.0f);
AppendUIEditorStatusBarBackground(drawList, m_layout, m_segments, m_state);
AppendUIEditorStatusBarForeground(drawList, m_layout, m_segments, m_state);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
std::vector<ButtonState> m_buttons = {};
std::vector<UIEditorStatusBarSegment> m_segments = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
UIEditorStatusBarState m_state = {};
UIEditorStatusBarLayout m_layout = {};
UIEditorStatusBarHitTarget m_hoverTarget = {};
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_tab_strip_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_tab_strip_basic_validation
OUTPUT_NAME "XCUIEditorTabStripBasicValidation"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,734 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Collections/UIEditorTabStripInteraction.h>
#include <XCEditor/Panels/UIEditorPanelRegistry.h>
#include <XCEditor/Workspace/UIEditorWorkspaceController.h>
#include <XCEditor/Collections/UIEditorTabStrip.h>
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <sstream>
#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::BuildDefaultUIEditorWorkspaceController;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::FindUIEditorPanelSessionState;
using XCEngine::UI::Editor::GetUIEditorWorkspaceCommandStatusName;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
using XCEngine::UI::Editor::UIEditorTabStripInteractionFrame;
using XCEngine::UI::Editor::UIEditorTabStripInteractionResult;
using XCEngine::UI::Editor::UIEditorTabStripInteractionState;
using XCEngine::UI::Editor::UIEditorWorkspaceCommand;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandKind;
using XCEngine::UI::Editor::UIEditorWorkspaceCommandResult;
using XCEngine::UI::Editor::UIEditorWorkspaceController;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspaceNode;
using XCEngine::UI::Editor::UIEditorWorkspaceNodeKind;
using XCEngine::UI::Editor::UpdateUIEditorTabStripInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTabStripBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTabStripForeground;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTabStrip;
using XCEngine::UI::Editor::Widgets::ResolveUIEditorTabStripSelectedIndex;
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorTabStripHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorTabStripInvalidIndex;
using XCEngine::UI::Editor::Widgets::UIEditorTabStripItem;
using XCEngine::UI::Editor::Widgets::UIEditorTabStripLayout;
using XCEngine::UI::Editor::Widgets::UIEditorTabStripState;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorTabStripBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | TabStrip Basic";
constexpr UIColor kWindowBg(0.15f, 0.15f, 0.15f, 1.0f);
constexpr UIColor kCardBg(0.19f, 0.19f, 0.19f, 1.0f);
constexpr UIColor kCardBorder(0.30f, 0.30f, 0.30f, 1.0f);
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 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);
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 MapTabNavigationKey(UINT keyCode) {
switch (keyCode) {
case VK_LEFT:
return static_cast<std::int32_t>(KeyCode::Left);
case VK_RIGHT:
return static_cast<std::int32_t>(KeyCode::Right);
case VK_HOME:
return static_cast<std::int32_t>(KeyCode::Home);
case VK_END:
return static_cast<std::int32_t>(KeyCode::End);
default:
return static_cast<std::int32_t>(KeyCode::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;
}
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 + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
}
}
void DrawButton(
UIDrawList& drawList,
const UIRect& rect,
std::string_view label,
bool hovered) {
drawList.AddFilledRect(rect, hovered ? kButtonHoveredBg : kButtonBg, 8.0f);
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 8.0f);
drawList.AddText(UIPoint(rect.x + 14.0f, rect.y + 11.0f), std::string(label), kTextPrimary, 13.0f);
}
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "doc-a", "Document A", {}, true, true, true },
{ "doc-b", "Document B", {}, true, true, true },
{ "doc-c", "Document C", {}, true, true, false }
};
return registry;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceTabStack(
"document-tabs",
{
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true),
BuildUIEditorWorkspacePanel("doc-c-node", "doc-c", "Document C", true)
},
0u);
workspace.activePanelId = "doc-a";
return workspace;
}
const UIEditorWorkspaceNode* GetRootTabStack(const UIEditorWorkspaceModel& workspace) {
return workspace.root.kind == UIEditorWorkspaceNodeKind::TabStack ? &workspace.root : nullptr;
}
std::string DescribeHitTarget(
const UIEditorTabStripHitTarget& target,
const std::vector<UIEditorTabStripItem>& items) {
switch (target.kind) {
case UIEditorTabStripHitTargetKind::HeaderBackground:
return "HeaderBackground";
case UIEditorTabStripHitTargetKind::Content:
return "Content";
case UIEditorTabStripHitTargetKind::Tab:
if (target.index < items.size()) {
return "Tab: " + items[target.index].title;
}
return "Tab";
case UIEditorTabStripHitTargetKind::None:
default:
return "None";
}
}
std::string JoinTabTitles(const std::vector<UIEditorTabStripItem>& items) {
if (items.empty()) {
return "(none)";
}
std::ostringstream stream;
for (std::size_t index = 0; index < items.size(); ++index) {
if (index > 0u) {
stream << " | ";
}
stream << items[index].title;
}
return stream.str();
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
} else {
const std::int32_t keyCode = MapTabNavigationKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->HandleKeyDown(keyCode);
}
}
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/tab_strip_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
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,
920,
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;
}
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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
void ResetScenario() {
m_controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
m_interactionState = {};
m_tabStripFrame = {};
m_tabItems.clear();
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_lastResult = "å·²é‡<EFBFBD>置到默认标签状æ€?;
}
UIRect GetTabStripRect() const {
RECT clientRect = {};
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 float outerPadding = 20.0f;
const float leftColumnWidth = 360.0f;
const UIRect previewCardRect(
leftColumnWidth + outerPadding * 2.0f,
outerPadding,
width - leftColumnWidth - outerPadding * 3.0f,
height - outerPadding * 2.0f);
return UIRect(
previewCardRect.x + 20.0f,
previewCardRect.y + 20.0f,
previewCardRect.width - 40.0f,
previewCardRect.height - 40.0f);
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
}
void HandleMouseMove(float x, float y) {
m_mousePosition = UIPoint(x, y);
TRACKMOUSEEVENT event = {};
event.cbSize = sizeof(event);
event.dwFlags = TME_LEAVE;
event.hwndTrack = m_hwnd;
TrackMouseEvent(&event);
m_resetButtonHovered = ContainsPoint(m_resetButtonRect, x, y);
PumpTabStripEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_resetButtonHovered = false;
PumpTabStripEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
if (ContainsPoint(m_resetButtonRect, x, y)) {
m_resetButtonHovered = true;
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
PumpTabStripEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
if (ContainsPoint(m_resetButtonRect, x, y)) {
ResetScenario();
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorTabStripInteractionResult result =
PumpTabStripEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
ApplyInteractionResult(result, "Mouse");
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleKeyDown(std::int32_t keyCode) {
const UIEditorTabStripInteractionResult result =
PumpTabStripEvents({ MakeKeyEvent(keyCode) });
ApplyInteractionResult(result, "Keyboard");
InvalidateRect(m_hwnd, nullptr, FALSE);
}
UIEditorTabStripInteractionResult PumpTabStripEvents(std::vector<UIInputEvent> events) {
RefreshTabItems();
std::string selectedTabId = m_controller.GetWorkspace().activePanelId;
m_tabStripFrame = UpdateUIEditorTabStripInteraction(
m_interactionState,
selectedTabId,
GetTabStripRect(),
m_tabItems,
events);
m_tabStripState = m_interactionState.tabStripState;
m_layout = m_tabStripFrame.layout;
m_hoverTarget = HitTestUIEditorTabStrip(m_layout, m_tabStripState, m_mousePosition);
return m_tabStripFrame.result;
}
void ApplyInteractionResult(
const UIEditorTabStripInteractionResult& result,
std::string_view source) {
if ((result.selectionChanged || result.keyboardNavigated) &&
!result.selectedTabId.empty()) {
DispatchCommand(
UIEditorWorkspaceCommandKind::ActivatePanel,
result.selectedTabId,
std::string(source) + " Activate -> " + result.selectedTabId);
PumpTabStripEvents({});
return;
}
if (result.hitTarget.kind == UIEditorTabStripHitTargetKind::HeaderBackground ||
result.hitTarget.kind == UIEditorTabStripHitTargetKind::Content) {
m_lastResult = "命中 TabStrip 背景,ä¿<C3A4>ç•?focus";
return;
}
if (result.hitTarget.kind == UIEditorTabStripHitTargetKind::None &&
!m_interactionState.tabStripState.focused) {
m_lastResult = "Focus cleared";
}
}
void DispatchCommand(
UIEditorWorkspaceCommandKind kind,
std::string_view panelId,
std::string label) {
UIEditorWorkspaceCommand command = {};
command.kind = kind;
command.panelId = std::string(panelId);
const UIEditorWorkspaceCommandResult result = m_controller.Dispatch(command);
m_lastResult =
std::move(label) + " -> " +
std::string(GetUIEditorWorkspaceCommandStatusName(result.status)) +
" | " +
result.message;
}
void RefreshTabItems() {
const UIEditorWorkspaceModel& workspace = m_controller.GetWorkspace();
const auto* tabStack = GetRootTabStack(workspace);
m_tabItems.clear();
if (tabStack != nullptr) {
for (const UIEditorWorkspaceNode& child : tabStack->children) {
if (child.kind != UIEditorWorkspaceNodeKind::Panel) {
continue;
}
const auto* sessionState =
FindUIEditorPanelSessionState(m_controller.GetSession(), child.panel.panelId);
if (sessionState == nullptr || !sessionState->open || !sessionState->visible) {
continue;
}
UIEditorTabStripItem item = {};
item.tabId = child.panel.panelId;
item.title = child.panel.title;
m_tabItems.push_back(std::move(item));
}
}
}
void RenderFrame() {
RECT clientRect = {};
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 float outerPadding = 20.0f;
const float leftColumnWidth = 360.0f;
const UIRect introRect(outerPadding, outerPadding, leftColumnWidth, 176.0f);
const UIRect stateRect(outerPadding, 216.0f, leftColumnWidth, height - 236.0f);
const UIRect previewCardRect(
leftColumnWidth + outerPadding * 2.0f,
outerPadding,
width - leftColumnWidth - outerPadding * 3.0f,
height - outerPadding * 2.0f);
PumpTabStripEvents({});
m_resetButtonRect = UIRect(
stateRect.x + 16.0f,
stateRect.y + 194.0f,
stateRect.width - 32.0f,
38.0f);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorTabStripBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(
drawList,
introRect,
"这个æµè¯•验è¯<EFBFBD>什么功能?",
"验è¯<EFBFBD> TabStrip çš?header å½ä¸­ã€<C3A3>选中切æ<E280A1>¢åŒé”®ç˜å¯¼èˆªï¼Œä¸<C3A4>接业务é<C2A1>¢æ<C2A2>¿ã€?);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 68.0f),
"1. 点击 tab,检æŸ?selected / active panel 是å<C2AF>¦å<C2A6>Œæ­¥ã€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 92.0f),
"2. 所æœ?tab 都没有关闭按é®ï¼è¿™é‡Œå<C592>ªéªŒè¯<C3A8>å½ä¸­ã€<C3A3>focus åŒé€‰ä¸­å<C2AD>Œæ­¥ã€?,
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(introRect.x + 16.0f, introRect.y + 116.0f),
"3. Left / Right / Home / End 验è¯<C3A8>é”®ç˜å¯¼èˆªï¼æŒ‰é‡<C3A9>ç½®æ<C2AE>¢å¤<C3A5>åˆ<C3A5>å§çжæ€<C3A6>ã€?,
kTextWeak,
12.0f);
DrawCard(
drawList,
stateRect,
"状æ€<EFBFBD>æ˜è¦?,
"æŒ<EFBFBD>续显示 hoverã€<C3A3>focusã€<C3A3>active panelã€<C3A3>tabsã€<C3A3>result 和校验结果ã€?);
DrawButton(drawList, m_resetButtonRect, "é‡<EFBFBD>ç½®", m_resetButtonHovered);
const std::size_t selectedIndex =
ResolveUIEditorTabStripSelectedIndex(m_tabItems, m_controller.GetWorkspace().activePanelId);
const std::string selectedIndexText =
selectedIndex == UIEditorTabStripInvalidIndex ? "(none)" : std::to_string(selectedIndex);
const auto validation = m_controller.ValidateState();
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(m_hoverTarget, m_tabItems),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 96.0f),
std::string("Focused: ") + (m_tabStripState.focused ? "On" : "Off"),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 122.0f),
"Active Panel: " +
(m_controller.GetWorkspace().activePanelId.empty()
? std::string("(none)")
: m_controller.GetWorkspace().activePanelId),
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 148.0f),
"Selected Index: " + selectedIndexText,
kTextPrimary,
13.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 174.0f),
"Tabs: " + JoinTabTitles(m_tabItems),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 250.0f),
"Result: " + m_lastResult,
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 276.0f),
validation.IsValid() ? "校验: OK" : "校验: " + validation.message,
validation.IsValid() ? kSuccess : kDanger,
12.0f);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队�.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/tab_strip_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(stateRect.x + 16.0f, stateRect.y + 302.0f),
captureSummary,
kTextWeak,
12.0f);
DrawCard(
drawList,
previewCardRect,
"预览�,
"这里å<EFBFBD>ªæ”¾ä¸€ä¸?TabStrip 和一ä¸?content placeholder,用æ<C2A8>¥è§å¯?header ä¸?content frameã€?);
AppendUIEditorTabStripBackground(drawList, m_layout, m_tabStripState);
AppendUIEditorTabStripForeground(drawList, m_layout, m_tabItems, m_tabStripState);
drawList.AddText(
UIPoint(m_layout.contentRect.x + 20.0f, m_layout.contentRect.y + 22.0f),
"Content Placeholder",
kTextPrimary,
18.0f);
drawList.AddText(
UIPoint(m_layout.contentRect.x + 20.0f, m_layout.contentRect.y + 52.0f),
"当å‰<EFBFBD> active panel: " +
(m_controller.GetWorkspace().activePanelId.empty()
? std::string("(none)")
: m_controller.GetWorkspace().activePanelId),
kTextMuted,
13.0f);
drawList.AddText(
UIPoint(m_layout.contentRect.x + 20.0f, m_layout.contentRect.y + 76.0f),
"这里å<EFBFBD>ªéªŒè¯?TabStrip çš?content frame ä¸?focus å<>Œæ­¥ï¼Œä¸<C3A4>接业务内容ã€?,
kTextWeak,
12.0f);
drawList.AddText(
UIPoint(m_layout.contentRect.x + 20.0f, m_layout.contentRect.y + 100.0f),
"å<EFBFBD>¯ç¹å‡?Document B 切æ<E280A1>¢ï¼Œå†<C3A5>ç”?Left / Right / Home / End 验è¯<C3A8>é”®ç˜å¯¼èˆªã€?,
kTextWeak,
12.0f);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
UIEditorWorkspaceController m_controller = {};
UIEditorTabStripInteractionState m_interactionState = {};
UIEditorTabStripInteractionFrame m_tabStripFrame = {};
UIEditorTabStripState m_tabStripState = {};
UIEditorTabStripLayout m_layout = {};
std::vector<UIEditorTabStripItem> m_tabItems = {};
UIEditorTabStripHitTarget m_hoverTarget = {};
UIRect m_resetButtonRect = {};
bool m_resetButtonHovered = false;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_text_field_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_text_field_basic_validation
OUTPUT_NAME "XCUIEditorTextFieldBasicValidation"
)

View File

@@ -0,0 +1,827 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Fields/UIEditorTextFieldInteraction.h>
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Fields/UIEditorTextField.h>
#include <XCEditor/Foundation/UIEditorTheme.h>
#include "EditorValidationTheme.h"
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/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
// <20>箸艶霂湔<E99C82>嚗𡁻<E59A97><EFBFBD>𤐄摰𡁏甅撘譍<E69298> TextField <20><>抅蝖<E68A85>蝻𤥁<E89DBB><F0A4A581><EFBFBD><EFBFBD>鈭扎<E988AD><E6898E><EFBFBD><EFBFBD><E798A8><EFBFBD><EFBFBD><E8A1A3><EFBFBD><EFBFBD>?
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::UIEditorTextFieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorTextFieldInteractionResult;
using XCEngine::UI::Editor::UIEditorTextFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorTextFieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTextField;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorTextFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | TextField 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 inspectorRect = {};
UIRect inspectorHeaderRect = {};
UIRect sectionRect = {};
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 MapTextFieldKey(UINT keyCode) {
switch (keyCode) {
case VK_RETURN:
return static_cast<std::int32_t>(KeyCode::Enter);
case VK_ESCAPE:
return static_cast<std::int32_t>(KeyCode::Escape);
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 = 460.0f;
const float gap = shellMetrics.gap;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 252.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)(244.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.inspectorRect = UIRect(
layout.previewRect.x + 18.0f,
layout.previewRect.y + 54.0f,
(std::min)(392.0f, layout.previewRect.width - 36.0f),
150.0f);
layout.inspectorHeaderRect = UIRect(
layout.inspectorRect.x,
layout.inspectorRect.y,
layout.inspectorRect.width,
24.0f);
layout.sectionRect = UIRect(
layout.inspectorRect.x,
layout.inspectorRect.y + layout.inspectorHeaderRect.height,
layout.inspectorRect.width,
24.0f);
layout.fieldRect = UIRect(
layout.inspectorRect.x,
layout.sectionRect.y + layout.sectionRect.height + 2.0f,
layout.inspectorRect.width,
22.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "<EFBFBD>滨蔭", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
{ ActionId::Capture, "<EFBFBD>芸㦛(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
};
return layout;
}
XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics ResolvePropertyGridTextFieldMetrics() {
return XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldMetrics(
XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(),
XCEngine::UI::Editor::ResolveUIEditorTextFieldMetrics());
}
XCEngine::UI::Editor::Widgets::UIEditorTextFieldPalette ResolvePropertyGridTextFieldPalette() {
return XCEngine::UI::Editor::BuildUIEditorPropertyGridTextFieldPalette(
XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(),
XCEngine::UI::Editor::ResolveUIEditorTextFieldPalette());
}
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 UIEditorTextFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorTextFieldHitTargetKind::ValueBox:
return "value_box";
case UIEditorTextFieldHitTargetKind::Row:
return "row";
case UIEditorTextFieldHitTargetKind::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 MakeCharacterEvent(wchar_t character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = static_cast<std::uint32_t>(character);
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "撌脰窈瘙<EFBFBD><EFBFBD><EFBFBD>颲枏枂<EFBFBD>?captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
if (wParam == VK_F6) {
app->HandleFocusLost();
return 0;
}
const std::int32_t keyCode = MapTextFieldKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->HandleKeyDown(keyCode);
return 0;
}
}
break;
case WM_CHAR:
if (app != nullptr) {
app->HandleCharacter(static_cast<wchar_t>(wParam));
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1480,
920,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/text_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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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::GetEditorValidationShellMetrics());
}
void ResetScenario() {
m_spec = {};
m_spec.fieldId = "game_object_name";
m_spec.label = "Name";
m_spec.value = "Player";
m_spec.readOnly = false;
m_interactionState = {};
m_interactionState.textFieldState.focused = true;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "撌脤<EFBFBD>蝵桀<EFBFBD>暺䁅恕 TextField <20><EFBFBD>?;
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolvePropertyGridTextFieldMetrics();
m_frame = UpdateUIEditorTextFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
{},
metrics);
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshFrame();
}
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);
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorTextFieldInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorTextFieldInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleKeyDown(std::int32_t keyCode) {
const UIEditorTextFieldInteractionResult result = PumpEvents({ MakeKeyEvent(keyCode) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleCharacter(wchar_t character) {
if (character < 32) {
return;
}
const UIEditorTextFieldInteractionResult result = PumpEvents({ MakeCharacterEvent(character) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleFocusLost() {
const UIEditorTextFieldInteractionResult result =
PumpEvents({ MakeFocusEvent(UIInputEventType::FocusLost) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorTextFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolvePropertyGridTextFieldMetrics();
m_frame = UpdateUIEditorTextFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
std::move(events),
metrics);
return m_frame.result;
}
void UpdateResultText(const UIEditorTextFieldInteractionResult& result) {
if (result.editCommitted) {
m_lastResult = std::string("撌脫<EFBFBD>鈭斗<EFBFBD><EFBFBD>? ") + result.committedText;
return;
}
if (result.editCanceled) {
m_lastResult = "撌脣<EFBFBD><EFBFBD><EFBFBD>颲?;
return;
}
if (result.editStarted) {
m_lastResult = "撌脰<EFBFBD><EFBFBD><EFBFBD>颲烐<EFBFBD>?;
return;
}
if (result.focusChanged) {
m_lastResult = std::string("<EFBFBD><EFBFBD><EFBFBD><EFBFBD>: ") + (m_interactionState.textFieldState.focused ? "focused" : "lost");
return;
}
if (result.consumed) {
m_lastResult = "<EFBFBD>找辣撌脫<EFBFBD>韐寡<EFBFBD><EFBFBD>?;
return;
}
m_lastResult = "蝑匧<EFBFBD>鈭支<EFBFBD>";
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::Reset:
ResetScenario();
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "撌脰窈瘙<EFBFBD><EFBFBD><EFBFBD>颲枏枂<EFBFBD>?captures/latest.png";
break;
}
}
void RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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::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 = ResolvePropertyGridTextFieldMetrics();
const auto textPalette = ResolvePropertyGridTextFieldPalette();
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorTextFieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"餈嗘葵瘚贝<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>",
"撉諹<EFBFBD> UIEditorTextField <20><>抅蝖<E68A85>蝻𤥁<E89DBB>憟𤑳漲嚗䔶<E59A97>瘨匧<E798A8> PropertyGrid <20>碶遙雿?Inspector 銝𡁜𦛚<F0A1819C><EFBFBD><E9A489>?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. <20>孵稬 value box嚗峕<E59A97><E5B395>交糓<E4BAA4>西<EFBFBD><E8A5BF><EFBFBD>颲烐<E9A2B2><E78390><EFBFBD>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. <20><EFBFBD> focus <20><EFBFBD> Enter 撘<>憪讠<E686AA>颲𡢅<E9A2B2><F0A1A285>湔𦻖颲枏<E9A2B2>摮㛖泵銋笔<E98A8B><EFBFBD>憪讠<E686AA>颲㻫<E9A2B2>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 蝻𤥁<E89DBB><F0A4A581><EFBFBD><EFBFBD><EFBFBD>?Enter commit嚗峕<E59A97> Escape cancel<65>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 蝻𤥁<E89DBB><F0A4A581><EFBFBD><EFBFBD><EFBFBD>?F6 璅⊥<E79285> FocusLost嚗<74><E59A97><EFBFBD>𣂷漱<F0A382B7><E6BCB1><EFBFBD><EFBFBD><EFBFBD>𧋦撟園<E6929F><E59C92><EFBFBD><EFBFBD>颲烐<E9A2B2><E78390><EFBFBD>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 璉<><E79289>?Hover / Focus / Editing / Value / Result <20>臬炏<E887AC>峕郊<E5B395>湔鰵<E6B994>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. <20>?F12 <20>𣇉<EFBFBD><F0A38789>餅⏛<E9A485><EFBFBD><E69AB9><EFBFBD>蝖株恕<E6A0AA>芸𢆡<E88AB8>芸㦛頝臬<E9A09D><EFBFBD><EFBFBD>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "<EFBFBD><EFBFBD>");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"<EFBFBD><EFBFBD><EFBFBD><EFBFBD>閬?,
"<EFBFBD><EFBFBD><EFBFBD><EFBFBD>?hit / focus / editing / value / display / result<6C>?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.textFieldState.focused ? "<EFBFBD>? : "<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Editing: ") + (m_interactionState.textFieldState.editing ? "<EFBFBD>? : "<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
"Value: " + m_spec.value,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Display: " + m_interactionState.textFieldState.displayText,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "<EFBFBD>芸㦛<EFBFBD><EFBFBD>銝?.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/text_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
"Style: fixed",
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"TextField 憸<><E686B8>",
"餈䠷<EFBFBD><EFBFBD>芣𦆮銝<EFBFBD>銝芸𤐄摰𡁏甅撘?TextField嚗𣬚鍂<F0A3AC9A>仿<EFBFBD><EFBFBD>抅蝖<E68A85>摮埈挾銵䔶蛹<E494B6>?);
drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor);
drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f);
drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground);
drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f);
drawList.AddText(
UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f),
"Inspector",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor);
drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f);
drawList.AddText(
UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f),
"v Transform",
propertyPalette.sectionTextColor,
shellMetrics.bodyFontSize);
AppendUIEditorTextField(
drawList,
layout.fieldRect,
m_spec,
m_interactionState.textFieldState,
textPalette,
textMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorTextFieldSpec m_spec = {};
UIEditorTextFieldInteractionState m_interactionState = {};
UIEditorTextFieldInteractionFrame m_frame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_tree_view_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_tree_view_basic_validation
OUTPUT_NAME "XCUIEditorTreeViewBasicValidation"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -0,0 +1,815 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Collections/UIEditorTreeView.h>
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#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::Widgets::UIExpansionModel;
using XCEngine::UI::Widgets::UISelectionModel;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::UIEditorTreeViewInteractionFrame;
using XCEngine::UI::Editor::UIEditorTreeViewInteractionResult;
using XCEngine::UI::Editor::UIEditorTreeViewInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorTreeViewInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTreeViewBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTreeViewForeground;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTreeView;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewItem;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewLayout;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorTreeViewBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | TreeView Basic";
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 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 kTextSuccess(0.63f, 0.76f, 0.63f, 1.0f);
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
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 treeRect = {};
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 MapTreeNavigationKey(UINT keyCode) {
switch (keyCode) {
case VK_UP:
return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN:
return static_cast<std::int32_t>(KeyCode::Down);
case VK_LEFT:
return static_cast<std::int32_t>(KeyCode::Left);
case VK_RIGHT:
return static_cast<std::int32_t>(KeyCode::Right);
case VK_HOME:
return static_cast<std::int32_t>(KeyCode::Home);
case VK_END:
return static_cast<std::int32_t>(KeyCode::End);
default:
return static_cast<std::int32_t>(KeyCode::None);
}
}
ScenarioLayout BuildScenarioLayout(float width, float height) {
constexpr float margin = 20.0f;
constexpr float leftWidth = 430.0f;
constexpr float gap = 16.0f;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 214.0f);
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 92.0f);
layout.stateRect = UIRect(
margin,
layout.controlRect.y + layout.controlRect.height + gap,
leftWidth,
(std::max)(200.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.treeRect = UIRect(
layout.previewRect.x + 18.0f,
layout.previewRect.y + 64.0f,
layout.previewRect.width - 36.0f,
layout.previewRect.height - 84.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 40.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,
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 + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
}
}
void DrawButton(
UIDrawList& drawList,
const ButtonLayout& button,
bool hovered) {
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
drawList.AddText(
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
button.label,
kTextPrimary,
12.0f);
}
std::vector<UIEditorTreeViewItem> BuildTreeItems() {
return {
{ "scene", "Scene", 0u, false, 0.0f },
{ "camera", "Camera", 1u, true, 0.0f },
{ "lights", "Lights", 1u, false, 0.0f },
{ "directional-light", "Directional Light", 2u, true, 0.0f },
{ "fill-light", "Fill Light", 2u, true, 0.0f },
{ "ui-root", "UI Root", 0u, false, 0.0f },
{ "canvas", "Canvas", 1u, true, 0.0f },
{ "event-system", "EventSystem", 1u, true, 0.0f }
};
}
std::string JoinExpandedItems(
const std::vector<UIEditorTreeViewItem>& items,
const UIExpansionModel& expansionModel) {
std::ostringstream stream = {};
bool first = true;
for (const UIEditorTreeViewItem& item : items) {
if (!expansionModel.IsExpanded(item.itemId)) {
continue;
}
if (!first) {
stream << " | ";
}
first = false;
stream << item.label;
}
return first ? "(none)" : stream.str();
}
std::string JoinVisibleItems(
const std::vector<UIEditorTreeViewItem>& items,
const UIEditorTreeViewLayout& layout) {
if (layout.visibleItemIndices.empty()) {
return "(none)";
}
constexpr std::size_t kMaxVisibleLabels = 5u;
std::ostringstream stream = {};
const std::size_t labelCount = (std::min)(layout.visibleItemIndices.size(), kMaxVisibleLabels);
for (std::size_t index = 0u; index < labelCount; ++index) {
if (index > 0u) {
stream << " | ";
}
const std::size_t itemIndex = layout.visibleItemIndices[index];
if (itemIndex < items.size()) {
stream << items[itemIndex].label;
}
}
if (layout.visibleItemIndices.size() > labelCount) {
stream << " | +" << (layout.visibleItemIndices.size() - labelCount) << " more";
}
return stream.str();
}
std::string DescribeHitTarget(
const UIEditorTreeViewHitTarget& hitTarget,
const std::vector<UIEditorTreeViewItem>& items) {
if (hitTarget.itemIndex >= items.size()) {
return "(none)";
}
const std::string& label = items[hitTarget.itemIndex].label;
switch (hitTarget.kind) {
case UIEditorTreeViewHitTargetKind::Disclosure:
return "disclosure: " + label;
case UIEditorTreeViewHitTargetKind::Row:
return "row: " + label;
case UIEditorTreeViewHitTargetKind::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;
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "蟾イ隸キ豎よ穐蝗セ<EFBFBD>瑚セ灘<EFBFBD>蛻?captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
const std::int32_t keyCode = MapTreeNavigationKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->HandleNavigationKey(keyCode);
return 0;
}
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1480,
920,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/tree_view_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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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);
}
void ResetScenario() {
m_items = BuildTreeItems();
m_selectionModel = {};
m_selectionModel.SetSelection("camera");
m_expansionModel = {};
m_expansionModel.Expand("scene");
m_expansionModel.Expand("lights");
m_expansionModel.Expand("ui-root");
m_interactionState = {};
m_interactionState.treeViewState.focused = true;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "蟾イ驥咲スョ蛻ー鮟倩ョ、譬醍サ捺<EFBFBD>?;
RefreshTreeFrame();
}
void RefreshTreeFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
m_treeFrame =
UpdateUIEditorTreeViewInteraction(
m_interactionState,
m_selectionModel,
m_expansionModel,
layout.treeRect,
m_items,
{});
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshTreeFrame();
}
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);
PumpTreeEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpTreeEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
PumpTreeEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const bool wasFocused = m_interactionState.treeViewState.focused;
const bool insideTree = ContainsPoint(layout.treeRect, x, y);
const UIEditorTreeViewInteractionResult result =
PumpTreeEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result, wasFocused, insideTree);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleNavigationKey(std::int32_t keyCode) {
const bool wasFocused = m_interactionState.treeViewState.focused;
const UIEditorTreeViewInteractionResult result =
PumpTreeEvents({ MakeKeyEvent(keyCode) });
UpdateResultText(result, wasFocused, true);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorTreeViewInteractionResult PumpTreeEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
m_treeFrame =
UpdateUIEditorTreeViewInteraction(
m_interactionState,
m_selectionModel,
m_expansionModel,
layout.treeRect,
m_items,
std::move(events));
return m_treeFrame.result;
}
void UpdateResultText(
const UIEditorTreeViewInteractionResult& result,
bool wasFocused,
bool insideTree) {
if (result.keyboardNavigated && result.expansionChanged && !result.toggledItemId.empty()) {
m_lastResult = "髞ョ逶伜<EFBFBD>謐「螻募シ€: " + result.toggledItemId;
return;
}
if (result.keyboardNavigated && !result.selectedItemId.empty()) {
m_lastResult = "髞ョ逶倬€画叫: " + result.selectedItemId;
return;
}
if (result.expansionChanged && result.selectionChanged && !result.selectedItemId.empty()) {
m_lastResult = "謚伜匠蜷主屓謾?selection: " + result.selectedItemId;
return;
}
if (result.expansionChanged && !result.toggledItemId.empty()) {
m_lastResult = "<EFBFBD>困螻募シ€: " + result.toggledItemId;
return;
}
if (result.selectionChanged && !result.selectedItemId.empty()) {
m_lastResult = "騾我クュ陦? " + result.selectedItemId;
return;
}
if (!insideTree && wasFocused && !m_interactionState.treeViewState.focused) {
m_lastResult = "轤ケ蜃サ譬大、也ゥコ逋ス: focus 蟾イ貂<EFBDB2><EFBFBD>茎election 菫晉蕗";
return;
}
if (insideTree) {
m_lastResult = "轤ケ蜃サ譬大<EFBFBD>遨コ逋ス: 莉<>峩譁?focus / hover";
return;
}
m_lastResult = "遲牙セ<EFBFBD>コ、莠<EFBFBD>";
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::Reset:
ResetScenario();
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "蟾イ隸キ豎よ穐蝗セ<EFBFBD>瑚セ灘<EFBFBD>蛻?captures/latest.png";
break;
}
}
void RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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 ScenarioLayout layout = BuildScenarioLayout(width, height);
RefreshTreeFrame();
const UIEditorTreeViewHitTarget currentHit =
HitTestUIEditorTreeView(m_treeFrame.layout, m_mousePosition);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorTreeViewBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(
drawList,
layout.introRect,
"霑吩クェ豬玖ッ暮ェ瑚ッ∽サ€荵亥粥閭ス<EFBFBD><EFBFBD>",
"蜿ェ鬪瑚ッ?Editor TreeView 逧<>黒騾峨€∝アらコァ螻募シ€/謚伜匠蜥碁醗逶伜ッシ闊ェ縲ゆク肴カ牙所莉サ菴穂ク壼苅髱「譚ソ縲?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 轤ケ蜃サ row<6F>壼宵蛻<E5AEB5>困 selection<6F>敬over / selected / focused 蠢<>。サ閭ス譏守。ョ蛹コ蛻<EFBDBA>€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 轤ケ蜃サ disclosure<72>壼宵蛻<E5AEB5>困螻募シ€/謚伜匠<E4BC9C>御ク榊コ碑ッッ謾?selection縲?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 謖?Left / Right<68>夐ェ瑚ッ∵釜蜿<E9879C>縲∝ア募シ€<EFBDBC>御サ・蜿顔宛蟄仙アらコァ霍ウ霓ャ縲?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 謖?Up / Down / Home / End<6E>夐ェ瑚ッ∝庄隗∬。悟ッシ闊ェ<E9978A>御サ・蜿?current 荳?selection 蜷梧ュ・縲?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 轤ケ蜃サ譬大、也ゥコ逋ス貂<EFBDBD>勁 focus<75>妲12 謇句勘謌ェ蝗セ<E89D97>婢CUI_AUTO_CAPTURE_ON_STARTUP=1 蜿ッ蜷ッ蜉ィ閾ェ蜉ィ謌ェ蝗セ縲?,
kTextPrimary,
12.0f);
DrawCard(drawList, layout.controlRect, "謫堺ス<EFBFBD>");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(drawList, layout.stateRect, "迥カ諤∵遭隕?, "€?hit / focus / selection / current / expanded / visible縲?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit, m_items),
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.treeViewState.focused ? "on" : "off"),
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
"Selected: " +
(m_selectionModel.HasSelection() ? m_selectionModel.GetSelectedId() : std::string("(none)")),
kTextSuccess,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
std::string("Current: ") +
(m_interactionState.keyboardNavigation.HasCurrentIndex()
? std::to_string(m_interactionState.keyboardNavigation.GetCurrentIndex())
: std::string("(none)")),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Expanded: " + JoinExpandedItems(m_items, m_expansionModel),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Visible(" + std::to_string(m_treeFrame.layout.visibleItemIndices.size()) + "): " +
JoinVisibleItems(m_items, m_treeFrame.layout),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
"Result: " + m_lastResult,
kTextPrimary,
12.0f);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "謌ェ蝗セ謗帝弌荳?.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/tree_view_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
captureSummary,
kTextWeak,
12.0f);
DrawCard(drawList, layout.previewRect, "TreeView 鬚<><EFBFBD>", "霑咎㈹蜿ェ謾セ荳€荳?TreeView<65>御ク肴キキ蜈・ Hierarchy / Inspector 遲我ク壼苅蜀<E88B85>ョケ縲?);
AppendUIEditorTreeViewBackground(
drawList,
m_treeFrame.layout,
m_items,
m_selectionModel,
m_interactionState.treeViewState);
AppendUIEditorTreeViewForeground(drawList, m_treeFrame.layout, m_items);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
std::vector<UIEditorTreeViewItem> m_items = {};
UISelectionModel m_selectionModel = {};
UIExpansionModel m_expansionModel = {};
UIEditorTreeViewInteractionState m_interactionState = {};
UIEditorTreeViewInteractionFrame m_treeFrame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_tree_view_inline_rename_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_tree_view_inline_rename_validation
OUTPUT_NAME "XCUIEditorTreeViewInlineRenameValidation"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,942 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#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"
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
// <20>箸艶霂湔<E99C82>嚗𡁻<E59A97><EFBFBD>𤐄摰𡁏甅撘譍<E69298> TreeView <20>?inline rename <20>臬𢆡<E887AC><F0A286A1><EFBFBD>鈭扎<E988AD><E6898E><EFBFBD><EFBFBD><E798A8>憭㚚<E686AD><E39A9A>孵稬<E5ADB5>𣂷漱<F0A382B7>?
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::Tests::EditorUI::EditorValidationShellMetrics;
using XCEngine::Tests::EditorUI::EditorValidationShellPalette;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Widgets::UIExpansionModel;
using XCEngine::UI::Widgets::UISelectionModel;
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;
using XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics;
using XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette;
using XCEngine::UI::Editor::ResolveUIEditorTextFieldMetrics;
using XCEngine::UI::Editor::ResolveUIEditorTextFieldPalette;
using XCEngine::UI::Editor::UIEditorInlineRenameSessionFrame;
using XCEngine::UI::Editor::UIEditorInlineRenameSessionRequest;
using XCEngine::UI::Editor::UIEditorInlineRenameSessionState;
using XCEngine::UI::Editor::UIEditorTreeViewInteractionFrame;
using XCEngine::UI::Editor::UIEditorTreeViewInteractionResult;
using XCEngine::UI::Editor::UIEditorTreeViewInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorInlineRenameSession;
using XCEngine::UI::Editor::UpdateUIEditorTreeViewInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTextFieldBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTextFieldForeground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTreeViewBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTreeViewForeground;
using XCEngine::UI::Editor::Widgets::FindUIEditorTreeViewItemIndex;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTreeView;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewInvalidIndex;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewItem;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldPalette;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorTreeViewInlineRenameValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | TreeView Inline Rename";
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 treeRect = {};
std::vector<ButtonLayout> buttons = {};
};
std::filesystem::path ResolveRepoRootPath();
bool ContainsPoint(const UIRect& rect, float x, float y);
std::int32_t MapTreeKey(UINT keyCode);
ScenarioLayout BuildScenarioLayout(float width, float height, const EditorValidationShellMetrics& shellMetrics);
void DrawCard(UIDrawList& drawList, const UIRect& rect, const EditorValidationShellPalette& shellPalette, const EditorValidationShellMetrics& shellMetrics, std::string_view title, std::string_view subtitle = {});
void DrawButton(UIDrawList& drawList, const ButtonLayout& button, const EditorValidationShellPalette& shellPalette, const EditorValidationShellMetrics& shellMetrics, bool hovered);
std::vector<UIEditorTreeViewItem> BuildTreeItems();
std::string DescribeHitTarget(const UIEditorTreeViewHitTarget& hitTarget, const std::vector<UIEditorTreeViewItem>& items);
UIInputEvent MakePointerEvent(UIInputEventType type, const UIPoint& position, UIPointerButton button = UIPointerButton::None);
UIInputEvent MakeKeyEvent(std::int32_t keyCode);
UIInputEvent MakeCharacterEvent(wchar_t character);
class ScenarioApp {
public:
int Run(HINSTANCE hInstance, int nCmdShow);
private:
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
bool Initialize(HINSTANCE hInstance, int nCmdShow);
void Shutdown();
ScenarioLayout GetLayout() const;
void ResetScenario();
void RefreshTreeFrame();
void OnResize(UINT width, UINT height);
void HandleMouseMove(float x, float y);
void HandleMouseLeave();
void HandleLeftButtonDown(float x, float y);
void HandleLeftButtonUp(float x, float y);
void HandleKeyDown(UINT virtualKey);
void HandleCharacter(wchar_t character);
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y);
const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const;
UIEditorTreeViewInteractionResult PumpTreeEvents(std::vector<UIInputEvent> events);
void BeginRename(const std::string& itemId);
void PumpRenameEvents(std::vector<UIInputEvent> events);
void ApplyRenameFrame(const UIEditorInlineRenameSessionFrame& frame);
void UpdateTreeResultText(const UIEditorTreeViewInteractionResult& result);
UIRect BuildRenameBoundsForActiveItem() const;
UIRect BuildRenameBounds(std::size_t itemIndex) const;
UIEditorInlineRenameSessionRequest BuildRenameRequest(bool beginSession) const;
UIEditorTextFieldMetrics ResolveHostedTextFieldMetrics() const;
UIEditorTextFieldMetrics ResolveInlineRenameMetrics(const UIRect& bounds) const;
UIEditorTextFieldPalette ResolveInlineRenamePalette() const;
void ExecuteAction(ActionId action);
void RenderFrame();
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
std::vector<UIEditorTreeViewItem> m_items = {};
UISelectionModel m_selectionModel = {};
UIExpansionModel m_expansionModel = {};
UIEditorTreeViewInteractionState m_treeInteractionState = {};
UIEditorTreeViewInteractionFrame m_treeFrame = {};
UIEditorInlineRenameSessionState m_renameState = {};
UIEditorInlineRenameSessionFrame m_renameFrame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastCommittedItemId = {};
std::string m_lastCommittedValue = {};
std::string m_lastResult = {};
};
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 MapTreeKey(UINT keyCode) {
switch (keyCode) {
case VK_UP: return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN: return static_cast<std::int32_t>(KeyCode::Down);
case VK_LEFT: return static_cast<std::int32_t>(KeyCode::Left);
case VK_RIGHT: return static_cast<std::int32_t>(KeyCode::Right);
case VK_HOME: return static_cast<std::int32_t>(KeyCode::Home);
case VK_END: return static_cast<std::int32_t>(KeyCode::End);
case VK_BACK: return static_cast<std::int32_t>(KeyCode::Backspace);
case VK_DELETE: return static_cast<std::int32_t>(KeyCode::Delete);
case VK_RETURN: return static_cast<std::int32_t>(KeyCode::Enter);
case VK_ESCAPE: return static_cast<std::int32_t>(KeyCode::Escape);
case VK_F2: return static_cast<std::int32_t>(KeyCode::F2);
default: return static_cast<std::int32_t>(KeyCode::None);
}
}
ScenarioLayout BuildScenarioLayout(float width, float height, const 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, 252.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)(260.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
layout.previewRect = UIRect(leftWidth + margin * 2.0f, margin, (std::max)(520.0f, width - leftWidth - margin * 3.0f), height - margin * 2.0f);
layout.treeRect = UIRect(layout.previewRect.x + 22.0f, layout.previewRect.y + 72.0f, layout.previewRect.width - 44.0f, layout.previewRect.height - 104.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "<EFBFBD>滨蔭", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
{ ActionId::Capture, "<EFBFBD>芸㦛(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
};
return layout;
}
void DrawCard(UIDrawList& drawList, const UIRect& rect, const EditorValidationShellPalette& shellPalette, const 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 EditorValidationShellPalette& shellPalette, const 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::vector<UIEditorTreeViewItem> BuildTreeItems() {
return {
{ "scene", "Scene", 0u, false, 0.0f },
{ "camera", "Camera", 1u, true, 0.0f },
{ "lights", "Lights", 1u, false, 0.0f },
{ "directional-light", "Directional Light", 2u, true, 0.0f },
{ "fill-light", "Fill Light", 2u, true, 0.0f },
{ "characters", "Characters", 0u, false, 0.0f },
{ "hero", "Hero", 1u, false, 0.0f },
{ "hero-mesh", "HeroMesh", 2u, true, 0.0f },
{ "hero-rig", "HeroRig", 2u, true, 0.0f }
};
}
std::string DescribeHitTarget(const UIEditorTreeViewHitTarget& hitTarget, const std::vector<UIEditorTreeViewItem>& items) {
if (hitTarget.kind == UIEditorTreeViewHitTargetKind::None || hitTarget.itemIndex >= items.size()) {
return "(none)";
}
return items[hitTarget.itemIndex].itemId;
}
UIInputEvent MakePointerEvent(UIInputEventType type, const UIPoint& position, UIPointerButton button) {
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 MakeCharacterEvent(wchar_t character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = static_cast<std::uint32_t>(character);
return event;
}
int ScenarioApp::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);
}
LRESULT CALLBACK ScenarioApp::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCCREATE) {
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE: if (app != nullptr && wParam != SIZE_MINIMIZED) { app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam))); } return 0;
case WM_MOUSEMOVE: if (app != nullptr) { app->HandleMouseMove(static_cast<float>(GET_X_LPARAM(lParam)), static_cast<float>(GET_Y_LPARAM(lParam))); return 0; } break;
case WM_MOUSELEAVE: if (app != nullptr) { app->HandleMouseLeave(); return 0; } break;
case WM_LBUTTONDOWN: if (app != nullptr) { app->HandleLeftButtonDown(static_cast<float>(GET_X_LPARAM(lParam)), static_cast<float>(GET_Y_LPARAM(lParam))); return 0; } break;
case WM_LBUTTONUP: if (app != nullptr) { app->HandleLeftButtonUp(static_cast<float>(GET_X_LPARAM(lParam)), static_cast<float>(GET_Y_LPARAM(lParam))); return 0; } break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN: if (app != nullptr) { app->HandleKeyDown(static_cast<UINT>(wParam)); return 0; } break;
case WM_CHAR: if (app != nullptr) { app->HandleCharacter(static_cast<wchar_t>(wParam)); return 0; } break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND: return 1;
case WM_DESTROY: if (app != nullptr) { app->m_hwnd = nullptr; } PostQuitMessage(0); return 0;
default: break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool ScenarioApp::Initialize(HINSTANCE hInstance, int nCmdShow) {
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, 1540, 940, 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_captureRoot = ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/tree_view_inline_rename/captures";
m_autoScreenshot.Initialize(m_captureRoot);
ResetScenario();
return true;
}
void ScenarioApp::Shutdown() {
m_autoScreenshot.Shutdown();
m_renderer.Shutdown();
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
DestroyWindow(m_hwnd);
}
m_hwnd = nullptr;
if (m_windowClassAtom != 0) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout ScenarioApp::GetLayout() const {
RECT clientRect = {};
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::GetEditorValidationShellMetrics());
}
void ScenarioApp::ResetScenario() {
m_items = BuildTreeItems();
m_selectionModel = {};
m_selectionModel.SetSelection("hero-mesh");
m_expansionModel = {};
m_expansionModel.Expand("scene");
m_expansionModel.Expand("lights");
m_expansionModel.Expand("characters");
m_expansionModel.Expand("hero");
m_treeInteractionState = {};
m_treeInteractionState.treeViewState.focused = true;
m_renameState = {};
m_renameFrame = {};
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastCommittedItemId.clear();
m_lastCommittedValue.clear();
m_lastResult = "撌脤<EFBFBD>蝵桀<EFBFBD>暺䁅恕<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>餈𥕦<EFBFBD> hero-mesh <20>?inline rename<6D>?;
RefreshTreeFrame();
BeginRename("hero-mesh");
}
void ScenarioApp::RefreshTreeFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
m_treeFrame = UpdateUIEditorTreeViewInteraction(m_treeInteractionState, m_selectionModel, m_expansionModel, layout.treeRect, m_items, {});
if (m_renameState.active) {
const UIEditorInlineRenameSessionRequest request = BuildRenameRequest(false);
m_renameFrame = UpdateUIEditorInlineRenameSession(m_renameState, request, {}, ResolveInlineRenameMetrics(request.bounds));
ApplyRenameFrame(m_renameFrame);
}
}
void ScenarioApp::OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshTreeFrame();
}
void ScenarioApp::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);
const UIInputEvent pointerEvent =
MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition);
if (m_renameState.active) {
PumpRenameEvents({ pointerEvent });
} else {
PumpTreeEvents({ pointerEvent });
}
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void ScenarioApp::HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
const UIInputEvent leaveEvent =
MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition);
if (m_renameState.active) {
PumpRenameEvents({ leaveEvent });
} else {
PumpTreeEvents({ leaveEvent });
}
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void ScenarioApp::HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIInputEvent pointerEvent =
MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left);
if (m_renameState.active) {
const UIRect renameBounds = BuildRenameBoundsForActiveItem();
const bool insideRename = ContainsPoint(renameBounds, x, y);
PumpRenameEvents({ pointerEvent });
if (!insideRename && !m_renameState.active) {
PumpTreeEvents({ pointerEvent });
}
} else {
PumpTreeEvents({ pointerEvent });
}
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void ScenarioApp::HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIInputEvent pointerEvent =
MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left);
if (m_renameState.active) {
const UIRect renameBounds = BuildRenameBoundsForActiveItem();
const bool insideRename = ContainsPoint(renameBounds, x, y);
PumpRenameEvents({ pointerEvent });
if (!insideRename && !m_renameState.active) {
const UIEditorTreeViewInteractionResult result = PumpTreeEvents({ pointerEvent });
UpdateTreeResultText(result);
}
} else {
const UIEditorTreeViewInteractionResult result = PumpTreeEvents({ pointerEvent });
UpdateTreeResultText(result);
}
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void ScenarioApp::HandleKeyDown(UINT virtualKey) {
if (virtualKey == VK_F12) {
m_autoScreenshot.RequestCapture("manual_f12");
m_lastResult = "撌脰窈瘙<EFBFBD><EFBFBD><EFBFBD>颲枏枂<EFBFBD>?captures/latest.png";
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const std::int32_t keyCode = MapTreeKey(virtualKey);
if (keyCode == static_cast<std::int32_t>(KeyCode::None)) {
return;
}
if (m_renameState.active) {
PumpRenameEvents({ MakeKeyEvent(keyCode) });
} else {
const UIEditorTreeViewInteractionResult result =
PumpTreeEvents({ MakeKeyEvent(keyCode) });
if (result.renameRequested) {
BeginRename(result.renameItemId);
} else {
UpdateTreeResultText(result);
}
}
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void ScenarioApp::HandleCharacter(wchar_t character) {
if (character < 32 || !m_renameState.active) {
return;
}
PumpRenameEvents({ MakeCharacterEvent(character) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void ScenarioApp::UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
const ButtonLayout* ScenarioApp::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;
}
UIEditorTreeViewInteractionResult ScenarioApp::PumpTreeEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
m_treeFrame = UpdateUIEditorTreeViewInteraction(
m_treeInteractionState,
m_selectionModel,
m_expansionModel,
layout.treeRect,
m_items,
std::move(events));
return m_treeFrame.result;
}
void ScenarioApp::BeginRename(const std::string& itemId) {
if (itemId.empty()) {
return;
}
const std::size_t itemIndex = FindUIEditorTreeViewItemIndex(m_items, itemId);
if (itemIndex == UIEditorTreeViewInvalidIndex) {
return;
}
UIEditorInlineRenameSessionRequest request = {};
request.beginSession = true;
request.itemId = itemId;
request.initialText = m_items[itemIndex].label;
request.bounds = BuildRenameBounds(itemIndex);
m_renameFrame = UpdateUIEditorInlineRenameSession(
m_renameState,
request,
{},
ResolveInlineRenameMetrics(request.bounds));
ApplyRenameFrame(m_renameFrame);
}
void ScenarioApp::PumpRenameEvents(std::vector<UIInputEvent> events) {
if (!m_renameState.active) {
return;
}
const UIEditorInlineRenameSessionRequest request = BuildRenameRequest(false);
m_renameFrame = UpdateUIEditorInlineRenameSession(
m_renameState,
request,
std::move(events),
ResolveInlineRenameMetrics(request.bounds));
ApplyRenameFrame(m_renameFrame);
}
void ScenarioApp::ApplyRenameFrame(const UIEditorInlineRenameSessionFrame& frame) {
const auto& result = frame.result;
if (result.sessionStarted) {
m_lastResult = "撌脰<EFBFBD><EFBFBD>?inline rename: " + result.itemId;
return;
}
if (result.sessionCommitted) {
const std::size_t itemIndex = FindUIEditorTreeViewItemIndex(m_items, result.itemId);
if (itemIndex != UIEditorTreeViewInvalidIndex) {
m_items[itemIndex].label = result.valueAfter;
m_lastCommittedItemId = result.itemId;
m_lastCommittedValue = result.valueAfter;
RefreshTreeFrame();
}
m_lastResult = "撌脫<EFBFBD>鈭?rename: " + result.itemId + " -> " + result.valueAfter;
return;
}
if (result.sessionCanceled) {
m_lastResult = "撌脣<EFBFBD>瘨?rename: " + result.itemId;
return;
}
if (m_renameState.active && result.textFieldResult.consumed) {
m_lastResult =
"蝻𤥁<EFBFBD>銝? " + m_renameState.itemId + " -> " +
m_renameState.textFieldInteraction.textFieldState.displayText;
}
}
void ScenarioApp::UpdateTreeResultText(const UIEditorTreeViewInteractionResult& result) {
if (result.renameRequested) {
m_lastResult = "<EFBFBD><EFBFBD> rename 霂瑟<E99C82>: " + result.renameItemId;
return;
}
if (result.keyboardNavigated && !result.selectedItemId.empty()) {
m_lastResult = "<EFBFBD><EFBFBD>撖潸⏛<EFBFBD>? " + result.selectedItemId;
return;
}
if (result.expansionChanged && !result.toggledItemId.empty()) {
m_lastResult = "<EFBFBD><EFBFBD>揢撅訫<EFBFBD>: " + result.toggledItemId;
return;
}
if (result.selectionChanged && !result.selectedItemId.empty()) {
m_lastResult = "<EFBFBD>劐葉銵? " + result.selectedItemId;
return;
}
if (result.consumed && result.hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) {
m_lastResult = "<EFBFBD>孵稬銵? " + DescribeHitTarget(result.hitTarget, m_items);
return;
}
if (result.consumed && result.hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure) {
m_lastResult = "<EFBFBD>孵稬 disclosure: " + DescribeHitTarget(result.hitTarget, m_items);
return;
}
m_lastResult = "蝑匧<EFBFBD>鈭支<EFBFBD>";
}
UIRect ScenarioApp::BuildRenameBoundsForActiveItem() const {
if (!m_renameState.active) {
return {};
}
const std::size_t itemIndex = FindUIEditorTreeViewItemIndex(m_items, m_renameState.itemId);
return BuildRenameBounds(itemIndex);
}
UIRect ScenarioApp::BuildRenameBounds(std::size_t itemIndex) const {
if (itemIndex == UIEditorTreeViewInvalidIndex) {
return {};
}
const auto& layout = m_treeFrame.layout;
std::size_t visibleIndex = UIEditorTreeViewInvalidIndex;
for (std::size_t index = 0u; index < layout.visibleItemIndices.size(); ++index) {
if (layout.visibleItemIndices[index] == itemIndex) {
visibleIndex = index;
break;
}
}
if (visibleIndex == UIEditorTreeViewInvalidIndex ||
visibleIndex >= layout.labelRects.size() ||
visibleIndex >= layout.rowRects.size()) {
return {};
}
const UIEditorTextFieldMetrics hostedMetrics = ResolveHostedTextFieldMetrics();
const UIRect& rowRect = layout.rowRects[visibleIndex];
const UIRect& labelRect = layout.labelRects[visibleIndex];
const float x = (std::max)(rowRect.x, labelRect.x - hostedMetrics.valueTextInsetX);
const float right = rowRect.x + rowRect.width - 8.0f;
const float width = (std::max)(120.0f, right - x);
return UIRect(x, rowRect.y, width, rowRect.height);
}
UIEditorInlineRenameSessionRequest ScenarioApp::BuildRenameRequest(bool beginSession) const {
UIEditorInlineRenameSessionRequest request = {};
request.beginSession = beginSession;
request.itemId = m_renameState.itemId;
request.initialText = m_renameState.textFieldSpec.value;
request.bounds = BuildRenameBoundsForActiveItem();
return request;
}
UIEditorTextFieldMetrics ScenarioApp::ResolveHostedTextFieldMetrics() const {
const auto propertyMetrics = ResolveUIEditorPropertyGridMetrics();
const auto textMetrics = ResolveUIEditorTextFieldMetrics();
return BuildUIEditorPropertyGridTextFieldMetrics(propertyMetrics, textMetrics);
}
UIEditorTextFieldMetrics ScenarioApp::ResolveInlineRenameMetrics(const UIRect& bounds) const {
return BuildUIEditorInlineRenameTextFieldMetrics(bounds, ResolveHostedTextFieldMetrics());
}
UIEditorTextFieldPalette ScenarioApp::ResolveInlineRenamePalette() const {
const auto propertyPalette = ResolveUIEditorPropertyGridPalette();
const auto textPalette = ResolveUIEditorTextFieldPalette();
return BuildUIEditorPropertyGridTextFieldPalette(propertyPalette, textPalette);
}
void ScenarioApp::ExecuteAction(ActionId action) {
switch (action) {
case ActionId::Reset:
ResetScenario();
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "撌脰窈瘙<EFBFBD><EFBFBD><EFBFBD>颲枏枂<EFBFBD>?captures/latest.png";
break;
}
}
void ScenarioApp::RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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::GetEditorValidationShellMetrics();
const auto shellPalette =
XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshTreeFrame();
const UIEditorTreeViewHitTarget currentHit =
HitTestUIEditorTreeView(m_treeFrame.layout, m_mousePosition);
std::vector<UIEditorTreeViewItem> renderItems = m_items;
if (m_renameState.active) {
const std::size_t activeIndex = FindUIEditorTreeViewItemIndex(m_items, m_renameState.itemId);
if (activeIndex != UIEditorTreeViewInvalidIndex && activeIndex < renderItems.size()) {
renderItems[activeIndex].label.clear();
}
}
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorTreeViewInlineRename");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"餈嗘葵瘚贝<EFBFBD>撉諹<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>",
"<EFBFBD><EFBFBD>霂?Editor TreeView <20>?inline rename嚗𡁻<E59A97>霈方<E99C88><E696B9><EFBFBD>颲㻫<E9A2B2><E3BBAB><EFBFBD>蝚衣<E89D9A>颲㻫<E9A2B2><E3BBAB>nter <20>𣂷漱<F0A382B7><E6BCB1>sc <20>𡝗<EFBFBD><F0A19D97><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E9A483><EFBFBD>鈭扎<E988AD>?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. <20>臬𢆡<E887AC>𡡞<EFBFBD>霈方<E99C88><E696B9>?hero-mesh rename嚗𥡝<E59A97><F0A5A19D><EFBFBD><E4BAA4><EFBFBD><EFBFBD><E996AC> label <20><EFBFBD>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 颲枏<E9A2B2>摮㛖泵<E39B96>𠬍<EFBFBD>Draft 敹<>◆摰墧𧒄<E5A2A7><EFBFBD>嚗𥕦<E59A97><F0A595A6><EFBFBD>倌銝滩<E98A9D><E6BBA9>?overlay <20><EFBFBD><E683A9>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. <20>?Enter嚗𡁜<E59A97>蝘啣<E89D98><E595A3>?TreeViewItem.label嚗<6C><EFBFBD><E5838E><EFBFBD>?rename<6D>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. <20>?Esc嚗𡁜<E59A97><EFBFBD><E798A8>颲𡢅<E9A2B2>靽萘<E99DBD><E89098><EFBFBD><EFBFBD>蝑整<E89D91>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. rename 銝剔<E98A9D><E58994><EFBFBD><E9A489><EFBFBD>憭㚚<E686AD>嚗𡁏<E59A97>鈭文<E988AD><E69687><EFBFBD>蝔選<E89D94>撟園<E6929F><E59C92><EFBFBD><EFBFBD>颲烐<E9A2B2><E78390><EFBFBD>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. <20><><EFBFBD>𧒄<EFBFBD><F0A79284><EFBFBD> Esc <20><><EFBFBD><EFBFBD><E7B6BD><EFBFBD><E6BB9A><EFBFBD><E9A485><EFBFBD><EFBFBD><EFBFBD><EFBFBD> F2嚗拃12 <20>舀⏛<E88880><EFBFBD>?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "<EFBFBD><EFBFBD>");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"<EFBFBD><EFBFBD><EFBFBD><EFBFBD>閬?,
"<EFBFBD><EFBFBD><EFBFBD><EFBFBD>?TreeView <20>㗇𥋘<E39787><EFBFBD><E59786><EFBFBD><EFBFBD>虾閫<E899BE>揣撘訫<E69298> rename <20>笔𦶢<E7AC94><EFBFBD><E586BD>臬炏<E887AC>峕郊<E5B395>?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit, m_items),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Tree Focused: ") + (m_treeInteractionState.treeViewState.focused ? "on" : "off"),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
"Selected Item: " +
(m_selectionModel.HasSelection() ? m_selectionModel.GetSelectedId() : std::string("(none)")),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
std::string("Rename Active: ") + (m_renameState.active ? "yes" : "no"),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Rename Item: " + (m_renameState.active ? m_renameState.itemId : std::string("(none)")),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Draft: " + (m_renameState.active
? m_renameState.textFieldInteraction.textFieldState.displayText
: std::string("(none)")),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
"Committed: " + (m_lastCommittedValue.empty()
? std::string("(none)")
: (m_lastCommittedItemId + " -> " + m_lastCommittedValue)),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
std::string("Current Index: ") +
(m_treeInteractionState.keyboardNavigation.HasCurrentIndex()
? std::to_string(m_treeInteractionState.keyboardNavigation.GetCurrentIndex())
: std::string("(none)")),
shellPalette.textMuted,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f),
"Visible Count: " + std::to_string(m_treeFrame.layout.visibleItemIndices.size()),
shellPalette.textMuted,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 286.0f),
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "<EFBFBD>芸㦛<EFBFBD><EFBFBD>銝?.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/tree_view_inline_rename/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 310.0f),
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 334.0f),
"Style: fixed",
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"TreeView 憸<><E686B8>",
"餈䠷<EFBFBD><EFBFBD>芣𦆮銝<EFBFBD>銝?TreeView嚗<77><EFBFBD>冽𧒄暺䁅恕<E48185>湔𦻖餈𥕦<E9A488> hero-mesh <20>?rename嚗䔶噶鈭擧⏛<E693A7><EFBFBD><E69B89><EFBFBD><E88AA3>?);
AppendUIEditorTreeViewBackground(
drawList,
m_treeFrame.layout,
renderItems,
m_selectionModel,
m_treeInteractionState.treeViewState);
AppendUIEditorTreeViewForeground(drawList, m_treeFrame.layout, renderItems);
if (m_renameState.active) {
const UIEditorTextFieldPalette palette = ResolveInlineRenamePalette();
const UIEditorTextFieldMetrics metrics =
ResolveInlineRenameMetrics(BuildRenameBoundsForActiveItem());
AppendUIEditorTextFieldBackground(
drawList,
m_renameFrame.layout,
m_renameState.textFieldSpec,
m_renameState.textFieldInteraction.textFieldState,
palette,
metrics);
AppendUIEditorTextFieldForeground(
drawList,
m_renameFrame.layout,
m_renameState.textFieldSpec,
m_renameState.textFieldInteraction.textFieldState,
palette,
metrics);
}
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_tree_view_multiselect_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_tree_view_multiselect_validation
OUTPUT_NAME "XCUIEditorTreeViewMultiSelectValidation"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -0,0 +1,971 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Collections/UIEditorTreeView.h>
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
#include <XCEngine/UI/Widgets/UISelectionModel.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#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::UIInputModifiers;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Widgets::UIExpansionModel;
using XCEngine::UI::Widgets::UISelectionModel;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::UIEditorTreeViewInteractionFrame;
using XCEngine::UI::Editor::UIEditorTreeViewInteractionResult;
using XCEngine::UI::Editor::UIEditorTreeViewInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorTreeViewInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTreeViewBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorTreeViewForeground;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTreeView;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewInvalidIndex;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewItem;
using XCEngine::UI::Editor::Widgets::UIEditorTreeViewLayout;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorTreeViewMultiSelectValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | TreeView MultiSelect";
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 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 kTextSuccess(0.63f, 0.76f, 0.63f, 1.0f);
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
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 treeRect = {};
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;
}
const char* BoolText(bool value) {
return value ? "true" : "false";
}
UIInputModifiers QueryKeyboardModifiers() {
UIInputModifiers modifiers = {};
modifiers.shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
modifiers.control = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
modifiers.alt = (GetKeyState(VK_MENU) & 0x8000) != 0;
modifiers.super = (GetKeyState(VK_LWIN) & 0x8000) != 0 || (GetKeyState(VK_RWIN) & 0x8000) != 0;
return modifiers;
}
UIInputModifiers QueryPointerModifiers(WPARAM wParam) {
UIInputModifiers modifiers = QueryKeyboardModifiers();
modifiers.shift = modifiers.shift || (wParam & MK_SHIFT) != 0;
modifiers.control = modifiers.control || (wParam & MK_CONTROL) != 0;
return modifiers;
}
std::int32_t MapTreeNavigationKey(UINT keyCode) {
switch (keyCode) {
case VK_UP:
return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN:
return static_cast<std::int32_t>(KeyCode::Down);
case VK_LEFT:
return static_cast<std::int32_t>(KeyCode::Left);
case VK_RIGHT:
return static_cast<std::int32_t>(KeyCode::Right);
case VK_HOME:
return static_cast<std::int32_t>(KeyCode::Home);
case VK_END:
return static_cast<std::int32_t>(KeyCode::End);
default:
return static_cast<std::int32_t>(KeyCode::None);
}
}
ScenarioLayout BuildScenarioLayout(float width, float height) {
constexpr float margin = 20.0f;
constexpr float leftWidth = 456.0f;
constexpr float gap = 16.0f;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 272.0f);
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 92.0f);
layout.stateRect = UIRect(
margin,
layout.controlRect.y + layout.controlRect.height + gap,
leftWidth,
(std::max)(240.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.treeRect = UIRect(
layout.previewRect.x + 18.0f,
layout.previewRect.y + 64.0f,
layout.previewRect.width - 36.0f,
layout.previewRect.height - 84.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 40.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,
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 + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
}
}
void DrawButton(UIDrawList& drawList, const ButtonLayout& button, bool hovered) {
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
drawList.AddText(UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f);
}
std::vector<UIEditorTreeViewItem> BuildTreeItems() {
return {
{ "scene", "Scene", 0u, false, 0.0f },
{ "camera", "Camera", 1u, true, 0.0f },
{ "lights", "Lights", 1u, false, 0.0f },
{ "directional-light", "Directional Light", 2u, true, 0.0f },
{ "fill-light", "Fill Light", 2u, true, 0.0f },
{ "characters", "Characters", 0u, false, 0.0f },
{ "hero", "Hero", 1u, false, 0.0f },
{ "hero-mesh", "HeroMesh", 2u, true, 0.0f },
{ "hero-rig", "HeroRig", 2u, true, 0.0f },
{ "ui-root", "UI Root", 0u, false, 0.0f },
{ "canvas", "Canvas", 1u, false, 0.0f },
{ "button", "Button", 2u, true, 0.0f },
{ "event-system", "EventSystem", 1u, true, 0.0f }
};
}
std::string JoinSelectedIds(const UISelectionModel& selectionModel) {
if (selectionModel.GetSelectedIds().empty()) {
return "(none)";
}
std::ostringstream stream = {};
for (std::size_t index = 0u; index < selectionModel.GetSelectedIds().size(); ++index) {
if (index > 0u) {
stream << " | ";
}
stream << selectionModel.GetSelectedIds()[index];
}
return stream.str();
}
std::string JoinExpandedIds(
const std::vector<UIEditorTreeViewItem>& items,
const UIExpansionModel& expansionModel) {
std::ostringstream stream = {};
bool first = true;
for (const UIEditorTreeViewItem& item : items) {
if (!expansionModel.IsExpanded(item.itemId)) {
continue;
}
if (!first) {
stream << " | ";
}
first = false;
stream << item.itemId;
}
return first ? "(none)" : stream.str();
}
std::string JoinVisibleIds(
const std::vector<UIEditorTreeViewItem>& items,
const UIEditorTreeViewLayout& layout) {
if (layout.visibleItemIndices.empty()) {
return "(none)";
}
std::ostringstream stream = {};
for (std::size_t visibleIndex = 0u; visibleIndex < layout.visibleItemIndices.size(); ++visibleIndex) {
if (visibleIndex > 0u) {
stream << " | ";
}
const std::size_t itemIndex = layout.visibleItemIndices[visibleIndex];
if (itemIndex < items.size()) {
stream << items[itemIndex].itemId;
}
}
return stream.str();
}
std::string DescribeHitTarget(
const UIEditorTreeViewHitTarget& hitTarget,
const std::vector<UIEditorTreeViewItem>& items) {
if (hitTarget.itemIndex >= items.size()) {
return "(none)";
}
const std::string& itemId = items[hitTarget.itemIndex].itemId;
switch (hitTarget.kind) {
case UIEditorTreeViewHitTargetKind::Disclosure:
return "disclosure: " + itemId;
case UIEditorTreeViewHitTargetKind::Row:
return "row: " + itemId;
case UIEditorTreeViewHitTargetKind::None:
default:
return "(none)";
}
}
UIInputEvent MakePointerEvent(
UIInputEventType type,
const UIPoint& position,
const UIInputModifiers& modifiers,
UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = position;
event.modifiers = modifiers;
event.pointerButton = button;
return event;
}
UIInputEvent MakeKeyEvent(std::int32_t keyCode, const UIInputModifiers& modifiers) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = keyCode;
event.modifiers = modifiers;
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)),
wParam);
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)),
wParam);
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)),
wParam);
return 0;
}
break;
case WM_RBUTTONDOWN:
if (app != nullptr) {
app->HandleRightButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)),
wParam);
return 0;
}
break;
case WM_RBUTTONUP:
if (app != nullptr) {
app->HandleRightButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)),
wParam);
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "蟾イ隸キ豎よ穐蝗セ<EFBFBD>瑚セ灘<EFBFBD>蛻?captures/latest.png";
app->m_resultFlags =
"selectionChanged=false, expansionChanged=false, keyboardNavigated=false, secondaryClicked=false, consumed=true";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
const std::int32_t keyCode = MapTreeNavigationKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->HandleNavigationKey(keyCode, QueryKeyboardModifiers());
return 0;
}
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1480,
920,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/tree_view_multiselect/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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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);
}
void ResetScenario() {
m_items = BuildTreeItems();
m_selectionModel = {};
m_selectionModel.SetSelections({ "camera", "lights", "ui-root" }, "lights");
m_expansionModel = {};
m_expansionModel.Expand("scene");
m_expansionModel.Expand("lights");
m_expansionModel.Expand("characters");
m_expansionModel.Expand("hero");
m_expansionModel.Expand("ui-root");
m_expansionModel.Expand("canvas");
m_interactionState = {};
m_interactionState.treeViewState.focused = true;
m_interactionState.selectionAnchorId = "lights";
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "蟾イ驥咲スョ蛻ー鮟倩ョ、螟夐€臥憾諤<EFBFBD>シ喞amera | lights | ui-root";
m_resultFlags =
"selectionChanged=false, expansionChanged=false, keyboardNavigated=false, secondaryClicked=false, consumed=false";
RefreshTreeFrame();
}
void RefreshTreeFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
m_treeFrame = UpdateUIEditorTreeViewInteraction(
m_interactionState,
m_selectionModel,
m_expansionModel,
layout.treeRect,
m_items,
{});
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshTreeFrame();
}
void HandleMouseMove(float x, float y, WPARAM wParam) {
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);
PumpTreeEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition, QueryPointerModifiers(wParam)) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpTreeEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition, {}) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y, WPARAM wParam) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
PumpTreeEvents({
MakePointerEvent(
UIInputEventType::PointerButtonDown,
m_mousePosition,
QueryPointerModifiers(wParam),
UIPointerButton::Left)
});
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y, WPARAM wParam) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIInputModifiers modifiers = QueryPointerModifiers(wParam);
const bool insideTree = ContainsPoint(layout.treeRect, x, y);
const UIEditorTreeViewInteractionResult result =
PumpTreeEvents({
MakePointerEvent(
UIInputEventType::PointerButtonUp,
m_mousePosition,
modifiers,
UIPointerButton::Left)
});
std::string actionLabel = "轤ケ蜃サ譬大、也ゥコ逋ス<EFBFBD>掲ocus 貂<>";
if (result.hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure) {
actionLabel = "轤ケ蜃サ disclosure 蛻<>困螻募シ€<EFBDBC>御ク肴<EFBDB8>?selection";
} else if (result.hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) {
if (modifiers.shift) {
actionLabel = "Shift+蜊募<E89C8A><EFBFBD>峩螟夐€?;
} else if (modifiers.control) {
actionLabel = "Ctrl+蜊募<E89C8A><EFBFBD>困螟夐€?;
} else {
actionLabel = "蜊募<EFBFBD>蜊暮€?;
}
} else if (insideTree) {
actionLabel = "轤ケ蜃サ譬大<EFBFBD>遨コ逋ス<EFBFBD>悟宵譖エ譁ー focus / hover";
}
UpdateResultSummary(result, actionLabel);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleRightButtonDown(float x, float y, WPARAM wParam) {
m_mousePosition = UIPoint(x, y);
PumpTreeEvents({
MakePointerEvent(
UIInputEventType::PointerButtonDown,
m_mousePosition,
QueryPointerModifiers(wParam),
UIPointerButton::Right)
});
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleRightButtonUp(float x, float y, WPARAM wParam) {
m_mousePosition = UIPoint(x, y);
const UIEditorTreeViewHitTarget hitBefore = HitTestUIEditorTreeView(m_treeFrame.layout, m_mousePosition);
const std::size_t selectedCountBefore = m_selectionModel.GetSelectionCount();
bool targetAlreadySelected = false;
if (hitBefore.itemIndex < m_items.size()) {
targetAlreadySelected = m_selectionModel.IsSelected(m_items[hitBefore.itemIndex].itemId);
}
const UIEditorTreeViewInteractionResult result =
PumpTreeEvents({
MakePointerEvent(
UIInputEventType::PointerButtonUp,
m_mousePosition,
QueryPointerModifiers(wParam),
UIPointerButton::Right)
});
std::string actionLabel = "蜿ウ髞ョ遨コ逋ス蛹コ蝓<EFBFBD>";
if (result.hitTarget.kind == UIEditorTreeViewHitTargetKind::Row ||
result.hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure) {
if (targetAlreadySelected && selectedCountBefore > 1u) {
actionLabel = "蜿ウ髞ョ蜻ス荳ュ蟾イ騾蛾寔蜷茨シ御ク肴遠謨?selection<6F>御サ<E5BEA1><EFBDBB>謐「 primary";
} else if (targetAlreadySelected) {
actionLabel = "蜿ウ髞ョ蜻ス荳ュ蟾イ騾蛾。ケ<EFBFBD>御サ<EFBFBD><EFBFBD>謐「 primary";
} else {
actionLabel = "蜿ウ髞ョ蜻ス荳ュ譛ェ騾蛾。ケ<EFBFBD>梧隼荳コ蜊暮€牙ケカ隗ヲ蜿<EFBFBD> secondary click";
}
}
UpdateResultSummary(result, actionLabel);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleNavigationKey(std::int32_t keyCode, const UIInputModifiers& modifiers) {
const UIEditorTreeViewInteractionResult result =
PumpTreeEvents({ MakeKeyEvent(keyCode, modifiers) });
std::string actionLabel = modifiers.shift
? "Shift+髞ョ逶倩激蝗エ謇ゥ騾?
: "髞ョ逶伜ッシ闊ェ";
if (keyCode == static_cast<std::int32_t>(KeyCode::Left) ||
keyCode == static_cast<std::int32_t>(KeyCode::Right)) {
actionLabel = "Left/Right 螻らコァ蟇シ闊ェ謌門ア募シ€<C280>";
}
UpdateResultSummary(result, actionLabel);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorTreeViewInteractionResult PumpTreeEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
m_treeFrame = UpdateUIEditorTreeViewInteraction(
m_interactionState,
m_selectionModel,
m_expansionModel,
layout.treeRect,
m_items,
std::move(events));
return m_treeFrame.result;
}
void UpdateResultSummary(
const UIEditorTreeViewInteractionResult& result,
std::string_view actionLabel) {
std::ostringstream summary = {};
summary << actionLabel;
if (!result.selectedItemId.empty()) {
summary << " -> selected " << result.selectedItemId;
}
if (!result.toggledItemId.empty()) {
summary << " -> toggled " << result.toggledItemId;
}
if (result.selectedVisibleIndex != UIEditorTreeViewInvalidIndex) {
summary << " (visible " << result.selectedVisibleIndex << ")";
}
m_lastResult = summary.str();
std::ostringstream flags = {};
flags << "selectionChanged=" << BoolText(result.selectionChanged)
<< ", expansionChanged=" << BoolText(result.expansionChanged)
<< ", keyboardNavigated=" << BoolText(result.keyboardNavigated)
<< ", secondaryClicked=" << BoolText(result.secondaryClicked)
<< ", consumed=" << BoolText(result.consumed);
m_resultFlags = flags.str();
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::Reset:
ResetScenario();
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "蟾イ隸キ豎よ穐蝗セ<EFBFBD>瑚セ灘<EFBFBD>蛻?captures/latest.png";
m_resultFlags =
"selectionChanged=false, expansionChanged=false, keyboardNavigated=false, secondaryClicked=false, consumed=true";
break;
}
}
void RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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 ScenarioLayout layout = BuildScenarioLayout(width, height);
RefreshTreeFrame();
const UIEditorTreeViewHitTarget currentHit = HitTestUIEditorTreeView(m_treeFrame.layout, m_mousePosition);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorTreeViewMultiSelect");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(
drawList,
layout.introRect,
"霑吩クェ豬玖ッ暮ェ瑚ッ∽サ€荵亥粥閭ス<EFBFBD><EFBFBD>",
"蜿ェ鬪瑚ッ?Editor TreeView 逧<>、夐€牙・醍コヲ<EFBDBA>御ク肴キキ蜈?Hierarchy / Inspector 荳壼苅髱「譚ソ縲?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 蜊募<E89C8A> row<6F>壼コ泌<EFBDBA>蝗槫黒騾会シ継rimary / anchor / current 蜷梧ュ・蛻ー隸・陦後€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. Ctrl+蜊募<E89C8A> / Shift+蜊募<E89C8A><E58B9F>壼宵鬪瑚ッ<E7919A> visible tree 闌<>峩蜀<E5B3A9>噪螟夐€<C280>謐「荳手ソ樒サュ謇ゥ騾峨€?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 轤ケ蜃サ disclosure<72>壼宵蛻<E5AEB5>困 expanded<65>御ク榊コ疲裏謨<E8A38F>遠謨?selection縲?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. Left / Right<68>壼コ秘ェ瑚ッ∝ア募シ€縲∵釜蜿<E9879C><E89CBF>御サ・蜿顔宛蟄仙アらコァ荵矩龍逧?current / selection 霍ウ霓ャ縲?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 蜿ウ髞ョ蜻ス荳ュ蟾イ騾蛾寔蜷井クュ逧<EFBDAD>€鬘ケ<E9AC98>壻ク榊セ玲遠謨」 selection<6F>悟宵蜈∬ョク蛻<EFBDB8>困 primary縲?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. 驥咲せ譽€譟・蟾ヲ萓ァ迥カ諤<EFBDB6>シ啀rimary / Count / Ids / Anchor / Current / Expanded / Visible / Result縲?,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 204.0f),
"7. 謖?F12 謇句勘謌ェ蝗セ<E89D97>幄ョセ鄂?XCUI_AUTO_CAPTURE_ON_STARTUP=1 蜿ッ蛛壼星蜉ィ閾ェ蜉ィ謌ェ蝗セ縲?,
kTextPrimary,
12.0f);
DrawCard(drawList, layout.controlRect, "謫堺ス<EFBFBD>");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(drawList, button, m_hasHoveredAction && m_hoveredAction == button.action);
}
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),
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + BoolText(m_interactionState.treeViewState.focused),
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
"Primary: " +
(m_selectionModel.HasSelection() ? m_selectionModel.GetSelectedId() : std::string("(none)")),
kTextSuccess,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
"Selected Count: " + std::to_string(m_selectionModel.GetSelectionCount()),
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Selected Ids: " + JoinSelectedIds(m_selectionModel),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Anchor: " +
(m_interactionState.selectionAnchorId.empty()
? std::string("(none)")
: m_interactionState.selectionAnchorId),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
std::string("Current: ") +
(m_interactionState.keyboardNavigation.HasCurrentIndex()
? std::to_string(m_interactionState.keyboardNavigation.GetCurrentIndex())
: std::string("(none)")),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
"Expanded: " + JoinExpandedIds(m_items, m_expansionModel),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f),
"Visible: " + JoinVisibleIds(m_items, m_treeFrame.layout),
kTextMuted,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 286.0f),
"Result: " + m_lastResult,
kTextPrimary,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 310.0f),
"Flags: " + m_resultFlags,
kTextWeak,
12.0f);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "謌ェ蝗セ謗帝弌荳?.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/tree_view_multiselect/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 334.0f),
captureSummary,
kTextWeak,
12.0f);
DrawCard(drawList, layout.previewRect, "TreeView 螟夐€蛾「<E89BBE>ァ?, "€?TreeView<EFBFBD>€€?);
AppendUIEditorTreeViewBackground(
drawList,
m_treeFrame.layout,
m_items,
m_selectionModel,
m_interactionState.treeViewState);
AppendUIEditorTreeViewForeground(drawList, m_treeFrame.layout, m_items);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
std::vector<UIEditorTreeViewItem> m_items = {};
UISelectionModel m_selectionModel = {};
UIExpansionModel m_expansionModel = {};
UIEditorTreeViewInteractionState m_interactionState = {};
UIEditorTreeViewInteractionFrame m_treeFrame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
std::string m_resultFlags = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_vector2_field_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_vector2_field_basic_validation
OUTPUT_NAME "XCUIEditorVector2FieldBasicValidation"
)

View File

@@ -0,0 +1,873 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorVector2FieldInteraction.h>
#include <XCEditor/Fields/UIEditorVector2Field.h>
#include "EditorValidationTheme.h"
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/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::UIEditorVector2FieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorVector2FieldInteractionResult;
using XCEngine::UI::Editor::UIEditorVector2FieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorVector2FieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorVector2Field;
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector2FieldComponentValue;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector2Field;
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldInvalidComponentIndex;
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector2FieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector2Field 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 inspectorRect = {};
UIRect inspectorHeaderRect = {};
UIRect sectionRect = {};
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 MapVector2FieldKey(UINT keyCode) {
switch (keyCode) {
case VK_LEFT:
return static_cast<std::int32_t>(KeyCode::Left);
case VK_RIGHT:
return static_cast<std::int32_t>(KeyCode::Right);
case VK_UP:
return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN:
return static_cast<std::int32_t>(KeyCode::Down);
case VK_HOME:
return static_cast<std::int32_t>(KeyCode::Home);
case VK_END:
return static_cast<std::int32_t>(KeyCode::End);
case VK_TAB:
return static_cast<std::int32_t>(KeyCode::Tab);
case VK_RETURN:
return static_cast<std::int32_t>(KeyCode::Enter);
case VK_ESCAPE:
return static_cast<std::int32_t>(KeyCode::Escape);
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, 272.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)(240.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.inspectorRect = UIRect(
layout.previewRect.x + 18.0f,
layout.previewRect.y + 54.0f,
(std::min)(392.0f, layout.previewRect.width - 36.0f),
172.0f);
layout.inspectorHeaderRect = UIRect(
layout.inspectorRect.x,
layout.inspectorRect.y,
layout.inspectorRect.width,
24.0f);
layout.sectionRect = UIRect(
layout.inspectorRect.x,
layout.inspectorRect.y + layout.inspectorHeaderRect.height,
layout.inspectorRect.width,
24.0f);
layout.fieldRect = UIRect(
layout.inspectorRect.x,
layout.sectionRect.y + layout.sectionRect.height + 2.0f,
layout.inspectorRect.width,
22.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "é‡<EFBFBD>ç½®", 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 UIEditorVector2FieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorVector2FieldHitTargetKind::Component:
return std::string("component_") + std::to_string(hitTarget.componentIndex);
case UIEditorVector2FieldHitTargetKind::Row:
return "row";
case UIEditorVector2FieldHitTargetKind::None:
default:
return "none";
}
}
std::string DescribeSelectedComponent(std::size_t componentIndex) {
if (componentIndex == UIEditorVector2FieldInvalidComponentIndex) {
return "none";
}
return componentIndex == 0u ? "X" : "Y";
}
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, bool shift = false) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = keyCode;
event.modifiers.shift = shift;
return event;
}
UIInputEvent MakeCharacterEvent(wchar_t character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = static_cast<std::uint32_t>(character);
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "已请求截图,输出�captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
if (wParam == VK_F6) {
app->HandleFocusLost();
return 0;
}
const std::int32_t keyCode = MapVector2FieldKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->HandleKeyDown(keyCode, (GetKeyState(VK_SHIFT) & 0x8000) != 0);
return 0;
}
}
break;
case WM_CHAR:
if (app != nullptr) {
app->HandleCharacter(static_cast<wchar_t>(wParam));
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1520,
920,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/vector2_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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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::GetEditorValidationShellMetrics());
}
void ResetScenario() {
m_spec = {};
m_spec.fieldId = "position";
m_spec.label = "Position";
m_spec.values = { 1.25, -2.5 };
m_spec.componentLabels = { std::string("X"), std::string("Y") };
m_spec.step = 0.25;
m_spec.minValue = -10.0;
m_spec.maxValue = 10.0;
m_spec.integerMode = false;
m_spec.readOnly = false;
m_interactionState = {};
m_interactionState.vector2FieldState.focused = true;
m_interactionState.vector2FieldState.selectedComponentIndex = 0u;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "å·²é‡<EFBFBD>置到默认 Vector2Field 状æ€?;
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics();
m_frame = UpdateUIEditorVector2FieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
{},
metrics);
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshFrame();
}
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);
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorVector2FieldInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorVector2FieldInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleKeyDown(std::int32_t keyCode, bool shift) {
const UIEditorVector2FieldInteractionResult result =
PumpEvents({ MakeKeyEvent(keyCode, shift) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleCharacter(wchar_t character) {
if (character < 32) {
return;
}
const UIEditorVector2FieldInteractionResult result =
PumpEvents({ MakeCharacterEvent(character) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleFocusLost() {
const UIEditorVector2FieldInteractionResult result =
PumpEvents({ MakeFocusEvent(UIInputEventType::FocusLost) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorVector2FieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics();
m_frame = UpdateUIEditorVector2FieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
std::move(events),
metrics);
return m_frame.result;
}
void UpdateResultText(const UIEditorVector2FieldInteractionResult& result) {
if (result.editCommitRejected) {
m_lastResult = "æ<EFBFBD><EFBFBD>交被æç»<EFBFBD>:当å‰<EFBFBD>输入ä¸<EFBFBD>是å<EFBFBD>ˆæ³•æ•°å­";
return;
}
if (result.editCommitted) {
m_lastResult =
std::string("å·²æ<EFBFBD><EFBFBD>交ç¼è¾? ") +
DescribeSelectedComponent(result.changedComponentIndex) +
" = " + result.committedText;
return;
}
if (result.editCanceled) {
m_lastResult = "å·²å<EFBFBD>消ç¼è¾?;
return;
}
if (result.editStarted) {
m_lastResult =
std::string("开始编�component ") +
DescribeSelectedComponent(result.selectedComponentIndex);
return;
}
if (result.stepApplied || result.valueChanged) {
m_lastResult =
std::string("值已更新,当�component = ") +
DescribeSelectedComponent(result.changedComponentIndex);
return;
}
if (result.selectionChanged) {
m_lastResult =
std::string("已切æ<EFBFBD>¢é€‰ä¸­ component: ") +
DescribeSelectedComponent(result.selectedComponentIndex);
return;
}
if (result.focusChanged) {
m_lastResult =
std::string("焦点状� ") +
(m_interactionState.vector2FieldState.focused ? "focused" : "lost");
return;
}
if (result.consumed) {
m_lastResult = "输入已处ç<EFBFBD>†ï¼Œä½†æ²¡æœ‰é¢<EFBFBD>å¤çжæ€<EFBFBD>å<EFBFBD>˜åŒ?;
return;
}
m_lastResult = "æ— å<EFBFBD>˜åŒ?;
}
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 RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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::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();
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector2FieldPalette();
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector2FieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个æµè¯•在验è¯<EFBFBD>什么功能?",
"验è¯<EFBFBD> UIEditorVector2Field 的选æ©åˆ‡æ<E280A1>¢ã€<C3A3>é”®ç˜æ­¥è¿ã€<C3A3>ç¼è¾æ<E28098><C3A6>äº?å<>消,以å<C2A5>Šåºå®?Inspector 风格承载ã€?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 X / Y value box å<>¯åˆ‡æ<E280A1>?selected component,并检æŸ?hover 与高亮ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 获得 focus å<>Žï¼ŒæŒ?Tab åœ?X / Y ä¹é—´åˆ‡æ<E280A1>¢ selected componentã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. ä¸<C3A4>è¿å…¥ç¼è¾æ—¶ï¼ŒUp / Down / Home / End 会对当å‰<C3A5> component å<>?step 或边界跳转ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. æŒ?Enter å¼€å§ç¼è¾ï¼Œè¾“å…¥å<C2A5>Žå†<C3A5>æŒ?Enter æ<><C3A6>交,按 Escape å<>消ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. æŒ?F6 模拟 FocusLost,检查未æ<C2AA><C3A6>交ç¼è¾æ˜¯å<C2AF>¦æŒ‰çº¦å®šç»“æ<E2809C>Ÿã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. 观察 Hover / Selected / Editing / Values / Result 是å<C2AF>¦å<C2A6>Œæ­¥ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 204.0f),
"7. �F12 或点击截图按钮,确认自动截图路径正确�,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "æ“<EFBFBD>作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状æ€<EFBFBD>æ˜è¦?,
"é‡<EFBFBD>ç¹æ£€æŸ?hit / selected / editing / values / display / resultã€?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
"Selected: " + DescribeSelectedComponent(m_interactionState.vector2FieldState.selectedComponentIndex),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Focused: ") + (m_interactionState.vector2FieldState.focused ? "æ˜? : "å<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
std::string("Editing: ") + (m_interactionState.vector2FieldState.editing ? "æ˜? : "å<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Values: X=" + FormatUIEditorVector2FieldComponentValue(m_spec, 0u) +
" Y=" + FormatUIEditorVector2FieldComponentValue(m_spec, 1u),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Display: X=" + m_interactionState.vector2FieldState.displayTexts[0] +
" Y=" + m_interactionState.vector2FieldState.displayTexts[1],
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队�.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/vector2_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"Vector2Field 预览",
"这里å<EFBFBD>ªæ”¾ä¸€ä¸ªåºå®šæ ·å¼<EFBFBD>çš„ Vector2 输入项ã€?);
drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor);
drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f);
drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground);
drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f);
drawList.AddText(
UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f),
"Inspector",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor);
drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f);
drawList.AddText(
UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f),
"v Transform",
propertyPalette.sectionTextColor,
shellMetrics.bodyFontSize);
AppendUIEditorVector2Field(
drawList,
layout.fieldRect,
m_spec,
m_interactionState.vector2FieldState,
vectorPalette,
vectorMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorVector2FieldSpec m_spec = {};
UIEditorVector2FieldInteractionState m_interactionState = {};
UIEditorVector2FieldInteractionFrame m_frame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = "æ— å<EFBFBD>˜åŒ?;
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
ScenarioApp app = {};
return app.Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_vector3_field_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_vector3_field_basic_validation
OUTPUT_NAME "XCUIEditorVector3FieldBasicValidation"
)

View File

@@ -0,0 +1,881 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorVector3FieldInteraction.h>
#include <XCEditor/Fields/UIEditorVector3Field.h>
#include "EditorValidationTheme.h"
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/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::UIEditorVector3FieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorVector3FieldInteractionResult;
using XCEngine::UI::Editor::UIEditorVector3FieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorVector3FieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorVector3Field;
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector3FieldComponentValue;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector3Field;
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldInvalidComponentIndex;
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector3FieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector3Field 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 inspectorRect = {};
UIRect inspectorHeaderRect = {};
UIRect sectionRect = {};
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 MapVector3FieldKey(UINT keyCode) {
switch (keyCode) {
case VK_LEFT:
return static_cast<std::int32_t>(KeyCode::Left);
case VK_RIGHT:
return static_cast<std::int32_t>(KeyCode::Right);
case VK_UP:
return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN:
return static_cast<std::int32_t>(KeyCode::Down);
case VK_HOME:
return static_cast<std::int32_t>(KeyCode::Home);
case VK_END:
return static_cast<std::int32_t>(KeyCode::End);
case VK_TAB:
return static_cast<std::int32_t>(KeyCode::Tab);
case VK_RETURN:
return static_cast<std::int32_t>(KeyCode::Enter);
case VK_ESCAPE:
return static_cast<std::int32_t>(KeyCode::Escape);
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, 272.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)(240.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.inspectorRect = UIRect(
layout.previewRect.x + 18.0f,
layout.previewRect.y + 54.0f,
(std::min)(392.0f, layout.previewRect.width - 36.0f),
172.0f);
layout.inspectorHeaderRect = UIRect(
layout.inspectorRect.x,
layout.inspectorRect.y,
layout.inspectorRect.width,
24.0f);
layout.sectionRect = UIRect(
layout.inspectorRect.x,
layout.inspectorRect.y + layout.inspectorHeaderRect.height,
layout.inspectorRect.width,
24.0f);
layout.fieldRect = UIRect(
layout.inspectorRect.x,
layout.sectionRect.y + layout.sectionRect.height + 2.0f,
layout.inspectorRect.width,
22.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "é‡<EFBFBD>ç½®", 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 UIEditorVector3FieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorVector3FieldHitTargetKind::Component:
return std::string("component_") + std::to_string(hitTarget.componentIndex);
case UIEditorVector3FieldHitTargetKind::Row:
return "row";
case UIEditorVector3FieldHitTargetKind::None:
default:
return "none";
}
}
std::string DescribeSelectedComponent(std::size_t componentIndex) {
switch (componentIndex) {
case 0u:
return "X";
case 1u:
return "Y";
case 2u:
return "Z";
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, bool shift = false) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = keyCode;
event.modifiers.shift = shift;
return event;
}
UIInputEvent MakeCharacterEvent(wchar_t character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = static_cast<std::uint32_t>(character);
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "已请求截图,输出�captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
if (wParam == VK_F6) {
app->HandleFocusLost();
return 0;
}
const std::int32_t keyCode = MapVector3FieldKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->HandleKeyDown(keyCode, (GetKeyState(VK_SHIFT) & 0x8000) != 0);
return 0;
}
}
break;
case WM_CHAR:
if (app != nullptr) {
app->HandleCharacter(static_cast<wchar_t>(wParam));
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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,
1520,
920,
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_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/vector3_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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
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::GetEditorValidationShellMetrics());
}
void ResetScenario() {
m_spec = {};
m_spec.fieldId = "position";
m_spec.label = "Position";
m_spec.values = { 1.25, -2.5, 4.75 };
m_spec.componentLabels = { std::string("X"), std::string("Y"), std::string("Z") };
m_spec.step = 0.25;
m_spec.minValue = -10.0;
m_spec.maxValue = 10.0;
m_spec.integerMode = false;
m_spec.readOnly = false;
m_interactionState = {};
m_interactionState.vector3FieldState.focused = true;
m_interactionState.vector3FieldState.selectedComponentIndex = 0u;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "å·²é‡<EFBFBD>置到默认 Vector3Field 状æ€?;
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics();
m_frame = UpdateUIEditorVector3FieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
{},
metrics);
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshFrame();
}
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);
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorVector3FieldInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
const UIEditorVector3FieldInteractionResult result =
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleKeyDown(std::int32_t keyCode, bool shift) {
const UIEditorVector3FieldInteractionResult result =
PumpEvents({ MakeKeyEvent(keyCode, shift) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleCharacter(wchar_t character) {
if (character < 32) {
return;
}
const UIEditorVector3FieldInteractionResult result =
PumpEvents({ MakeCharacterEvent(character) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleFocusLost() {
const UIEditorVector3FieldInteractionResult result =
PumpEvents({ MakeFocusEvent(UIInputEventType::FocusLost) });
UpdateResultText(result);
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
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;
}
UIEditorVector3FieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics();
m_frame = UpdateUIEditorVector3FieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
std::move(events),
metrics);
return m_frame.result;
}
void UpdateResultText(const UIEditorVector3FieldInteractionResult& result) {
if (result.editCommitRejected) {
m_lastResult = "æ<EFBFBD><EFBFBD>交被æç»<EFBFBD>:当å‰<EFBFBD>输入ä¸<EFBFBD>是å<EFBFBD>ˆæ³•æ•°å­";
return;
}
if (result.editCommitted) {
m_lastResult =
std::string("å·²æ<EFBFBD><EFBFBD>交ç¼è¾? ") +
DescribeSelectedComponent(result.changedComponentIndex) +
" = " + result.committedText;
return;
}
if (result.editCanceled) {
m_lastResult = "å·²å<EFBFBD>消ç¼è¾?;
return;
}
if (result.editStarted) {
m_lastResult =
std::string("开始编�component ") +
DescribeSelectedComponent(result.selectedComponentIndex);
return;
}
if (result.stepApplied || result.valueChanged) {
m_lastResult =
std::string("值已更新,当�component = ") +
DescribeSelectedComponent(result.changedComponentIndex);
return;
}
if (result.selectionChanged) {
m_lastResult =
std::string("已切æ<EFBFBD>¢é€‰ä¸­ component: ") +
DescribeSelectedComponent(result.selectedComponentIndex);
return;
}
if (result.focusChanged) {
m_lastResult =
std::string("焦点状� ") +
(m_interactionState.vector3FieldState.focused ? "focused" : "lost");
return;
}
if (result.consumed) {
m_lastResult = "输入已处ç<EFBFBD>†ï¼Œä½†æ²¡æœ‰é¢<EFBFBD>å¤çжæ€<EFBFBD>å<EFBFBD>˜åŒ?;
return;
}
m_lastResult = "æ— å<EFBFBD>˜åŒ?;
}
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 RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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::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();
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector3FieldPalette();
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector3FieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个æµè¯•在验è¯<EFBFBD>什么功能?",
"验è¯<EFBFBD> UIEditorVector3Field 的选æ©åˆ‡æ<E280A1>¢ã€<C3A3>é”®ç˜æ­¥è¿ã€<C3A3>ç¼è¾æ<E28098><C3A6>äº?å<>消,以å<C2A5>Šåºå®?Inspector 风格承载ã€?);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 X / Y / Z value box å<>¯åˆ‡æ<E280A1>?selected component,并检æŸ?hover 与高亮ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 获得 focus å<>Žï¼ŒæŒ?Tab åœ?X / Y / Z ä¹é—´åˆ‡æ<E280A1>¢ selected componentã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. ä¸<C3A4>è¿å…¥ç¼è¾æ—¶ï¼ŒUp / Down / Home / End 会对当å‰<C3A5> component å<>?step 或边界跳转ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. æŒ?Enter å¼€å§ç¼è¾ï¼Œè¾“å…¥å<C2A5>Žå†<C3A5>æŒ?Enter æ<><C3A6>交,按 Escape å<>消ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. æŒ?F6 模拟 FocusLost,检查未æ<C2AA><C3A6>交ç¼è¾æ˜¯å<C2AF>¦æŒ‰çº¦å®šç»“æ<E2809C>Ÿã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. 观察 Hover / Selected / Editing / Values / Result 是å<C2AF>¦å<C2A6>Œæ­¥ã€?,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 204.0f),
"7. �F12 或点击截图按钮,确认自动截图路径正确�,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "æ“<EFBFBD>作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状æ€<EFBFBD>æ˜è¦?,
"é‡<EFBFBD>ç¹æ£€æŸ?hit / selected / editing / values / display / resultã€?);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
"Selected: " + DescribeSelectedComponent(m_interactionState.vector3FieldState.selectedComponentIndex),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Focused: ") + (m_interactionState.vector3FieldState.focused ? "æ˜? : "å<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
std::string("Editing: ") + (m_interactionState.vector3FieldState.editing ? "æ˜? : "å<EFBFBD>?),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Values: X=" + FormatUIEditorVector3FieldComponentValue(m_spec, 0u) +
" Y=" + FormatUIEditorVector3FieldComponentValue(m_spec, 1u) +
" Z=" + FormatUIEditorVector3FieldComponentValue(m_spec, 2u),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Display: X=" + m_interactionState.vector3FieldState.displayTexts[0] +
" Y=" + m_interactionState.vector3FieldState.displayTexts[1] +
" Z=" + m_interactionState.vector3FieldState.displayTexts[2],
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队�.."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/manual_validation/shell/vector3_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"Vector3Field 预览",
"这里å<EFBFBD>ªæ”¾ä¸€ä¸ªåºå®šæ ·å¼<EFBFBD>çš„ Vector3 输入项ã€?);
drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor);
drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f);
drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground);
drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f);
drawList.AddText(
UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f),
"Inspector",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor);
drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f);
drawList.AddText(
UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f),
"v Transform",
propertyPalette.sectionTextColor,
shellMetrics.bodyFontSize);
AppendUIEditorVector3Field(
drawList,
layout.fieldRect,
m_spec,
m_interactionState.vector3FieldState,
vectorPalette,
vectorMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorVector3FieldSpec m_spec = {};
UIEditorVector3FieldInteractionState m_interactionState = {};
UIEditorVector3FieldInteractionFrame m_frame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = "æ— å<EFBFBD>˜åŒ?;
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
ScenarioApp app = {};
return app.Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_vector4_field_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_vector4_field_basic_validation
OUTPUT_NAME "XCUIEditorVector4FieldBasicValidation"
)

View File

@@ -0,0 +1,443 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Foundation/UIEditorTheme.h>
#include <XCEditor/Fields/UIEditorVector4FieldInteraction.h>
#include <XCEditor/Fields/UIEditorVector4Field.h>
#include "EditorValidationTheme.h"
#include "Rendering/Native/AutoScreenshot.h"
#include "Rendering/Native/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 <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::UIEditorVector4FieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorVector4FieldInteractionResult;
using XCEngine::UI::Editor::UIEditorVector4FieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorVector4FieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorVector4Field;
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector4FieldComponentValue;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector4Field;
using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldSpec;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector4FieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector4Field Basic";
struct ScenarioLayout {
UIRect introRect = {};
UIRect stateRect = {};
UIRect previewRect = {};
UIRect inspectorRect = {};
UIRect inspectorHeaderRect = {};
UIRect sectionRect = {};
UIRect fieldRect = {};
};
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();
}
std::int32_t MapVectorKey(UINT keyCode) {
switch (keyCode) {
case VK_LEFT: return static_cast<std::int32_t>(KeyCode::Left);
case VK_RIGHT: return static_cast<std::int32_t>(KeyCode::Right);
case VK_UP: return static_cast<std::int32_t>(KeyCode::Up);
case VK_DOWN: return static_cast<std::int32_t>(KeyCode::Down);
case VK_HOME: return static_cast<std::int32_t>(KeyCode::Home);
case VK_END: return static_cast<std::int32_t>(KeyCode::End);
case VK_TAB: return static_cast<std::int32_t>(KeyCode::Tab);
case VK_RETURN: return static_cast<std::int32_t>(KeyCode::Enter);
case VK_ESCAPE: return static_cast<std::int32_t>(KeyCode::Escape);
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, 230.0f);
layout.stateRect = UIRect(
margin,
layout.introRect.y + layout.introRect.height + gap,
leftWidth,
(std::max)(280.0f, height - (layout.introRect.y + layout.introRect.height + gap) - margin));
layout.previewRect = UIRect(
leftWidth + margin * 2.0f,
margin,
(std::max)(480.0f, width - leftWidth - margin * 3.0f),
height - margin * 2.0f);
layout.inspectorRect = UIRect(layout.previewRect.x + 18.0f, layout.previewRect.y + 54.0f, 460.0f, 174.0f);
layout.inspectorHeaderRect = UIRect(layout.inspectorRect.x, layout.inspectorRect.y, layout.inspectorRect.width, 24.0f);
layout.sectionRect = UIRect(layout.inspectorRect.x, layout.inspectorRect.y + 24.0f, layout.inspectorRect.width, 24.0f);
layout.fieldRect = UIRect(layout.inspectorRect.x, layout.sectionRect.y + 26.0f, layout.inspectorRect.width, 22.0f);
return layout;
}
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, bool shift = false) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = keyCode;
event.modifiers.shift = shift;
return event;
}
UIInputEvent MakeCharacterEvent(wchar_t character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = static_cast<std::uint32_t>(character);
return event;
}
std::string DescribeHitTarget(const UIEditorVector4FieldHitTarget& hitTarget) {
if (hitTarget.kind == UIEditorVector4FieldHitTargetKind::Component) {
return std::string("component_") + std::to_string(hitTarget.componentIndex);
}
if (hitTarget.kind == UIEditorVector4FieldHitTargetKind::Row) {
return "row";
}
return "none";
}
std::string DescribeSelectedComponent(std::size_t componentIndex) {
switch (componentIndex) {
case 0u: return "X";
case 1u: return "Y";
case 2u: return "Z";
case 3u: return "W";
default: return "none";
}
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
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)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->m_mousePosition = UIPoint(static_cast<float>(GET_X_LPARAM(lParam)), static_cast<float>(GET_Y_LPARAM(lParam)));
TRACKMOUSEEVENT trackEvent = { sizeof(trackEvent), TME_LEAVE, hwnd, 0 };
TrackMouseEvent(&trackEvent);
app->PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, app->m_mousePosition) });
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->m_mousePosition = UIPoint(-1000.0f, -1000.0f);
app->PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, app->m_mousePosition) });
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
break;
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
if (app != nullptr) {
app->m_mousePosition = UIPoint(static_cast<float>(GET_X_LPARAM(lParam)), static_cast<float>(GET_Y_LPARAM(lParam)));
const UIInputEventType type =
message == WM_LBUTTONDOWN ? UIInputEventType::PointerButtonDown : UIInputEventType::PointerButtonUp;
app->UpdateResultText(app->PumpEvents({ MakePointerEvent(type, app->m_mousePosition, UIPointerButton::Left) }));
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "已请求截图,输出�captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
if (wParam == 'R') {
app->ResetScenario();
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
if (wParam == VK_F6) {
app->UpdateResultText(app->PumpEvents({ MakePointerEvent(UIInputEventType::FocusLost, app->m_mousePosition) }));
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
const std::int32_t keyCode = MapVectorKey(static_cast<UINT>(wParam));
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
app->UpdateResultText(app->PumpEvents({ MakeKeyEvent(keyCode, (GetKeyState(VK_SHIFT) & 0x8000) != 0) }));
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
}
break;
case WM_CHAR:
if (app != nullptr && wParam >= 32) {
app->UpdateResultText(app->PumpEvents({ MakeCharacterEvent(static_cast<wchar_t>(wParam)) }));
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
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, 1560, 920, nullptr, nullptr, hInstance, this);
if (m_hwnd == nullptr || !m_renderer.Initialize(m_hwnd)) {
return false;
}
ShowWindow(m_hwnd, nCmdShow);
UpdateWindow(m_hwnd);
m_captureRoot = ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/vector4_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);
}
if (m_windowClassAtom != 0) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
}
}
void ResetScenario() {
m_spec = {};
m_spec.fieldId = "rotation";
m_spec.label = "Rotation";
m_spec.values = { 1.25, -2.5, 4.75, 0.5 };
m_spec.step = 0.25;
m_spec.minValue = -10.0;
m_spec.maxValue = 10.0;
m_interactionState = {};
m_interactionState.vector4FieldState.focused = true;
m_interactionState.vector4FieldState.selectedComponentIndex = 0u;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_lastResult = "å·²é‡<EFBFBD>置到默认 Vector4Field 状æ€?;
PumpEvents({});
}
UIEditorVector4FieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
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::GetEditorValidationShellMetrics());
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector4FieldMetrics();
m_frame = UpdateUIEditorVector4FieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
std::move(events),
metrics);
return m_frame.result;
}
void UpdateResultText(const UIEditorVector4FieldInteractionResult& result) {
if (result.editCommitRejected) {
m_lastResult = "æ<EFBFBD><EFBFBD>交被æç»<EFBFBD>:当å‰<EFBFBD>输入ä¸<EFBFBD>是å<EFBFBD>ˆæ³•æ•°å­";
} else if (result.editCommitted) {
m_lastResult = "å·²æ<EFBFBD><EFBFBD>交ç¼è¾? " + DescribeSelectedComponent(result.changedComponentIndex) + " = " + result.committedText;
} else if (result.editCanceled) {
m_lastResult = "å·²å<EFBFBD>消ç¼è¾?;
} else if (result.editStarted) {
m_lastResult = "开始编�component " + DescribeSelectedComponent(result.selectedComponentIndex);
} else if (result.stepApplied || result.valueChanged) {
m_lastResult = "值已更新,当�component = " + DescribeSelectedComponent(result.changedComponentIndex);
} else if (result.selectionChanged) {
m_lastResult = "已切æ<EFBFBD>¢é€‰ä¸­ component: " + DescribeSelectedComponent(result.selectedComponentIndex);
} else if (result.focusChanged) {
m_lastResult = std::string("焦点状� ") + (m_interactionState.vector4FieldState.focused ? "focused" : "lost");
} else if (result.consumed) {
m_lastResult = "输入已处ç<EFBFBD>†ï¼Œä½†æ²¡æœ‰é¢<EFBFBD>å¤çжæ€<EFBFBD>å<EFBFBD>˜åŒ?;
}
}
void RenderFrame() {
RECT clientRect = {};
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::GetEditorValidationShellMetrics();
const auto shellPalette = XCEngine::Tests::EditorUI::GetEditorValidationShellPalette();
const auto layout = BuildScenarioLayout(width, height, shellMetrics);
PumpEvents({});
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 = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector4FieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
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), "这个æµè¯•在验è¯<EFBFBD>什么功能?", shellPalette.textPrimary, shellMetrics.titleFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 40.0f), "验è¯<EFBFBD> UIEditorVector4Field çš„å分é‡<C3A9>切æ<E280A1>¢ã€<C3A3>é”®ç˜æ­¥è¿ã€<C3A3>ç¼è¾æ<E28098><C3A6>äº?å<>消,以å<C2A5>Šåºå®?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,å<C592>¯åˆ‡æ<E280A1>¢ selected componentã€?, shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), "2. æŒ?Tab / Shift+Tab 切æ<E280A1>¢ componentï¼Up / Down / Home / End 检æŸ?step 与边界ã€?, shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), "3. æŒ?Enter å¼€å§ç¼è¾ï¼ç´æŽ¥è¾“入字符也应开å§ç¼è¾ï¼Enter æ<><C3A6>交,Escape å<>消ã€?, shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), "4. æŒ?F6 模拟 FocusLost,按 F12 截图,按 R é‡<C3A9>置当å‰<C3A5>æµè¯•ã€?, shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f), "5. é‡<C3A9>ç¹æ£€æŸ?Hover / Selected / Editing / Values / Result 是å<C2AF>¦å<C2A6>Œæ­¥ã€?, shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddFilledRect(layout.stateRect, shellPalette.cardBackground, shellMetrics.cardRadius);
drawList.AddRectOutline(layout.stateRect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 14.0f), "状æ€<EFBFBD>æ˜è¦?, shellPalette.textPrimary, shellMetrics.titleFontSize);
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 46.0f), "Hover: " + DescribeHitTarget(currentHit), shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f), "Selected: " + DescribeSelectedComponent(m_interactionState.vector4FieldState.selectedComponentIndex), shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f), std::string("Focused: ") + (m_interactionState.vector4FieldState.focused ? "æ˜? : "å<EFBFBD>?), shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f), std::string("Editing: ") + (m_interactionState.vector4FieldState.editing ? "æ˜? : "å<EFBFBD>?), shellPalette.textPrimary, shellMetrics.bodyFontSize);
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), "截图: " + (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), "这里å<EFBFBD>ªæ”¾ä¸€ä¸ªåºå®šæ ·å¼<EFBFBD>çš„ 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);
drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f);
drawList.AddText(UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f), "Inspector", shellPalette.textPrimary, shellMetrics.bodyFontSize);
drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor);
drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f);
drawList.AddText(UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f), "v Transform", propertyPalette.sectionTextColor, shellMetrics.bodyFontSize);
AppendUIEditorVector4Field(drawList, layout.fieldRect, m_spec, m_interactionState.vector4FieldState, vectorPalette, vectorMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(m_renderer, drawData, static_cast<unsigned int>(width), static_cast<unsigned int>(height), framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorVector4FieldSpec m_spec = {};
UIEditorVector4FieldInteractionState m_interactionState = {};
UIEditorVector4FieldInteractionFrame m_frame = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
std::string m_lastResult = "æ— å<EFBFBD>˜åŒ?;
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,8 @@
add_executable(editor_ui_viewport_shell_basic_validation WIN32
main.cpp
)
xcengine_configure_editor_ui_integration_validation_target(
editor_ui_viewport_shell_basic_validation
OUTPUT_NAME "XCUIEditorViewportShellBasicValidation"
)

View File

@@ -0,0 +1,827 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Viewport/UIEditorViewportShell.h>
#include <XCEditor/Viewport/UIEditorViewportSlot.h>
#include "Rendering/Native/AutoScreenshot.h"
#include "Platform/Win32/InputModifierTracker.h"
#include "Rendering/Native/NativeRenderer.h"
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <filesystem>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
namespace {
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::UISize;
using XCEngine::UI::Editor::ResolveUIEditorViewportShellRequest;
using XCEngine::UI::Editor::UIEditorViewportShellFrame;
using XCEngine::UI::Editor::UIEditorViewportShellModel;
using XCEngine::UI::Editor::UIEditorViewportShellRequest;
using XCEngine::UI::Editor::UIEditorViewportShellSpec;
using XCEngine::UI::Editor::UIEditorViewportShellState;
using XCEngine::UI::Editor::UpdateUIEditorViewportShell;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::InputModifierTracker;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorViewportSlotForeground;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorViewportSlot;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSegment;
using XCEngine::UI::Editor::Widgets::UIEditorStatusBarSlot;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotFrame;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolItem;
using XCEngine::UI::Editor::Widgets::UIEditorViewportSlotToolSlot;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorViewportShellBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ViewportShell Basic";
constexpr UIColor kWindowBg(0.12f, 0.12f, 0.12f, 1.0f);
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 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.25f, 0.25f, 0.25f, 1.0f);
constexpr UIColor kButtonOnBg(0.39f, 0.39f, 0.39f, 1.0f);
constexpr UIColor kButtonBorder(0.47f, 0.47f, 0.47f, 1.0f);
constexpr UIColor kPreviewBg(0.15f, 0.15f, 0.15f, 1.0f);
enum class ActionId : unsigned char {
ToggleTopBar = 0,
ToggleBottomBar,
ToggleTexture,
Reset,
Capture
};
struct ButtonState {
ActionId action = ActionId::ToggleTopBar;
std::string label = {};
UIRect rect = {};
bool selected = false;
};
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::string BoolText(bool value) {
return value ? "On" : "Off";
}
std::string FormatFloat(float value) {
std::ostringstream stream = {};
stream.setf(std::ios::fixed, std::ios::floatfield);
stream.precision(1);
stream << value;
return stream.str();
}
std::string FormatSize(const UISize& size) {
return FormatFloat(size.width) + " x " + FormatFloat(size.height);
}
std::string FormatRect(const UIRect& rect) {
return "x=" + FormatFloat(rect.x) +
" y=" + FormatFloat(rect.y) +
" w=" + FormatFloat(rect.width) +
" h=" + FormatFloat(rect.height);
}
std::string DescribeHitTarget(const UIEditorViewportSlotHitTarget& hit) {
switch (hit.kind) {
case UIEditorViewportSlotHitTargetKind::TopBar:
return "TopBar";
case UIEditorViewportSlotHitTargetKind::Title:
return "Title";
case UIEditorViewportSlotHitTargetKind::ToolItem:
return "ToolItem[" + std::to_string(hit.index) + "]";
case UIEditorViewportSlotHitTargetKind::Surface:
return "Surface";
case UIEditorViewportSlotHitTargetKind::BottomBar:
return "BottomBar";
case UIEditorViewportSlotHitTargetKind::StatusSegment:
return "StatusSegment[" + std::to_string(hit.index) + "]";
case UIEditorViewportSlotHitTargetKind::StatusSeparator:
return "StatusSeparator[" + std::to_string(hit.index) + "]";
case UIEditorViewportSlotHitTargetKind::None:
default:
return "None";
}
}
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);
}
}
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);
}
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 = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
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)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->QueuePointerEvent(UIInputEventType::PointerMove, UIPointerButton::None, wParam, lParam);
TRACKMOUSEEVENT event = {};
event.cbSize = sizeof(event);
event.dwFlags = TME_LEAVE;
event.hwndTrack = hwnd;
TrackMouseEvent(&event);
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->QueuePointerLeaveEvent();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
SetFocus(hwnd);
app->HandlePointerDown(UIPointerButton::Left, wParam, lParam);
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandlePointerUp(UIPointerButton::Left, wParam, lParam);
return 0;
}
break;
case WM_MOUSEWHEEL:
if (app != nullptr) {
app->QueuePointerWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam), wParam, lParam);
return 0;
}
break;
case WM_SETFOCUS:
if (app != nullptr) {
app->m_inputModifierTracker.SyncFromSystemState();
app->QueueFocusEvent(UIInputEventType::FocusGained);
return 0;
}
break;
case WM_KILLFOCUS:
if (app != nullptr) {
if (GetCapture() == hwnd) {
ReleaseCapture();
}
app->m_inputModifierTracker.Reset();
app->QueueFocusEvent(UIInputEventType::FocusLost);
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr && wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
InvalidateRect(hwnd, nullptr, FALSE);
UpdateWindow(hwnd);
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/manual_validation/shell/viewport_shell_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
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,
1520,
940,
nullptr,
nullptr,
hInstance,
this);
if (m_hwnd == nullptr) {
return false;
}
if (!m_renderer.Initialize(m_hwnd)) {
return false;
}
ShowWindow(m_hwnd, nCmdShow);
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) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
void ResetScenario() {
m_showTopBar = true;
m_showBottomBar = true;
m_textureEnabled = true;
m_inputModifierTracker.Reset();
m_shellState = {};
m_shellRequest = {};
m_shellFrame = {};
m_shellSpec = {};
m_shellModel = {};
m_hoverHit = {};
m_pendingEvents.clear();
m_lastResult = "Ready";
m_hasSnapshot = false;
}
void QueuePointerEvent(UIInputEventType type, UIPointerButton button, WPARAM wParam, LPARAM lParam) {
UIInputEvent event = {};
event.type = type;
event.pointerButton = button;
event.position = UIPoint(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
m_pendingEvents.push_back(event);
m_mousePosition = event.position;
}
void QueuePointerLeaveEvent() {
UIInputEvent event = {};
event.type = UIInputEventType::PointerLeave;
if (m_hwnd != nullptr) {
POINT clientPoint = {};
GetCursorPos(&clientPoint);
ScreenToClient(m_hwnd, &clientPoint);
event.position = UIPoint(static_cast<float>(clientPoint.x), static_cast<float>(clientPoint.y));
m_mousePosition = event.position;
}
m_pendingEvents.push_back(event);
}
void QueuePointerWheelEvent(short wheelDelta, WPARAM wParam, LPARAM lParam) {
if (m_hwnd == nullptr) {
return;
}
POINT screenPoint = {
GET_X_LPARAM(lParam),
GET_Y_LPARAM(lParam)
};
ScreenToClient(m_hwnd, &screenPoint);
UIInputEvent event = {};
event.type = UIInputEventType::PointerWheel;
event.position = UIPoint(static_cast<float>(screenPoint.x), static_cast<float>(screenPoint.y));
event.wheelDelta = static_cast<float>(wheelDelta);
event.modifiers = m_inputModifierTracker.BuildPointerModifiers(static_cast<std::size_t>(wParam));
m_pendingEvents.push_back(event);
m_mousePosition = event.position;
}
void QueueFocusEvent(UIInputEventType type) {
UIInputEvent event = {};
event.type = type;
event.modifiers = m_inputModifierTracker.GetCurrentModifiers();
m_pendingEvents.push_back(event);
}
void HandlePointerDown(UIPointerButton button, WPARAM wParam, LPARAM lParam) {
QueuePointerEvent(UIInputEventType::PointerButtonDown, button, wParam, lParam);
const UIPoint point(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
if (ContainsPoint(m_slotRect, point.x, point.y) &&
ContainsPoint(m_shellFrame.slotLayout.inputRect, point.x, point.y)) {
SetCapture(m_hwnd);
}
}
void HandlePointerUp(UIPointerButton button, WPARAM wParam, LPARAM lParam) {
QueuePointerEvent(UIInputEventType::PointerButtonUp, button, wParam, lParam);
const UIPoint point(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
if (GetCapture() == m_hwnd) {
ReleaseCapture();
}
for (const ButtonState& buttonState : m_buttons) {
if (!ContainsPoint(buttonState.rect, point.x, point.y)) {
continue;
}
ExecuteAction(buttonState.action);
break;
}
}
std::vector<UIEditorViewportSlotToolItem> BuildToolItems() const {
return {
{ "mode", "Perspective", UIEditorViewportSlotToolSlot::Leading, true, true, 98.0f },
{ "focus", "ViewportShell", UIEditorViewportSlotToolSlot::Trailing, true, false, 108.0f }
};
}
std::vector<UIEditorStatusBarSegment> BuildStatusSegments() const {
return {
{
"chrome-top",
std::string("TopBar ") + BoolText(m_showTopBar),
UIEditorStatusBarSlot::Leading,
{},
true,
true,
96.0f
},
{
"chrome-bottom",
std::string("BottomBar ") + BoolText(m_showBottomBar),
UIEditorStatusBarSlot::Leading,
{},
true,
false,
120.0f
},
{
"frame",
m_textureEnabled ? "Texture On" : "Fallback",
UIEditorStatusBarSlot::Trailing,
{},
true,
true,
108.0f
}
};
}
UIEditorViewportShellSpec BuildShellSpec() const {
UIEditorViewportShellSpec spec = {};
spec.chrome.title = "Scene View";
spec.chrome.subtitle = "ViewportShell 基础壳层";
spec.chrome.showTopBar = m_showTopBar;
spec.chrome.showBottomBar = m_showBottomBar;
spec.chrome.topBarHeight = 40.0f;
spec.chrome.bottomBarHeight = 28.0f;
spec.toolItems = BuildToolItems();
spec.statusSegments = BuildStatusSegments();
return spec;
}
UIEditorViewportSlotFrame BuildViewportFrame(const UISize& requestedSize) const {
UIEditorViewportSlotFrame frame = {};
frame.requestedSize = requestedSize;
if (m_textureEnabled) {
frame.hasTexture = true;
frame.texture = { 1u, 1280u, 720u };
frame.presentedSize = UISize(1280.0f, 720.0f);
frame.statusText = "Fake viewport frame";
} else {
frame.hasTexture = false;
frame.statusText = "这里å<EFBFBD>ªéªŒè¯?ViewportShell contract,ä¸<C3A4>æŽ?Scene/Game 业务ã€?;
}
return frame;
}
void UpdateLayoutForCurrentWindow() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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 float leftColumnWidth = 460.0f;
const float outerPadding = 20.0f;
m_introRect = UIRect(outerPadding, outerPadding, leftColumnWidth, 246.0f);
m_controlsRect = UIRect(outerPadding, 286.0f, leftColumnWidth, 246.0f);
m_stateRect = UIRect(outerPadding, 552.0f, leftColumnWidth, height - 572.0f);
m_previewRect = UIRect(
leftColumnWidth + outerPadding * 2.0f,
outerPadding,
width - leftColumnWidth - outerPadding * 3.0f,
height - outerPadding * 2.0f);
m_slotRect = UIRect(
m_previewRect.x + 18.0f,
m_previewRect.y + 18.0f,
m_previewRect.width - 36.0f,
m_previewRect.height - 36.0f);
const float buttonHeight = 34.0f;
const float gap = 10.0f;
const float left = m_controlsRect.x + 16.0f;
const float top = m_controlsRect.y + 54.0f;
const float widthAvailable = m_controlsRect.width - 32.0f;
m_buttons = {
{
ActionId::ToggleTopBar,
std::string("TopBar: ") + (m_showTopBar ? "å¼€å<EFBFBD>? : "å³é­"),
UIRect(left, top, widthAvailable, buttonHeight),
m_showTopBar
},
{
ActionId::ToggleBottomBar,
std::string("BottomBar: ") + (m_showBottomBar ? "å¼€å<EFBFBD>? : "å³é­"),
UIRect(left, top + (buttonHeight + gap), widthAvailable, buttonHeight),
m_showBottomBar
},
{
ActionId::ToggleTexture,
std::string("Texture: ") + (m_textureEnabled ? "å¼€å<EFBFBD>? : "å³é­"),
UIRect(left, top + (buttonHeight + gap) * 2.0f, widthAvailable, buttonHeight),
m_textureEnabled
},
{
ActionId::Reset,
"é‡<EFBFBD>ç½®",
UIRect(left, top + (buttonHeight + gap) * 3.0f, widthAvailable, buttonHeight),
false
},
{
ActionId::Capture,
"截图",
UIRect(left, top + (buttonHeight + gap) * 4.0f, widthAvailable, buttonHeight),
false
}
};
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::ToggleTopBar:
m_showTopBar = !m_showTopBar;
m_lastResult = m_showTopBar ? "TopBar 已打开" : "TopBar 已关�;
break;
case ActionId::ToggleBottomBar:
m_showBottomBar = !m_showBottomBar;
m_lastResult = m_showBottomBar ? "BottomBar 已打开" : "BottomBar 已关�;
break;
case ActionId::ToggleTexture:
m_textureEnabled = !m_textureEnabled;
m_lastResult = m_textureEnabled ? "切到 Texture 分支" : "切到 Fallback 分支";
break;
case ActionId::Reset:
ResetScenario();
m_lastResult = "状æ€<EFBFBD>å·²é‡<EFBFBD>ç½®";
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
InvalidateRect(m_hwnd, nullptr, FALSE);
UpdateWindow(m_hwnd);
m_lastResult = "截图已排�;
break;
}
}
void UpdateLastResult() {
const auto& inputFrame = m_shellFrame.inputFrame;
if (inputFrame.captureStarted) {
m_lastResult = "CaptureStarted";
} else if (inputFrame.captureEnded) {
m_lastResult = "CaptureEnded";
} else if (inputFrame.focusLost) {
m_lastResult = "FocusLost";
} else if (inputFrame.focusGained) {
m_lastResult = "FocusGained";
} else if (inputFrame.pointerPressedInside) {
m_lastResult = "PointerDownInside";
} else if (inputFrame.pointerReleasedInside) {
m_lastResult = "PointerUpInside";
} else if (inputFrame.wheelDelta != 0.0f) {
m_lastResult = "Wheel " + FormatFloat(inputFrame.wheelDelta);
} else if (m_hasSnapshot && m_previousHovered != inputFrame.hovered) {
m_lastResult = std::string("Hover ") + BoolText(inputFrame.hovered);
}
m_previousHovered = inputFrame.hovered;
m_previousFocused = inputFrame.focused;
m_previousCaptured = inputFrame.captured;
m_hasSnapshot = true;
}
void UpdateScenarioFrame() {
m_shellSpec = BuildShellSpec();
m_shellRequest = ResolveUIEditorViewportShellRequest(m_slotRect, m_shellSpec);
m_shellModel = {};
m_shellModel.spec = m_shellSpec;
m_shellModel.frame = BuildViewportFrame(m_shellRequest.requestedViewportSize);
std::vector<UIInputEvent> frameEvents = std::move(m_pendingEvents);
m_pendingEvents.clear();
m_shellFrame = UpdateUIEditorViewportShell(
m_shellState,
m_slotRect,
m_shellModel,
frameEvents);
m_hoverHit = HitTestUIEditorViewportSlot(m_shellFrame.slotLayout, m_mousePosition);
UpdateLastResult();
}
void RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
RECT clientRect = {};
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));
UpdateLayoutForCurrentWindow();
UpdateScenarioFrame();
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("ViewportShellBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
DrawCard(
drawList,
m_introRect,
"这个æµè¯•验è¯<EFBFBD>什么功能?",
"å<EFBFBD>ªéªŒè¯?Resolve + Update çš?ViewportShell contract,ä¸<C3A4>æŽ?Scene/Game 业务ã€?);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 66.0f),
"1. 验è¯<C3A8> TopBar / BottomBar å¯?Request Size å’?Input Rect 的影å“<C3A5>ã€?,
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 88.0f),
"2. 验è¯<C3A8> Hover / Focus / Capture 是å<C2AF>¦ä¸?surface 输入桥å<C2A5>Œæ­¥ã€?,
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 110.0f),
"3. 验è¯<C3A8> Texture / Fallback 分支切æ<E280A1>¢å<C2A2>Žï¼Œframe 输出ä»<C3A4>ç”± shell 承接ã€?,
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 132.0f),
"4. 验è¯<C3A8> hover Surface ä¸?click Surface å<>Žçš„ Focus / Capture å<>˜åŒã€?,
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 154.0f),
"5. 验è¯<C3A8> TopBar / BottomBar / Texture 三个开关切æ<E280A1>¢å<C2A2>Žçš„æ•´ä½“表现ã€?,
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 176.0f),
"6. 验è¯<C3A8>截å¾é“¾è·¯ï¼Œæ”¯æŒ<C3A6>按é®è§¦å<C2A6>åŒ F12 手动截图ã€?,
kTextMuted,
11.0f);
drawList.AddText(
UIPoint(m_introRect.x + 16.0f, m_introRect.y + 204.0f),
"7. 左侧æŒ<C3A6>续展示 Request / Input / HoverHit / Hover / Focus / Capture æ˜è¦<C3A8>ã€?,
kTextWeak,
11.0f);
DrawCard(drawList, m_controlsRect, "æ“<EFBFBD>作", "å<EFBFBD>ªä¿<EFBFBD>留当å‰<EFBFBD>æµè¯•真正需è¦<EFBFBD>检查的 5 个控件ã€?);
for (const ButtonState& button : m_buttons) {
DrawButton(drawList, button);
}
DrawCard(drawList, m_stateRect, "状æ€?, "é<EFBFBD>ç¹çœ?requestã<EFBFBD>input rectã<EFBFBD>hover å½ä¸­ä¸Žè¾å¥æ¡¥å<EFBFBD>Œæ­¥ã?);
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 += 22.0f;
};
addStateLine("TopBar: " + BoolText(m_showTopBar), kTextPrimary);
addStateLine("BottomBar: " + BoolText(m_showBottomBar), kTextPrimary);
addStateLine("Texture: " + BoolText(m_textureEnabled), kTextPrimary);
addStateLine("Request Size: " + FormatSize(m_shellRequest.requestedViewportSize), kTextPrimary);
addStateLine("Input Rect: " + FormatRect(m_shellFrame.slotLayout.inputRect), kTextMuted, 11.0f);
addStateLine("Hover Hit: " + DescribeHitTarget(m_hoverHit), kTextPrimary);
addStateLine("Hover: " + BoolText(m_shellFrame.inputFrame.hovered), kTextPrimary);
addStateLine("Focus: " + BoolText(m_shellFrame.inputFrame.focused), kTextPrimary);
addStateLine("æ<EFBFBD>•获: " + 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/")
: m_autoScreenshot.GetLastCaptureSummary());
addStateLine(captureSummary, kTextWeak, 11.0f);
DrawCard(drawList, m_previewRect, "Preview", "这里å<EFBFBD>ªæœ‰ä¸€ä¸?ViewportShell,用æ<C2A8>¥æ£€æŸ?Editor 基础壳层 composeã€?);
drawList.AddFilledRect(
UIRect(
m_previewRect.x + 12.0f,
m_previewRect.y + 44.0f,
m_previewRect.width - 24.0f,
m_previewRect.height - 56.0f),
kPreviewBg,
10.0f);
AppendUIEditorViewportSlotBackground(
drawList,
m_shellFrame.slotLayout,
m_shellModel.spec.toolItems,
m_shellModel.spec.statusSegments,
m_shellFrame.slotState);
AppendUIEditorViewportSlotForeground(
drawList,
m_shellFrame.slotLayout,
m_shellModel.spec.chrome,
m_shellModel.frame,
m_shellModel.spec.toolItems,
m_shellModel.spec.statusSegments,
m_shellFrame.slotState);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
InputModifierTracker m_inputModifierTracker = {};
std::filesystem::path m_captureRoot = {};
std::vector<UIInputEvent> m_pendingEvents = {};
std::vector<ButtonState> m_buttons = {};
UIEditorViewportShellState m_shellState = {};
UIEditorViewportShellRequest m_shellRequest = {};
UIEditorViewportShellFrame m_shellFrame = {};
UIEditorViewportShellSpec m_shellSpec = {};
UIEditorViewportShellModel m_shellModel = {};
UIEditorViewportSlotHitTarget m_hoverHit = {};
UIRect m_introRect = {};
UIRect m_controlsRect = {};
UIRect m_stateRect = {};
UIRect m_previewRect = {};
UIRect m_slotRect = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
bool m_showTopBar = true;
bool m_showBottomBar = true;
bool m_textureEnabled = true;
bool m_previousHovered = false;
bool m_previousFocused = false;
bool m_previousCaptured = false;
bool m_hasSnapshot = false;
std::string m_lastResult = {};
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

Some files were not shown because too many files have changed in this diff Show More