ui: add typed editor field foundations

This commit is contained in:
2026-04-08 02:52:28 +08:00
parent 805e07bf90
commit 0a392e1311
69 changed files with 11676 additions and 1169 deletions

View File

@@ -3,6 +3,7 @@ add_executable(editor_ui_bool_field_basic_validation WIN32
)
target_include_directories(editor_ui_bool_field_basic_validation PRIVATE
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
${CMAKE_SOURCE_DIR}/new_editor/include
${CMAKE_SOURCE_DIR}/new_editor/app
${CMAKE_SOURCE_DIR}/engine/include

View File

@@ -3,7 +3,9 @@
#endif
#include <XCEditor/Core/UIEditorBoolFieldInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorBoolField.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -45,19 +47,11 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorBoolField;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorBoolFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | BoolField Basic";
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 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
@@ -83,10 +77,14 @@ std::filesystem::path ResolveRepoRootPath() {
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
root = root.substr(1u, root.size() - 2u);
}
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -105,10 +103,13 @@ std::int32_t MapBoolFieldKey(UINT keyCode) {
}
}
ScenarioLayout BuildScenarioLayout(float width, float height) {
constexpr float margin = 20.0f;
ScenarioLayout BuildScenarioLayout(
float width,
float height,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
const float margin = shellMetrics.margin;
constexpr float leftWidth = 430.0f;
constexpr float gap = 16.0f;
const float gap = shellMetrics.gap;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 214.0f);
@@ -141,33 +142,48 @@ ScenarioLayout BuildScenarioLayout(float width, float height) {
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, kCardBg, 10.0f);
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
drawList.AddFilledRect(rect, 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), kTextMuted, 12.0f);
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 ? kButtonHoverBg : kButtonBg, 8.0f);
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
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,
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
}
std::string DescribeHitTarget(const UIEditorBoolFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorBoolFieldHitTargetKind::Toggle:
return "toggle";
case UIEditorBoolFieldHitTargetKind::Checkbox:
return "checkbox";
case UIEditorBoolFieldHitTargetKind::Row:
return "row";
case UIEditorBoolFieldHitTargetKind::None:
@@ -355,6 +371,14 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/bool_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
@@ -380,7 +404,10 @@ private:
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
return BuildScenarioLayout(width, height);
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
}
void ResetScenario() {
@@ -403,12 +430,14 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme);
m_frame = UpdateUIEditorBoolFieldInteraction(
m_interactionState,
m_value,
layout.fieldRect,
m_spec,
{});
{},
metrics);
}
void OnResize(UINT width, UINT height) {
@@ -502,18 +531,20 @@ private:
UIEditorBoolFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme);
m_frame = UpdateUIEditorBoolFieldInteraction(
m_interactionState,
m_value,
layout.fieldRect,
m_spec,
std::move(events));
std::move(events),
metrics);
return m_frame.result;
}
void UpdateResultText(const UIEditorBoolFieldInteractionResult& result) {
if (result.valueChanged) {
m_lastResult = std::string("值已切换到 ") + (m_value ? "true" : "false");
m_lastResult = std::string("值已切换到: ") + (m_value ? "true" : "false");
return;
}
@@ -547,76 +578,90 @@ private:
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const ScenarioLayout layout = BuildScenarioLayout(width, height);
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
const UIEditorBoolFieldHitTarget currentHit =
HitTestUIEditorBoolField(m_frame.layout, m_mousePosition);
const auto boolMetrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme);
const auto boolPalette = XCEngine::UI::Editor::ResolveUIEditorBoolFieldPalette(m_theme);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorBoolFieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个测试在验证什么功能",
"只验证 Editor BoolField 的基础交互契约,不涉及 PropertyGrid 或任何业务 Inspector。");
"只验证 Editor BoolField 的基础交互契约,不涉及 PropertyGrid 或业务 Inspector。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 row 或 toggle,检查 true / false 是否稳定切换。",
kTextPrimary,
12.0f);
"1. 点击 row 或 checkbox,检查 true / false 是否稳定切换。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 控件获得 focus 后按 Space / Enter也必须能切换值。",
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 检查 Hover / Focus / Value / Result 是否同步更新。",
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 按 F12 或点击截图按钮,确认自动截图路径正确。",
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, "操作");
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(drawList, layout.stateRect, "状态摘要", "重点检查 hit / focus / value / result。");
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状态摘要",
"重点检查 hit / focus / value / result。");
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "" : ""),
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Active: ") + (m_interactionState.fieldState.active ? "" : ""),
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
std::string("Value: ") + (m_value ? "true" : "false"),
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Result: " + m_lastResult,
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
@@ -627,17 +672,30 @@ private:
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
captureSummary,
kTextWeak,
12.0f);
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.previewRect, "BoolField 预览", "这里只放一个 BoolField。");
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"BoolField 预览",
"这里只放一个 Unity 风格 BoolField。");
UIEditorBoolFieldSpec previewSpec = m_spec;
previewSpec.value = m_value;
AppendUIEditorBoolField(
drawList,
layout.fieldRect,
previewSpec,
m_interactionState.fieldState);
m_interactionState.fieldState,
boolPalette,
boolMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
@@ -657,10 +715,12 @@ private:
bool m_value = false;
UIEditorBoolFieldInteractionState m_interactionState = {};
UIEditorBoolFieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
std::string m_themeStatus = "fallback";
};
} // namespace