Refactor new editor boundaries and test ownership
91
tests/UI/Editor/manual_validation/shell/CMakeLists.txt
Normal 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()
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 115 KiB |
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
1249
tests/UI/Editor/manual_validation/shell/context_menu_basic/main.cpp
Normal 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"
|
||||
)
|
||||
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 84 KiB |
676
tests/UI/Editor/manual_validation/shell/dock_host_basic/main.cpp
Normal 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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
767
tests/UI/Editor/manual_validation/shell/list_view_basic/main.cpp
Normal 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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 81 KiB |
@@ -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"
|
||||
)
|
||||
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 85 KiB |
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
1745
tests/UI/Editor/manual_validation/shell/menu_bar_basic/main.cpp
Normal 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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
1036
tests/UI/Editor/manual_validation/shell/property_grid_basic/main.cpp
Normal 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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 58 KiB |
734
tests/UI/Editor/manual_validation/shell/tab_strip_basic/main.cpp
Normal 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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 63 KiB |
815
tests/UI/Editor/manual_validation/shell/tree_view_basic/main.cpp
Normal 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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 66 KiB |
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 83 KiB |
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||