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_number_field_basic_validation WIN32
)
target_include_directories(editor_ui_number_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/UIEditorNumberFieldInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorNumberField.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -46,19 +48,11 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorNumberFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | NumberField Basic";
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
@@ -75,6 +69,9 @@ struct ScenarioLayout {
UIRect controlRect = {};
UIRect stateRect = {};
UIRect previewRect = {};
UIRect inspectorRect = {};
UIRect inspectorHeaderRect = {};
UIRect sectionRect = {};
UIRect fieldRect = {};
std::vector<ButtonLayout> buttons = {};
};
@@ -87,6 +84,11 @@ std::filesystem::path ResolveRepoRootPath() {
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
@@ -117,13 +119,16 @@ std::int32_t MapNumberFieldKey(UINT keyCode) {
}
}
ScenarioLayout BuildScenarioLayout(float width, float height) {
constexpr float margin = 20.0f;
constexpr float leftWidth = 430.0f;
constexpr float gap = 16.0f;
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, 232.0f);
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,
@@ -135,11 +140,26 @@ ScenarioLayout BuildScenarioLayout(float width, float height) {
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.previewRect.x + 24.0f,
layout.previewRect.y + 82.0f,
320.0f,
32.0f);
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;
@@ -150,34 +170,63 @@ ScenarioLayout BuildScenarioLayout(float width, float height) {
return layout;
}
XCEngine::UI::Editor::Widgets::UIEditorNumberFieldMetrics ResolveHostedNumberFieldMetrics(
const Style::UITheme& theme) {
const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(theme);
const auto numberMetrics = XCEngine::UI::Editor::ResolveUIEditorNumberFieldMetrics(theme);
return XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldMetrics(propertyMetrics, numberMetrics);
}
XCEngine::UI::Editor::Widgets::UIEditorNumberFieldPalette ResolveHostedNumberFieldPalette(
const Style::UITheme& theme) {
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(theme);
const auto numberPalette = XCEngine::UI::Editor::ResolveUIEditorNumberFieldPalette(theme);
return XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldPalette(propertyPalette, numberPalette);
}
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.AddText(UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.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,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
}
std::string DescribeHitTarget(const UIEditorNumberFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorNumberFieldHitTargetKind::DecrementButton:
return "decrement";
case UIEditorNumberFieldHitTargetKind::IncrementButton:
return "increment";
case UIEditorNumberFieldHitTargetKind::ValueBox:
return "value_box";
case UIEditorNumberFieldHitTargetKind::Row:
@@ -381,6 +430,14 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/number_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
@@ -406,7 +463,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() {
@@ -434,11 +494,13 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolveHostedNumberFieldMetrics(m_theme);
m_frame = UpdateUIEditorNumberFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
{});
{},
metrics);
}
void OnResize(UINT width, UINT height) {
@@ -542,17 +604,19 @@ private:
UIEditorNumberFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolveHostedNumberFieldMetrics(m_theme);
m_frame = UpdateUIEditorNumberFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
std::move(events));
std::move(events),
metrics);
return m_frame.result;
}
void UpdateResultText(const UIEditorNumberFieldInteractionResult& result) {
if (result.editCommitRejected) {
m_lastResult = "提交失败,仍保在编辑态";
m_lastResult = "提交失败,仍保在编辑态";
return;
}
if (result.editCommitted) {
@@ -600,83 +664,101 @@ 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 UIEditorNumberFieldHitTarget currentHit =
HitTestUIEditorNumberField(m_frame.layout, m_mousePosition);
const auto numberMetrics = ResolveHostedNumberFieldMetrics(m_theme);
const auto numberPalette = ResolveHostedNumberFieldPalette(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorNumberFieldBasic");
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 NumberField 的基础交互契约,不涉及 PropertyGrid 或任何业务 Inspector");
"验证 Inspector 宿主中的 NumberField 交互契约和默认宿主风格");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 +/- 按钮,检查步进和上下界钳制是否稳定",
kTextPrimary,
12.0f);
"1. 点击 value box检查是否进入编辑态外观应是 Unity 风格单输入框",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 控件获得 focus 后按 Left / Right / Up / Down / Home / End检查键盘步进。",
kTextPrimary,
12.0f);
"2. 获得 focus 后按 Left / Right / Up / Down / Home / End检查键盘步进。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 按 Enter 进入编辑态直接输入字符Enter commitEsc cancel。",
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 检查 Hover / Focus / Editing / Value / Result 是否同步更新。",
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 按 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, m_hasHoveredAction && m_hoveredAction == button.action);
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(drawList, layout.stateRect, "状态摘要", "重点检查 hit / focus / editing / value / result。");
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状态摘要",
"重点检查 hit / focus / editing / 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.numberFieldState.focused ? "" : ""),
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Editing: ") + (m_interactionState.numberFieldState.editing ? "" : ""),
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
"Value: " + FormatUIEditorNumberFieldValue(m_spec),
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Display: " + m_interactionState.numberFieldState.displayText,
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Result: " + m_lastResult,
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
@@ -687,15 +769,44 @@ private:
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
captureSummary,
kTextWeak,
12.0f);
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.previewRect, "NumberField 预览", "这里只放一个 NumberField。");
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"NumberField 预览",
"这里仅预览 Inspector 宿主中的 Unity 风格 Number 字段。");
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);
m_interactionState.numberFieldState,
numberPalette,
numberMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
@@ -714,10 +825,12 @@ private:
UIEditorNumberFieldSpec m_spec = {};
UIEditorNumberFieldInteractionState m_interactionState = {};
UIEditorNumberFieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
std::string m_themeStatus = "fallback";
};
} // namespace