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_property_grid_basic_validation WIN32
)
target_include_directories(editor_ui_property_grid_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/UIEditorPropertyGridInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorPropertyGrid.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -48,24 +50,17 @@ using XCEngine::UI::Editor::UpdateUIEditorPropertyGridInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorPropertyGridBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorPropertyGridForeground;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorPropertyGrid;
using XCEngine::UI::Editor::Widgets::ResolveUIEditorPropertyGridFieldValueText;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorPropertyGridBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | PropertyGrid Basic";
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
@@ -95,6 +90,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 &&
@@ -129,10 +129,13 @@ std::int32_t MapEditorKey(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);
@@ -166,48 +169,109 @@ 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);
}
UIEditorPropertyGridField MakeTextField(
std::string id,
std::string label,
std::string value,
bool readOnly = false) {
UIEditorPropertyGridField field = {};
field.fieldId = std::move(id);
field.label = std::move(label);
field.valueText = std::move(value);
field.readOnly = readOnly;
return field;
}
UIEditorPropertyGridField MakeBoolField(
std::string id,
std::string label,
bool value) {
UIEditorPropertyGridField field = {};
field.fieldId = std::move(id);
field.label = std::move(label);
field.kind = UIEditorPropertyGridFieldKind::Bool;
field.boolValue = value;
return field;
}
UIEditorPropertyGridField MakeNumberField(
std::string id,
std::string label,
double value) {
UIEditorPropertyGridField field = {};
field.fieldId = std::move(id);
field.label = std::move(label);
field.kind = UIEditorPropertyGridFieldKind::Number;
field.numberValue.value = value;
field.numberValue.step = 1.0;
field.numberValue.minValue = 0.0;
field.numberValue.maxValue = 5000.0;
field.numberValue.integerMode = true;
return field;
}
UIEditorPropertyGridField MakeEnumField(
std::string id,
std::string label,
std::vector<std::string> options,
std::size_t selectedIndex) {
UIEditorPropertyGridField field = {};
field.fieldId = std::move(id);
field.label = std::move(label);
field.kind = UIEditorPropertyGridFieldKind::Enum;
field.enumValue.options = std::move(options);
field.enumValue.selectedIndex = selectedIndex;
return field;
}
std::vector<UIEditorPropertyGridSection> BuildSections() {
return {
{
"transform",
"Transform",
"inspector",
"Inspector",
{
{ "position", "Position", "0, 0, 0", false, 0.0f },
{ "rotation", "Rotation", "0, 45, 0", false, 0.0f },
{ "scale", "Scale", "1, 1, 1", false, 0.0f }
},
0.0f
},
{
"material",
"Material",
{
{ "shader", "Shader", "Standard/Lit", false, 0.0f },
{ "queue", "Render Queue", "2000", false, 0.0f },
{ "guid", "GUID", "asset-guid-001", true, 0.0f }
MakeBoolField("enabled", "Enabled", true),
MakeNumberField("render_queue", "Render Queue", 2000.0),
MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u),
MakeTextField("tag", "Tag", "Player")
},
0.0f
},
@@ -215,8 +279,7 @@ std::vector<UIEditorPropertyGridSection> BuildSections() {
"metadata",
"Metadata",
{
{ "tag", "Tag", "", false, 0.0f },
{ "layer", "Layer", "Default", false, 0.0f }
MakeTextField("guid", "GUID", "asset-guid-001", true)
},
0.0f
}
@@ -246,8 +309,7 @@ std::string DescribeHitTarget(
std::string BuildExpandedSummary(const UIExpansionModel& expansionModel) {
std::ostringstream stream = {};
stream << (expansionModel.IsExpanded("transform") ? "Transform" : "-");
stream << " / " << (expansionModel.IsExpanded("material") ? "Material" : "-");
stream << (expansionModel.IsExpanded("inspector") ? "Inspector" : "-");
stream << " / " << (expansionModel.IsExpanded("metadata") ? "Metadata" : "-");
return stream.str();
}
@@ -467,6 +529,14 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/property_grid_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
return true;
@@ -492,16 +562,18 @@ 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() {
m_sections = BuildSections();
m_selectionModel = {};
m_selectionModel.SetSelection("rotation");
m_selectionModel.SetSelection("render_queue");
m_expansionModel = {};
m_expansionModel.Expand("transform");
m_expansionModel.Expand("material");
m_expansionModel.Expand("inspector");
m_expansionModel.Expand("metadata");
m_propertyEditModel = {};
m_interactionState = {};
@@ -520,6 +592,7 @@ private:
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme);
m_gridFrame =
UpdateUIEditorPropertyGridInteraction(
m_interactionState,
@@ -528,23 +601,16 @@ private:
m_propertyEditModel,
layout.gridRect,
m_sections,
{});
{},
metrics);
}
void ApplyCommittedValue(const UIEditorPropertyGridInteractionResult& result) {
if (!result.editCommitted || result.committedFieldId.empty()) {
void UpdateLastCommit(const UIEditorPropertyGridInteractionResult& result) {
if (!result.fieldValueChanged || result.changedFieldId.empty()) {
return;
}
for (UIEditorPropertyGridSection& section : m_sections) {
for (UIEditorPropertyGridField& field : section.fields) {
if (field.fieldId == result.committedFieldId) {
field.valueText = result.committedValue;
m_lastCommit = result.committedFieldId + " = " + result.committedValue;
return;
}
}
}
m_lastCommit = result.changedFieldId + " = " + result.changedValue;
}
void OnResize(UINT width, UINT height) {
@@ -661,6 +727,7 @@ private:
UIEditorPropertyGridInteractionResult PumpGridEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme);
m_gridFrame =
UpdateUIEditorPropertyGridInteraction(
m_interactionState,
@@ -669,8 +736,9 @@ private:
m_propertyEditModel,
layout.gridRect,
m_sections,
std::move(events));
ApplyCommittedValue(m_gridFrame.result);
std::move(events),
metrics);
UpdateLastCommit(m_gridFrame.result);
return m_gridFrame.result;
}
@@ -678,6 +746,26 @@ private:
const UIEditorPropertyGridInteractionResult& result,
bool wasFocused,
bool insideGrid) {
if (result.popupOpened && !m_interactionState.propertyGridState.popupFieldId.empty()) {
m_lastResult = "打开枚举下拉: " + m_interactionState.propertyGridState.popupFieldId;
return;
}
if (result.popupClosed && !result.fieldValueChanged) {
m_lastResult = "关闭枚举下拉";
return;
}
if (result.fieldValueChanged) {
m_lastResult = "字段已更新: " + result.changedFieldId + " = " + result.changedValue;
return;
}
if (result.editCommitRejected && !result.activeFieldId.empty()) {
m_lastResult = "提交被拒绝: " + result.activeFieldId;
return;
}
if (result.editCommitted) {
m_lastResult = "提交字段: " + result.committedFieldId + " = " + result.committedValue;
return;
@@ -753,99 +841,124 @@ 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);
RefreshGridFrame();
const UIEditorPropertyGridHitTarget currentHit =
HitTestUIEditorPropertyGrid(m_gridFrame.layout, m_mousePosition);
const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
const auto popupPalette = XCEngine::UI::Editor::ResolveUIEditorMenuPopupPalette(m_theme);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorPropertyGridBasic");
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 PropertyGrid 基础控件,不涉及任何 Inspector 业务逻辑。");
"只验证 Editor PropertyGrid 作为 typed 属性宿主的基础契约,不涉及任何 Inspector 业务逻辑。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 section header检查展开/折叠是否稳定,字段布局不能歪。",
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 点击 field row 只切换 selection点击 value box 才进入编辑态",
kTextPrimary,
12.0f);
"2. 点击 value hostBool toggle、Number/Text edit、Enum popup",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 编辑态输入字符,按 Enter commit,按 Esc cancelread-only 字段不能进编辑",
kTextPrimary,
12.0f);
"3. Number / Text 编辑后按 Enter commitEsc cancelGUID 只读",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. Grid 获得 focus 后按 Up / Down / Home / End检查字段导航和 selection 同步。",
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 按 F12 手动截图;设置 XCUI_AUTO_CAPTURE_ON_STARTUP=1 可自动截图。",
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 / selection / edit / commit。");
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状态摘要",
"重点检查 hit / focus / selection / edit / popup / commit。");
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit, m_sections),
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.propertyGridState.focused ? "" : ""),
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
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);
shellPalette.textSuccess,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
"Active Edit: " +
(m_propertyEditModel.HasActiveEdit() ? m_propertyEditModel.GetActiveFieldId() : std::string("(none)")),
kTextMuted,
12.0f);
shellPalette.textMuted,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Staged: " +
(m_propertyEditModel.HasActiveEdit() ? m_propertyEditModel.GetStagedValue() : std::string("(none)")),
kTextMuted,
12.0f);
shellPalette.textMuted,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
"Expanded: " + BuildExpandedSummary(m_expansionModel),
kTextMuted,
12.0f);
"Popup: " +
(m_interactionState.propertyGridState.popupFieldId.empty()
? std::string("(none)")
: (m_interactionState.propertyGridState.popupFieldId + " / index " +
std::to_string(m_interactionState.propertyGridState.popupHighlightedIndex))),
shellPalette.textMuted,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
"Last Commit: " + m_lastCommit,
kTextPrimary,
12.0f);
"Expanded: " + BuildExpandedSummary(m_expansionModel),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
"Last Commit: " + m_lastCommit,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f),
"Result: " + m_lastResult,
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
@@ -854,24 +967,42 @@ private:
? std::string("F12 -> tests/UI/Editor/integration/shell/property_grid_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f),
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 286.0f),
captureSummary,
kTextWeak,
12.0f);
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 310.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.previewRect, "PropertyGrid 预览", "这里只放一个 PropertyGrid不混入任何业务面板。");
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"PropertyGrid 预览",
"这里只放一个 PropertyGrid用来验证 typed 属性宿主。");
AppendUIEditorPropertyGridBackground(
drawList,
m_gridFrame.layout,
m_sections,
m_selectionModel,
m_propertyEditModel,
m_interactionState.propertyGridState);
m_interactionState.propertyGridState,
propertyPalette,
propertyMetrics);
AppendUIEditorPropertyGridForeground(
drawList,
m_gridFrame.layout,
m_sections,
m_propertyEditModel);
m_interactionState.propertyGridState,
m_propertyEditModel,
propertyPalette,
propertyMetrics,
popupPalette,
popupMetrics);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
@@ -893,11 +1024,13 @@ private:
UIPropertyEditModel m_propertyEditModel = {};
UIEditorPropertyGridInteractionState m_interactionState = {};
UIEditorPropertyGridInteractionFrame m_gridFrame = {};
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_lastCommit = {};
std::string m_themeStatus = "fallback";
};
} // namespace