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

@@ -68,6 +68,21 @@ if(TARGET editor_ui_number_field_basic_validation)
editor_ui_number_field_basic_validation)
endif()
if(TARGET editor_ui_text_field_basic_validation)
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
editor_ui_text_field_basic_validation)
endif()
if(TARGET editor_ui_vector2_field_basic_validation)
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
editor_ui_vector2_field_basic_validation)
endif()
if(TARGET editor_ui_vector3_field_basic_validation)
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
editor_ui_vector3_field_basic_validation)
endif()
if(TARGET editor_ui_enum_field_basic_validation)
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
editor_ui_enum_field_basic_validation)

View File

@@ -0,0 +1,218 @@
#pragma once
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEngine/UI/Style/DocumentStyleCompiler.h>
#include <XCEngine/UI/Style/Theme.h>
#include <filesystem>
#include <initializer_list>
#include <string>
#include <string_view>
namespace XCEngine::Tests::EditorUI {
struct EditorValidationThemeLoadResult {
::XCEngine::UI::Style::UITheme theme = {};
std::string error = {};
bool succeeded = false;
};
struct EditorValidationShellPalette {
::XCEngine::UI::UIColor windowBackground = ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f);
::XCEngine::UI::UIColor cardBackground = ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
::XCEngine::UI::UIColor cardBorder = ::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f);
::XCEngine::UI::UIColor textPrimary = ::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 1.0f);
::XCEngine::UI::UIColor textMuted = ::XCEngine::UI::UIColor(0.72f, 0.72f, 0.72f, 1.0f);
::XCEngine::UI::UIColor textWeak = ::XCEngine::UI::UIColor(0.56f, 0.56f, 0.56f, 1.0f);
::XCEngine::UI::UIColor textSuccess = ::XCEngine::UI::UIColor(0.63f, 0.76f, 0.63f, 1.0f);
::XCEngine::UI::UIColor buttonBackground = ::XCEngine::UI::UIColor(0.25f, 0.25f, 0.25f, 1.0f);
::XCEngine::UI::UIColor buttonHoverBackground = ::XCEngine::UI::UIColor(0.32f, 0.32f, 0.32f, 1.0f);
};
struct EditorValidationShellMetrics {
float margin = 20.0f;
float gap = 16.0f;
float cardRadius = 10.0f;
float buttonRadius = 8.0f;
float titleFontSize = 17.0f;
float bodyFontSize = 12.0f;
};
inline ::XCEngine::UI::UIColor ToUIColor(const ::XCEngine::Math::Color& color) {
return ::XCEngine::UI::UIColor(color.r, color.g, color.b, color.a);
}
inline bool TryResolveThemeFloat(
const ::XCEngine::UI::Style::UITheme& theme,
std::string_view tokenName,
float& outValue) {
const auto resolution =
theme.ResolveToken(std::string(tokenName), ::XCEngine::UI::Style::UIStyleValueType::Float);
if (resolution.status != ::XCEngine::UI::Style::UITokenResolveStatus::Resolved) {
return false;
}
const float* value = resolution.value.TryGetFloat();
if (value == nullptr) {
return false;
}
outValue = *value;
return true;
}
inline bool TryResolveThemeColor(
const ::XCEngine::UI::Style::UITheme& theme,
std::string_view tokenName,
::XCEngine::UI::UIColor& outColor) {
const auto resolution =
theme.ResolveToken(std::string(tokenName), ::XCEngine::UI::Style::UIStyleValueType::Color);
if (resolution.status != ::XCEngine::UI::Style::UITokenResolveStatus::Resolved) {
return false;
}
const ::XCEngine::Math::Color* value = resolution.value.TryGetColor();
if (value == nullptr) {
return false;
}
outColor = ToUIColor(*value);
return true;
}
inline float ResolveThemeFloatAliases(
const ::XCEngine::UI::Style::UITheme& theme,
std::initializer_list<std::string_view> tokenNames,
float fallbackValue) {
float resolvedValue = fallbackValue;
for (std::string_view tokenName : tokenNames) {
if (TryResolveThemeFloat(theme, tokenName, resolvedValue)) {
return resolvedValue;
}
}
return fallbackValue;
}
inline ::XCEngine::UI::UIColor ResolveThemeColorAliases(
const ::XCEngine::UI::Style::UITheme& theme,
std::initializer_list<std::string_view> tokenNames,
const ::XCEngine::UI::UIColor& fallbackValue) {
::XCEngine::UI::UIColor resolvedValue = fallbackValue;
for (std::string_view tokenName : tokenNames) {
if (TryResolveThemeColor(theme, tokenName, resolvedValue)) {
return resolvedValue;
}
}
return fallbackValue;
}
inline EditorValidationThemeLoadResult LoadEditorValidationTheme(
const std::filesystem::path& themePath) {
EditorValidationThemeLoadResult result = {};
::XCEngine::Resources::UIDocumentCompileResult compileResult = {};
const ::XCEngine::Containers::String pathString(themePath.generic_string().c_str());
if (!::XCEngine::Resources::CompileUIDocument(
::XCEngine::Resources::UIDocumentCompileRequest {
::XCEngine::Resources::UIDocumentKind::Theme,
pathString,
::XCEngine::Resources::GetUIDocumentDefaultRootTag(
::XCEngine::Resources::UIDocumentKind::Theme)
},
compileResult)) {
result.error = compileResult.errorMessage.Empty()
? std::string("Failed to compile editor validation theme document.")
: std::string(compileResult.errorMessage.CStr());
return result;
}
const auto styleCompileResult =
::XCEngine::UI::Style::CompileDocumentStyle(compileResult.document);
if (!styleCompileResult.succeeded) {
result.error = styleCompileResult.errorMessage;
return result;
}
result.theme = styleCompileResult.theme;
result.succeeded = true;
return result;
}
inline EditorValidationShellPalette ResolveEditorValidationShellPalette(
const ::XCEngine::UI::Style::UITheme& theme) {
EditorValidationShellPalette palette = {};
palette.windowBackground = ResolveThemeColorAliases(
theme,
{ "editor.color.validation.window", "color.bg.workspace" },
palette.windowBackground);
palette.cardBackground = ResolveThemeColorAliases(
theme,
{ "editor.color.validation.card", "color.bg.panel" },
palette.cardBackground);
palette.cardBorder = ResolveThemeColorAliases(
theme,
{ "editor.color.validation.card_border", "editor.color.menu_popup.border" },
palette.cardBorder);
palette.textPrimary = ResolveThemeColorAliases(
theme,
{ "editor.color.validation.text_primary", "color.text.primary" },
palette.textPrimary);
palette.textMuted = ResolveThemeColorAliases(
theme,
{ "editor.color.validation.text_muted", "color.text.muted" },
palette.textMuted);
palette.textWeak = ResolveThemeColorAliases(
theme,
{ "editor.color.validation.text_weak", "color.text.muted" },
palette.textWeak);
palette.textSuccess = ResolveThemeColorAliases(
theme,
{ "editor.color.validation.text_success" },
palette.textSuccess);
palette.buttonBackground = ResolveThemeColorAliases(
theme,
{ "editor.color.validation.button", "color.bg.selection" },
palette.buttonBackground);
palette.buttonHoverBackground = ResolveThemeColorAliases(
theme,
{ "editor.color.validation.button_hover", "editor.color.validation.button", "color.bg.selection" },
palette.buttonHoverBackground);
return palette;
}
inline EditorValidationShellMetrics ResolveEditorValidationShellMetrics(
const ::XCEngine::UI::Style::UITheme& theme) {
EditorValidationShellMetrics metrics = {};
metrics.margin = ResolveThemeFloatAliases(
theme,
{ "editor.space.validation.margin", "space.shell" },
metrics.margin);
metrics.gap = ResolveThemeFloatAliases(
theme,
{ "editor.space.validation.gap" },
metrics.gap);
metrics.cardRadius = ResolveThemeFloatAliases(
theme,
{ "editor.radius.validation.card", "radius.panel" },
metrics.cardRadius);
metrics.buttonRadius = ResolveThemeFloatAliases(
theme,
{ "editor.radius.validation.button", "radius.control" },
metrics.buttonRadius);
metrics.titleFontSize = ResolveThemeFloatAliases(
theme,
{ "editor.font.validation.title" },
metrics.titleFontSize);
metrics.bodyFontSize = ResolveThemeFloatAliases(
theme,
{ "editor.font.validation.body", "editor.font.field.value" },
metrics.bodyFontSize);
return metrics;
}
} // namespace XCEngine::Tests::EditorUI

View File

@@ -5,10 +5,156 @@
<Color name="color.bg.selection" value="#3A3A3A" />
<Color name="color.text.primary" value="#F2F2F2" />
<Color name="color.text.muted" value="#B4B4B4" />
<Color name="color.text.weak" value="#8F8F8F" />
<Color name="color.text.success" value="#A6C3A6" />
<Color name="color.border.panel" value="#4A4A4A" />
<Color name="color.button.default" value="#343434" />
<Color name="color.button.hover" value="#404040" />
<Spacing name="space.shell" value="18" />
<Spacing name="space.panel" value="12" />
<Radius name="radius.panel" value="10" />
<Radius name="radius.control" value="8" />
<Spacing name="editor.space.validation.margin" value="20" />
<Spacing name="editor.space.validation.gap" value="16" />
<Radius name="editor.radius.validation.card" value="radius.panel" />
<Radius name="editor.radius.validation.button" value="radius.control" />
<Number name="editor.font.validation.title" value="17" />
<Number name="editor.font.validation.body" value="12" />
<Color name="editor.color.validation.window" value="color.bg.workspace" />
<Color name="editor.color.validation.card" value="color.bg.panel" />
<Color name="editor.color.validation.card_border" value="color.border.panel" />
<Color name="editor.color.validation.text_primary" value="color.text.primary" />
<Color name="editor.color.validation.text_muted" value="color.text.muted" />
<Color name="editor.color.validation.text_weak" value="color.text.weak" />
<Color name="editor.color.validation.text_success" value="color.text.success" />
<Color name="editor.color.validation.button" value="color.button.default" />
<Color name="editor.color.validation.button_hover" value="color.button.hover" />
<Number name="editor.size.field.row" value="22" />
<Number name="editor.space.field.padding_x" value="12" />
<Number name="editor.space.field.label_gap" value="20" />
<Number name="editor.layout.field.control_column" value="236" />
<Number name="editor.space.field.control_trailing_inset" value="8" />
<Number name="editor.size.field.checkbox" value="14" />
<Number name="editor.size.field.control_min_width" value="96" />
<Number name="editor.space.field.vector_component_gap" value="6" />
<Number name="editor.size.field.vector_component_min_width" value="72" />
<Number name="editor.size.field.vector_prefix_width" value="9" />
<Number name="editor.space.field.vector_prefix_gap" value="4" />
<Number name="editor.space.field.vector_prefix_inset_x" value="0" />
<Number name="editor.space.field.vector_prefix_inset_y" value="-1" />
<Number name="editor.space.field.control_inset_y" value="1" />
<Number name="editor.space.field.label_inset_y" value="0" />
<Number name="editor.space.field.value_inset_x" value="5" />
<Number name="editor.space.field.value_inset_y" value="0" />
<Number name="editor.space.field.checkbox_glyph_inset_x" value="1" />
<Number name="editor.space.field.checkbox_glyph_inset_y" value="-2" />
<Number name="editor.size.field.dropdown_arrow_width" value="16" />
<Number name="editor.space.field.dropdown_arrow_inset_x" value="4" />
<Number name="editor.space.field.dropdown_arrow_inset_y" value="3" />
<Number name="editor.radius.field.row" value="0" />
<Number name="editor.radius.field.control" value="2" />
<Number name="editor.border.field" value="1" />
<Number name="editor.border.field.focus" value="1" />
<Number name="editor.font.field.label" value="11" />
<Number name="editor.font.field.value" value="12" />
<Number name="editor.font.field.glyph" value="10" />
<Color name="editor.color.field.row" value="#00000000" />
<Color name="editor.color.field.row_hover" value="#2A2A2AFF" />
<Color name="editor.color.field.row_active" value="#313131FF" />
<Color name="editor.color.field.border" value="#00000000" />
<Color name="editor.color.field.border_focus" value="#00000000" />
<Color name="editor.color.field.label" value="#C9C9C9FF" />
<Color name="editor.color.field.value" value="#EEEEEEFF" />
<Color name="editor.color.field.value_readonly" value="#8E8E8EFF" />
<Color name="editor.color.field.control" value="#2E2E2EFF" />
<Color name="editor.color.field.control_hover" value="#353535FF" />
<Color name="editor.color.field.control_editing" value="#3A3A3AFF" />
<Color name="editor.color.field.control_readonly" value="#252525FF" />
<Color name="editor.color.field.control_border" value="#242424FF" />
<Color name="editor.color.field.control_border_focus" value="#2A2A2AFF" />
<Color name="editor.color.field.vector_prefix" value="#222222FF" />
<Color name="editor.color.field.vector_prefix_border" value="#343434FF" />
<Color name="editor.color.field.vector_axis_x" value="#A8A8A8FF" />
<Color name="editor.color.field.vector_axis_y" value="#A8A8A8FF" />
<Color name="editor.color.field.vector_axis_z" value="#A8A8A8FF" />
<Color name="editor.color.field.checkbox" value="#1C1C1CFF" />
<Color name="editor.color.field.checkbox_hover" value="#202020FF" />
<Color name="editor.color.field.checkbox_border" value="#343434FF" />
<Color name="editor.color.field.checkbox_mark" value="#D8D8D8FF" />
<Color name="editor.color.field.dropdown_arrow" value="#D0D0D0FF" />
<Number name="editor.space.property.content_inset" value="6" />
<Number name="editor.space.property.section_gap" value="4" />
<Number name="editor.size.property.section_header" value="24" />
<Number name="editor.size.property.field_row" value="24" />
<Number name="editor.space.property.row_gap" value="1" />
<Number name="editor.size.property.disclosure" value="10" />
<Number name="editor.space.property.disclosure_label_gap" value="6" />
<Number name="editor.space.property.section_inset_y" value="5" />
<Number name="editor.space.property.disclosure_glyph_inset_x" value="1" />
<Number name="editor.space.property.disclosure_glyph_inset_y" value="-1" />
<Number name="editor.space.property.label_inset_y" value="5" />
<Number name="editor.space.property.value_inset_y" value="4" />
<Number name="editor.space.property.value_box_inset_y" value="2" />
<Number name="editor.space.property.value_box_inset_x" value="6" />
<Number name="editor.radius.property.panel" value="0" />
<Number name="editor.radius.property.value" value="2" />
<Number name="editor.border.property" value="1" />
<Number name="editor.border.property.focus" value="1" />
<Number name="editor.border.property.edit" value="1" />
<Number name="editor.font.property.section" value="11" />
<Number name="editor.font.property.disclosure" value="10" />
<Number name="editor.font.property.label" value="11" />
<Number name="editor.font.property.value" value="12" />
<Number name="editor.font.property.tag" value="10" />
<Color name="editor.color.property.surface" value="#232323FF" />
<Color name="editor.color.property.border" value="#171717FF" />
<Color name="editor.color.property.border_focus" value="#717171FF" />
<Color name="editor.color.property.section" value="#2B2B2BFF" />
<Color name="editor.color.property.section_hover" value="#313131FF" />
<Color name="editor.color.property.field_hover" value="#262626FF" />
<Color name="editor.color.property.field_selected" value="#303030FF" />
<Color name="editor.color.property.field_selected_focused" value="#393939FF" />
<Color name="editor.color.property.value" value="#1C1C1CFF" />
<Color name="editor.color.property.value_hover" value="#222222FF" />
<Color name="editor.color.property.value_editing" value="#292929FF" />
<Color name="editor.color.property.value_readonly" value="#171717FF" />
<Color name="editor.color.property.value_border" value="#343434FF" />
<Color name="editor.color.property.value_border_editing" value="#767676FF" />
<Color name="editor.color.property.disclosure" value="#C9C9C9FF" />
<Color name="editor.color.property.section_text" value="#E2E2E2FF" />
<Color name="editor.color.property.label" value="#CDCDCDFF" />
<Color name="editor.color.property.value_text" value="#EFEFEFFF" />
<Color name="editor.color.property.value_text_readonly" value="#8E8E8EFF" />
<Color name="editor.color.property.edit_tag" value="#82A7DAFF" />
<Number name="editor.space.menu_popup.padding_x" value="6" />
<Number name="editor.space.menu_popup.padding_y" value="5" />
<Number name="editor.size.menu_popup.item" value="24" />
<Number name="editor.size.menu_popup.separator" value="8" />
<Number name="editor.size.menu_popup.check_column" value="16" />
<Number name="editor.space.menu_popup.shortcut_gap" value="18" />
<Number name="editor.size.menu_popup.submenu_indicator" value="12" />
<Number name="editor.radius.menu_popup.row" value="3" />
<Number name="editor.radius.menu_popup.surface" value="4" />
<Number name="editor.space.menu_popup.label_inset_x" value="10" />
<Number name="editor.space.menu_popup.label_inset_y" value="-1" />
<Number name="editor.font.menu_popup.label" value="11" />
<Number name="editor.space.menu_popup.shortcut_inset_right" value="18" />
<Number name="editor.size.menu_popup.estimated_glyph_width" value="6" />
<Number name="editor.font.menu_popup.glyph" value="10" />
<Number name="editor.border.menu_popup.separator" value="1" />
<Number name="editor.border.menu_popup.surface" value="1" />
<Color name="editor.color.menu_popup.surface" value="#242424FF" />
<Color name="editor.color.menu_popup.border" value="#343434FF" />
<Color name="editor.color.menu_popup.item_hover" value="#2C2C2CFF" />
<Color name="editor.color.menu_popup.item_open" value="#313131FF" />
<Color name="editor.color.menu_popup.separator" value="#3A3A3AFF" />
<Color name="editor.color.menu_popup.label" value="#E8E8E8FF" />
<Color name="editor.color.menu_popup.text_muted" value="#B9B9B9FF" />
<Color name="editor.color.menu_popup.text_disabled" value="#757575FF" />
<Color name="editor.color.menu_popup.glyph" value="#D0D0D0FF" />
</Tokens>
<Widgets>

View File

@@ -25,6 +25,15 @@ endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/number_field_basic/CMakeLists.txt")
add_subdirectory(number_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}/enum_field_basic/CMakeLists.txt")
add_subdirectory(enum_field_basic)
endif()

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

View File

@@ -3,6 +3,7 @@ add_executable(editor_ui_enum_field_basic_validation WIN32
)
target_include_directories(editor_ui_enum_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,10 @@
#endif
#include <XCEditor/Core/UIEditorEnumFieldInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorEnumField.h>
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
@@ -41,24 +44,18 @@ 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;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorEnumFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | EnumField 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
@@ -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 &&
@@ -96,34 +98,41 @@ bool ContainsPoint(const UIRect& rect, float x, float y) {
std::int32_t MapEnumFieldKey(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_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) {
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 = 440.0f;
const float gap = shellMetrics.gap;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 220.0f);
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)(220.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
(std::max)(250.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
layout.previewRect = UIRect(
leftWidth + margin * 2.0f,
margin,
@@ -132,7 +141,7 @@ ScenarioLayout BuildScenarioLayout(float width, float height) {
layout.fieldRect = UIRect(
layout.previewRect.x + 24.0f,
layout.previewRect.y + 82.0f,
320.0f,
340.0f,
32.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
@@ -147,31 +156,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.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 UIEditorEnumFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorEnumFieldHitTargetKind::PreviousButton:
return "previous";
case UIEditorEnumFieldHitTargetKind::NextButton:
return "next";
case UIEditorEnumFieldHitTargetKind::DropdownArrow:
return "dropdown_arrow";
case UIEditorEnumFieldHitTargetKind::ValueBox:
return "value_box";
case UIEditorEnumFieldHitTargetKind::Row:
@@ -361,6 +387,14 @@ private:
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/enum_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;
@@ -386,7 +420,20 @@ 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));
}
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() {
@@ -410,14 +457,19 @@ private:
return;
}
m_spec.selectedIndex = m_selectedIndex;
const ScenarioLayout layout = GetLayout();
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme);
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
m_spec.selectedIndex = m_selectedIndex;
m_frame = UpdateUIEditorEnumFieldInteraction(
m_interactionState,
m_selectedIndex,
layout.fieldRect,
m_spec,
{});
{},
fieldMetrics,
popupMetrics,
GetViewportRect());
m_spec.selectedIndex = m_selectedIndex;
}
@@ -511,13 +563,18 @@ private:
UIEditorEnumFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme);
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
m_spec.selectedIndex = m_selectedIndex;
m_frame = UpdateUIEditorEnumFieldInteraction(
m_interactionState,
m_selectedIndex,
layout.fieldRect,
m_spec,
std::move(events));
std::move(events),
fieldMetrics,
popupMetrics,
GetViewportRect());
m_spec.selectedIndex = m_selectedIndex;
return m_frame.result;
}
@@ -527,6 +584,14 @@ private:
m_lastResult = std::string("已切换到: ") + m_spec.options[m_selectedIndex];
return;
}
if (result.popupOpened) {
m_lastResult = "下拉菜单已展开";
return;
}
if (result.popupClosed) {
m_lastResult = "下拉菜单已关闭";
return;
}
if (result.consumed) {
m_lastResult = "控件已消费输入";
return;
@@ -534,6 +599,16 @@ private:
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:
@@ -551,77 +626,103 @@ private:
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);
const UIRect viewportRect = GetViewportRect();
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
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(m_theme);
const auto enumPalette = XCEngine::UI::Editor::ResolveUIEditorEnumFieldPalette(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("EditorEnumFieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
drawList.AddFilledRect(viewportRect, shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个测试在验证什么功能",
"只验证 Editor EnumField 基础控件,不涉及 PropertyGrid 或业务 Inspector。");
"只验证 Editor EnumField 基础交互契约,不涉及 PropertyGrid 或业务 Inspector。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 < / > 按钮,检查枚举选项是否稳定切换",
kTextPrimary,
12.0f);
"1. 点击 value box 或 dropdown arrow检查下拉菜单是否展开/收起",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 控件获得 focus 后按 Left / Right / Home / End检查键盘切换",
kTextPrimary,
12.0f);
"2. 展开后 hover 列表项,检查高亮是否稳定跟随",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 检查 Hover / Focus / Selected / Result 是否同步更新",
kTextPrimary,
12.0f);
"3. 获得 focus 后按 Up / Down / Home / End再按 Enter / Space 选中Esc 关闭",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 这个场景只覆盖基础交互契约,不提前承载任何业务面板",
kTextPrimary,
12.0f);
"4. 检查 Hover / Popup / Highlight / Selected / Result 是否同步更新",
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, "操作");
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, "状态摘要");
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状态摘要",
"重点检查 hit / popup / highlight / selected / 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);
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),
"Selected: " + ResolveUIEditorEnumFieldValueText(m_spec),
kTextPrimary,
12.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,
kTextPrimary,
12.0f);
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
@@ -630,24 +731,46 @@ private:
? std::string("F12 -> tests/UI/Editor/integration/shell/enum_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
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, "EnumField 预览", "这里只放一个 EnumField。");
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"EnumField 预览",
"这里只放一个 Unity 风格 EnumField。");
AppendUIEditorEnumField(
drawList,
layout.fieldRect,
m_spec,
m_interactionState.fieldState);
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>(width),
static_cast<unsigned int>(height),
static_cast<unsigned int>(viewportRect.width),
static_cast<unsigned int>(viewportRect.height),
framePresented);
}
@@ -660,10 +783,12 @@ private:
std::size_t m_selectedIndex = 0u;
UIEditorEnumFieldInteractionState m_interactionState = {};
UIEditorEnumFieldInteractionFrame 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

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

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

View File

@@ -0,0 +1,31 @@
add_executable(editor_ui_text_field_basic_validation WIN32
main.cpp
)
target_include_directories(editor_ui_text_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
)
target_compile_definitions(editor_ui_text_field_basic_validation PRIVATE
UNICODE
_UNICODE
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
)
if(MSVC)
target_compile_options(editor_ui_text_field_basic_validation PRIVATE /utf-8 /FS)
set_property(TARGET editor_ui_text_field_basic_validation PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
target_link_libraries(editor_ui_text_field_basic_validation PRIVATE
XCUIEditorLib
XCUIEditorHost
)
set_target_properties(editor_ui_text_field_basic_validation PROPERTIES
OUTPUT_NAME "XCUIEditorTextFieldBasicValidation"
)

View File

@@ -0,0 +1,842 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorTextFieldInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorTextField.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/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::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;
namespace Style = XCEngine::UI::Style;
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();
}
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 &&
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, "重置", 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;
}
XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics ResolveHostedTextFieldMetrics(
const Style::UITheme& theme) {
const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(theme);
const auto textMetrics = XCEngine::UI::Editor::ResolveUIEditorTextFieldMetrics(theme);
return XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(propertyMetrics, textMetrics);
}
XCEngine::UI::Editor::Widgets::UIEditorTextFieldPalette ResolveHostedTextFieldPalette(
const Style::UITheme& theme) {
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(theme);
const auto textPalette = XCEngine::UI::Editor::ResolveUIEditorTextFieldPalette(theme);
return XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette(propertyPalette, textPalette);
}
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 = "已请求截图,输出到 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/integration/shell/text_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;
}
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::ResolveEditorValidationShellMetrics(m_theme));
}
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 = "已重置到默认 TextField 状态";
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto metrics = ResolveHostedTextFieldMetrics(m_theme);
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 = ResolveHostedTextFieldMetrics(m_theme);
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("已提交文本: ") + result.committedText;
return;
}
if (result.editCanceled) {
m_lastResult = "已取消编辑";
return;
}
if (result.editStarted) {
m_lastResult = "已进入编辑态";
return;
}
if (result.focusChanged) {
m_lastResult = std::string("焦点变化: ") + (m_interactionState.textFieldState.focused ? "focused" : "lost");
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::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
const UIEditorTextFieldHitTarget currentHit =
HitTestUIEditorTextField(m_frame.layout, m_mousePosition);
const auto textMetrics = ResolveHostedTextFieldMetrics(m_theme);
const auto textPalette = ResolveHostedTextFieldPalette(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorTextFieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个测试验证什么功能",
"验证 UIEditorTextField 的基础编辑交互契约,不涉及 PropertyGrid 或业务 Inspector。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 value box检查是否进入编辑态。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 获得 focus 后按 Enter 开始编辑;直接输入字符也应开始编辑。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 编辑态下按 Enter commit按 Escape cancel。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 编辑态下按 F6 模拟 FocusLost应提交暂存文本并退出编辑态。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 检查 Hover / Focus / Editing / Value / Result 是否同步更新。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. 按 F12 或点击截图按钮,确认自动截图路径正确。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
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,
shellPalette,
shellMetrics,
"状态摘要",
"重点检查 hit / focus / editing / value / 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),
std::string("Focused: ") + (m_interactionState.textFieldState.focused ? "" : ""),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
std::string("Editing: ") + (m_interactionState.textFieldState.editing ? "" : ""),
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()
? "截图排队中..."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/integration/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),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"TextField 预览",
"这里只放一个 Editor TextField用来验证基础字段行为。");
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 = {};
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
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,31 @@
add_executable(editor_ui_vector2_field_basic_validation WIN32
main.cpp
)
target_include_directories(editor_ui_vector2_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
)
target_compile_definitions(editor_ui_vector2_field_basic_validation PRIVATE
UNICODE
_UNICODE
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
)
if(MSVC)
target_compile_options(editor_ui_vector2_field_basic_validation PRIVATE /utf-8 /FS)
set_property(TARGET editor_ui_vector2_field_basic_validation PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
target_link_libraries(editor_ui_vector2_field_basic_validation PRIVATE
XCUIEditorLib
XCUIEditorHost
)
set_target_properties(editor_ui_vector2_field_basic_validation PROPERTIES
OUTPUT_NAME "XCUIEditorVector2FieldBasicValidation"
)

View File

@@ -0,0 +1,896 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Core/UIEditorVector2FieldInteraction.h>
#include <XCEditor/Widgets/UIEditorVector2Field.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/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;
namespace Style = XCEngine::UI::Style;
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();
}
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 &&
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, "重置", 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/integration/shell/vector2_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;
}
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::ResolveEditorValidationShellMetrics(m_theme));
}
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 = "已重置到默认 Vector2Field 状态";
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme);
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_theme);
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 = "提交失败,当前文本不是合法数字";
return;
}
if (result.editCommitted) {
m_lastResult =
std::string("已提交 ") +
DescribeSelectedComponent(result.changedComponentIndex) +
" = " + result.committedText;
return;
}
if (result.editCanceled) {
m_lastResult = "已取消编辑";
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("已切换选中 component: ") +
DescribeSelectedComponent(result.selectedComponentIndex);
return;
}
if (result.focusChanged) {
m_lastResult =
std::string("焦点变化: ") +
(m_interactionState.vector2FieldState.focused ? "focused" : "lost");
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::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
const UIEditorVector2FieldHitTarget currentHit =
HitTestUIEditorVector2Field(m_frame.layout, m_mousePosition);
const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme);
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector2FieldPalette(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector2FieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个测试验证什么功能",
"验证 UIEditorVector2Field 的双通道数值编辑契约,不涉及 PropertyGrid 或业务 Inspector。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 X / Y 对应的 value box检查 selected component 是否切换,并且应进入编辑态。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 获得 focus 后按 Tab检查 selected component 在 X / Y 之间切换。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 非编辑态按 Up / Down / Home / End检查当前 component 的 step / 边界行为。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 按 Enter 开始编辑直接输入字符也应开始编辑Enter commitEscape cancel。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 编辑态按 F6 模拟 FocusLost应提交暂存文本并退出编辑态。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. 检查 Hover / Selected / Editing / Values / Result 是否同步更新。",
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, "操作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状态摘要",
"重点检查 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 ? "" : ""),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
std::string("Editing: ") + (m_interactionState.vector2FieldState.editing ? "" : ""),
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/integration/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);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"Vector2Field 预览",
"这里只放一个 Unity 风格的双通道 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 = {};
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
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
ScenarioApp app = {};
return app.Run(hInstance, nCmdShow);
}

View File

@@ -0,0 +1,31 @@
add_executable(editor_ui_vector3_field_basic_validation WIN32
main.cpp
)
target_include_directories(editor_ui_vector3_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
)
target_compile_definitions(editor_ui_vector3_field_basic_validation PRIVATE
UNICODE
_UNICODE
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
)
if(MSVC)
target_compile_options(editor_ui_vector3_field_basic_validation PRIVATE /utf-8 /FS)
set_property(TARGET editor_ui_vector3_field_basic_validation PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
target_link_libraries(editor_ui_vector3_field_basic_validation PRIVATE
XCUIEditorLib
XCUIEditorHost
)
set_target_properties(editor_ui_vector3_field_basic_validation PROPERTIES
OUTPUT_NAME "XCUIEditorVector3FieldBasicValidation"
)

View File

@@ -0,0 +1,904 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Core/UIEditorVector3FieldInteraction.h>
#include <XCEditor/Widgets/UIEditorVector3Field.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/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;
namespace Style = XCEngine::UI::Style;
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();
}
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 &&
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, "重置", 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/integration/shell/vector3_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;
}
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::ResolveEditorValidationShellMetrics(m_theme));
}
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 = "已重置到默认 Vector3Field 状态";
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme);
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_theme);
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 = "提交失败,当前文本不是合法数字";
return;
}
if (result.editCommitted) {
m_lastResult =
std::string("已提交 ") +
DescribeSelectedComponent(result.changedComponentIndex) +
" = " + result.committedText;
return;
}
if (result.editCanceled) {
m_lastResult = "已取消编辑";
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("已切换选中 component: ") +
DescribeSelectedComponent(result.selectedComponentIndex);
return;
}
if (result.focusChanged) {
m_lastResult =
std::string("焦点变化: ") +
(m_interactionState.vector3FieldState.focused ? "focused" : "lost");
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::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
RefreshFrame();
const UIEditorVector3FieldHitTarget currentHit =
HitTestUIEditorVector3Field(m_frame.layout, m_mousePosition);
const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme);
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector3FieldPalette(m_theme);
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector3FieldBasic");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个测试验证什么功能",
"验证 UIEditorVector3Field 的三通道数值编辑契约,不涉及 PropertyGrid 或业务 Inspector。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 X / Y / Z 对应的 value box检查 selected component 是否切换,并应进入编辑态。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 获得 focus 后按 Tab检查 selected component 在 X / Y / Z 之间切换。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 非编辑态按 Up / Down / Home / End检查当前 component 的 step / 边界行为。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 按 Enter 开始编辑直接输入字符也应开始编辑Enter commitEscape cancel。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 编辑态按 F6 模拟 FocusLost应提交暂存文本并退出编辑态。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
"6. 检查 Hover / Selected / Editing / Values / Result 是否同步更新。",
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, "操作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状态摘要",
"重点检查 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 ? "" : ""),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
std::string("Editing: ") + (m_interactionState.vector3FieldState.editing ? "" : ""),
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/integration/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);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"Vector3Field 预览",
"这里只放一个 Unity 风格的三通道 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 = {};
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
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
ScenarioApp app = {};
return app.Run(hInstance, nCmdShow);
}

View File

@@ -17,6 +17,8 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_shell_compose.cpp
test_ui_editor_shell_interaction.cpp
test_ui_editor_collection_primitives.cpp
test_ui_editor_field_row_layout.cpp
test_ui_editor_theme.cpp
test_ui_editor_bool_field.cpp
test_ui_editor_bool_field_interaction.cpp
test_ui_editor_dock_host.cpp
@@ -28,6 +30,12 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_enum_field_interaction.cpp
test_ui_editor_number_field.cpp
test_ui_editor_number_field_interaction.cpp
test_ui_editor_text_field.cpp
test_ui_editor_text_field_interaction.cpp
test_ui_editor_vector2_field.cpp
test_ui_editor_vector2_field_interaction.cpp
test_ui_editor_vector3_field.cpp
test_ui_editor_vector3_field_interaction.cpp
test_ui_editor_scroll_view.cpp
test_ui_editor_scroll_view_interaction.cpp
test_ui_editor_status_bar.cpp

View File

@@ -1,9 +1,12 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEditor/Widgets/UIEditorBoolField.h>
namespace {
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIColor;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolFieldBackground;
@@ -16,39 +19,54 @@ using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldState;
TEST(UIEditorBoolFieldTest, LayoutBuildsLabelAndToggleRects) {
UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false };
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
EXPECT_GT(layout.labelRect.width, 0.0f);
EXPECT_FLOAT_EQ(layout.toggleRect.width, 42.0f);
EXPECT_GT(layout.knobRect.x, layout.toggleRect.x);
EXPECT_FLOAT_EQ(layout.controlRect.x, 236.0f);
EXPECT_FLOAT_EQ(layout.checkboxRect.width, 18.0f);
EXPECT_FLOAT_EQ(layout.checkmarkRect.width, layout.checkboxRect.width);
EXPECT_FLOAT_EQ(layout.checkboxRect.y, 2.0f);
}
TEST(UIEditorBoolFieldTest, HitTestResolvesToggleAndRow) {
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
const auto toggleHit = HitTestUIEditorBoolField(
const auto checkboxHit = HitTestUIEditorBoolField(
layout,
UIPoint(layout.toggleRect.x + 4.0f, layout.toggleRect.y + 4.0f));
EXPECT_EQ(toggleHit.kind, UIEditorBoolFieldHitTargetKind::Toggle);
UIPoint(layout.controlRect.x + layout.controlRect.width - 2.0f, layout.controlRect.y + 2.0f));
EXPECT_EQ(checkboxHit.kind, UIEditorBoolFieldHitTargetKind::Checkbox);
const auto rowHit = HitTestUIEditorBoolField(layout, UIPoint(20.0f, 16.0f));
const auto rowHit = HitTestUIEditorBoolField(layout, UIPoint(20.0f, 11.0f));
EXPECT_EQ(rowHit.kind, UIEditorBoolFieldHitTargetKind::Row);
}
TEST(UIEditorBoolFieldTest, BackgroundAndForegroundEmitStableCommands) {
TEST(UIEditorBoolFieldTest, BackgroundAndForegroundEmitCheckboxOnlyChromeAndCenteredText) {
UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false };
UIEditorBoolFieldState state = {};
state.focused = true;
state.hoveredTarget = UIEditorBoolFieldHitTargetKind::Toggle;
state.hoveredTarget = UIEditorBoolFieldHitTargetKind::Checkbox;
XCEngine::UI::UIDrawData drawData = {};
auto& drawList = drawData.EmplaceDrawList("BoolField");
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
AppendUIEditorBoolFieldBackground(drawList, layout, spec, state);
AppendUIEditorBoolFieldForeground(drawList, layout, spec);
ASSERT_GE(drawList.GetCommands().size(), 6u);
const auto& commands = drawList.GetCommands();
ASSERT_EQ(commands.size(), 6u);
EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRect);
EXPECT_EQ(commands[0].rect.x, layout.checkboxRect.x);
EXPECT_EQ(commands[0].rect.y, layout.checkboxRect.y);
EXPECT_EQ(commands[1].type, UIDrawCommandType::RectOutline);
EXPECT_EQ(commands[1].color.r, 0.14f);
EXPECT_EQ(commands[2].type, UIDrawCommandType::PushClipRect);
EXPECT_EQ(commands[3].type, UIDrawCommandType::Text);
EXPECT_FLOAT_EQ(commands[3].position.y, 2.0f);
EXPECT_EQ(commands[4].type, UIDrawCommandType::PopClipRect);
EXPECT_EQ(commands[5].type, UIDrawCommandType::Text);
EXPECT_EQ(commands[5].text, "V");
EXPECT_FLOAT_EQ(commands[5].position.y, 2.0f);
}
} // namespace

View File

@@ -45,7 +45,7 @@ TEST(UIEditorBoolFieldInteractionTest, ClickToggleFlipsValue) {
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{});
const auto toggle = frame.layout.toggleRect;
const auto checkbox = frame.layout.checkboxRect;
frame = UpdateUIEditorBoolFieldInteraction(
state,
@@ -53,8 +53,8 @@ TEST(UIEditorBoolFieldInteractionTest, ClickToggleFlipsValue) {
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{
MakePointer(UIInputEventType::PointerButtonDown, toggle.x + 4.0f, toggle.y + 4.0f, UIPointerButton::Left),
MakePointer(UIInputEventType::PointerButtonUp, toggle.x + 4.0f, toggle.y + 4.0f, UIPointerButton::Left)
MakePointer(UIInputEventType::PointerButtonDown, checkbox.x + 4.0f, checkbox.y + 4.0f, UIPointerButton::Left),
MakePointer(UIInputEventType::PointerButtonUp, checkbox.x + 4.0f, checkbox.y + 4.0f, UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.valueChanged);
@@ -97,13 +97,13 @@ TEST(UIEditorBoolFieldInteractionTest, HoverTracksToggleHitTarget) {
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{});
const auto toggle = frame.layout.toggleRect;
const auto checkbox = frame.layout.checkboxRect;
frame = UpdateUIEditorBoolFieldInteraction(
state,
value,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{ MakePointer(UIInputEventType::PointerMove, toggle.x + 4.0f, toggle.y + 4.0f) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorBoolFieldHitTargetKind::Toggle);
{ MakePointer(UIInputEventType::PointerMove, checkbox.x + 4.0f, checkbox.y + 4.0f) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorBoolFieldHitTargetKind::Checkbox);
}

View File

@@ -1,35 +1,77 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEditor/Widgets/UIEditorEnumField.h>
namespace {
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumFieldBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumFieldForeground;
using XCEngine::UI::Editor::Widgets::BuildUIEditorEnumFieldLayout;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorEnumField;
using XCEngine::UI::Editor::Widgets::ResolveUIEditorEnumFieldValueText;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldState;
TEST(UIEditorEnumFieldTest, ValueTextUsesSelectedOption) {
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
EXPECT_EQ(ResolveUIEditorEnumFieldValueText(spec), "Cutout");
}
TEST(UIEditorEnumFieldTest, HitTestResolvesPreviousNextAndValueBox) {
TEST(UIEditorEnumFieldTest, LayoutKeepsInspectorControlColumnAndUnityArrowWidth) {
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
EXPECT_FLOAT_EQ(layout.controlRect.x, 236.0f);
EXPECT_FLOAT_EQ(layout.valueRect.y, 1.0f);
EXPECT_FLOAT_EQ(layout.valueRect.height, 20.0f);
EXPECT_FLOAT_EQ(layout.arrowRect.width, 20.0f);
}
TEST(UIEditorEnumFieldTest, HitTestResolvesArrowAndValueBox) {
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
EXPECT_EQ(
HitTestUIEditorEnumField(layout, UIPoint(layout.previousRect.x + 2.0f, layout.previousRect.y + 2.0f)).kind,
UIEditorEnumFieldHitTargetKind::PreviousButton);
EXPECT_EQ(
HitTestUIEditorEnumField(layout, UIPoint(layout.nextRect.x + 2.0f, layout.nextRect.y + 2.0f)).kind,
UIEditorEnumFieldHitTargetKind::NextButton);
HitTestUIEditorEnumField(layout, UIPoint(layout.arrowRect.x + 2.0f, layout.arrowRect.y + 2.0f)).kind,
UIEditorEnumFieldHitTargetKind::DropdownArrow);
EXPECT_EQ(
HitTestUIEditorEnumField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
UIEditorEnumFieldHitTargetKind::ValueBox);
}
TEST(UIEditorEnumFieldTest, BackgroundAndForegroundEmitControlOnlyChromeAndCenteredText) {
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
UIEditorEnumFieldState state = {};
state.popupOpen = true;
state.hoveredTarget = UIEditorEnumFieldHitTargetKind::DropdownArrow;
XCEngine::UI::UIDrawData drawData = {};
auto& drawList = drawData.EmplaceDrawList("EnumField");
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
AppendUIEditorEnumFieldBackground(drawList, layout, spec, state);
AppendUIEditorEnumFieldForeground(drawList, layout, spec);
const auto& commands = drawList.GetCommands();
ASSERT_EQ(commands.size(), 9u);
EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRect);
EXPECT_EQ(commands[0].rect.x, layout.valueRect.x);
EXPECT_EQ(commands[1].type, UIDrawCommandType::RectOutline);
EXPECT_EQ(commands[2].type, UIDrawCommandType::PushClipRect);
EXPECT_EQ(commands[3].type, UIDrawCommandType::Text);
EXPECT_FLOAT_EQ(commands[3].position.y, 2.0f);
EXPECT_EQ(commands[4].type, UIDrawCommandType::PopClipRect);
EXPECT_EQ(commands[5].type, UIDrawCommandType::PushClipRect);
EXPECT_EQ(commands[6].type, UIDrawCommandType::Text);
EXPECT_FLOAT_EQ(commands[6].position.y, 1.0f);
EXPECT_EQ(commands[7].type, UIDrawCommandType::PopClipRect);
EXPECT_EQ(commands[8].type, UIDrawCommandType::Text);
EXPECT_EQ(commands[8].text, "V");
EXPECT_FLOAT_EQ(commands[8].position.y, 2.0f);
}
} // namespace

View File

@@ -33,7 +33,7 @@ UIInputEvent MakeKey(KeyCode keyCode) {
} // namespace
TEST(UIEditorEnumFieldInteractionTest, ClickButtonsAdjustSelection) {
TEST(UIEditorEnumFieldInteractionTest, ClickValueBoxOpensPopupAndSelectsItem) {
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
UIEditorEnumFieldInteractionState state = {};
std::size_t selectedIndex = 1u;
@@ -51,14 +51,29 @@ TEST(UIEditorEnumFieldInteractionTest, ClickButtonsAdjustSelection) {
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{
MakePointer(UIInputEventType::PointerButtonDown, frame.layout.nextRect.x + 2.0f, frame.layout.nextRect.y + 2.0f, UIPointerButton::Left),
MakePointer(UIInputEventType::PointerButtonUp, frame.layout.nextRect.x + 2.0f, frame.layout.nextRect.y + 2.0f, UIPointerButton::Left)
MakePointer(UIInputEventType::PointerButtonDown, frame.layout.valueRect.x + 2.0f, frame.layout.valueRect.y + 2.0f, UIPointerButton::Left),
MakePointer(UIInputEventType::PointerButtonUp, frame.layout.valueRect.x + 2.0f, frame.layout.valueRect.y + 2.0f, UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.popupOpened);
EXPECT_TRUE(frame.popupOpen);
ASSERT_FALSE(frame.popupLayout.itemRects.empty());
const auto itemRect = frame.popupLayout.itemRects[2];
frame = UpdateUIEditorEnumFieldInteraction(
state,
selectedIndex,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{
MakePointer(UIInputEventType::PointerButtonDown, itemRect.x + 2.0f, itemRect.y + 2.0f, UIPointerButton::Left),
MakePointer(UIInputEventType::PointerButtonUp, itemRect.x + 2.0f, itemRect.y + 2.0f, UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.selectionChanged);
EXPECT_EQ(selectedIndex, 2u);
EXPECT_FALSE(frame.popupOpen);
}
TEST(UIEditorEnumFieldInteractionTest, KeyboardControlsMoveToEnds) {
TEST(UIEditorEnumFieldInteractionTest, KeyboardCanOpenMoveAndCommitPopupSelection) {
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
UIEditorEnumFieldInteractionState state = {};
state.fieldState.focused = true;
@@ -69,16 +84,17 @@ TEST(UIEditorEnumFieldInteractionTest, KeyboardControlsMoveToEnds) {
selectedIndex,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{ MakeKey(KeyCode::Home) });
EXPECT_TRUE(frame.result.selectionChanged);
EXPECT_EQ(selectedIndex, 0u);
{ MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.popupOpened);
EXPECT_TRUE(frame.popupOpen);
frame = UpdateUIEditorEnumFieldInteraction(
state,
selectedIndex,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{ MakeKey(KeyCode::End) });
{ MakeKey(KeyCode::Down), MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.selectionChanged);
EXPECT_EQ(selectedIndex, 2u);
EXPECT_FALSE(frame.popupOpen);
}

View File

@@ -0,0 +1,52 @@
#include <gtest/gtest.h>
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
namespace {
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Widgets::BuildUIEditorFieldRowLayout;
using XCEngine::UI::Editor::Widgets::UIEditorFieldRowLayoutMetrics;
TEST(UIEditorFieldRowLayoutTest, WideRowsKeepStableControlColumnAnchor) {
UIEditorFieldRowLayoutMetrics metrics = {};
const auto layout = BuildUIEditorFieldRowLayout(
UIRect(10.0f, 20.0f, 392.0f, 22.0f),
96.0f,
metrics);
EXPECT_FLOAT_EQ(layout.bounds.height, 22.0f);
EXPECT_FLOAT_EQ(layout.labelRect.x, 22.0f);
EXPECT_FLOAT_EQ(layout.controlRect.x, 246.0f);
EXPECT_FLOAT_EQ(layout.controlRect.y, 21.0f);
EXPECT_FLOAT_EQ(layout.controlRect.height, 20.0f);
}
TEST(UIEditorFieldRowLayoutTest, NarrowRowsCompressFromRightWithoutMagicRatioFallback) {
UIEditorFieldRowLayoutMetrics metrics = {};
const auto layout = BuildUIEditorFieldRowLayout(
UIRect(10.0f, 20.0f, 280.0f, 22.0f),
96.0f,
metrics);
EXPECT_FLOAT_EQ(layout.controlRect.x, 186.0f);
EXPECT_FLOAT_EQ(layout.controlRect.width, 96.0f);
EXPECT_FLOAT_EQ(layout.labelRect.width, 144.0f);
}
TEST(UIEditorFieldRowLayoutTest, ZeroHeightFallsBackToMetricRowHeight) {
UIEditorFieldRowLayoutMetrics metrics = {};
metrics.rowHeight = 24.0f;
metrics.controlInsetY = 2.0f;
const auto layout = BuildUIEditorFieldRowLayout(
UIRect(0.0f, 0.0f, 360.0f, 0.0f),
120.0f,
metrics);
EXPECT_FLOAT_EQ(layout.bounds.height, 24.0f);
EXPECT_FLOAT_EQ(layout.controlRect.y, 2.0f);
EXPECT_FLOAT_EQ(layout.controlRect.height, 20.0f);
}
} // namespace

View File

@@ -20,30 +20,26 @@ TEST(UIEditorNumberFieldTest, FormatSupportsIntegerAndFloatMode) {
EXPECT_EQ(FormatUIEditorNumberFieldValue(floatSpec), "1.25");
}
TEST(UIEditorNumberFieldTest, LayoutBuildsValueAndStepperRects) {
TEST(UIEditorNumberFieldTest, LayoutBuildsValueRectWithoutStepperButtons) {
UIEditorNumberFieldSpec spec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
const auto layout = BuildUIEditorNumberFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
EXPECT_GT(layout.labelRect.width, 0.0f);
EXPECT_GT(layout.controlRect.width, 0.0f);
EXPECT_GT(layout.valueRect.width, 0.0f);
EXPECT_FLOAT_EQ(layout.decrementRect.width, 22.0f);
EXPECT_FLOAT_EQ(layout.incrementRect.width, 22.0f);
EXPECT_FLOAT_EQ(layout.controlRect.width, layout.valueRect.width);
}
TEST(UIEditorNumberFieldTest, HitTestResolvesButtonsAndValueBox) {
TEST(UIEditorNumberFieldTest, HitTestResolvesValueBoxAndRow) {
UIEditorNumberFieldSpec spec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
const auto layout = BuildUIEditorNumberFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
EXPECT_EQ(
HitTestUIEditorNumberField(layout, UIPoint(layout.decrementRect.x + 2.0f, layout.decrementRect.y + 2.0f)).kind,
UIEditorNumberFieldHitTargetKind::DecrementButton);
EXPECT_EQ(
HitTestUIEditorNumberField(layout, UIPoint(layout.incrementRect.x + 2.0f, layout.incrementRect.y + 2.0f)).kind,
UIEditorNumberFieldHitTargetKind::IncrementButton);
EXPECT_EQ(
HitTestUIEditorNumberField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
UIEditorNumberFieldHitTargetKind::ValueBox);
EXPECT_EQ(
HitTestUIEditorNumberField(layout, UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f)).kind,
UIEditorNumberFieldHitTargetKind::Row);
}
} // namespace

View File

@@ -40,7 +40,7 @@ UIInputEvent MakeCharacter(char character) {
} // namespace
TEST(UIEditorNumberFieldInteractionTest, ClickStepperButtonsAdjustValue) {
TEST(UIEditorNumberFieldInteractionTest, ClickValueBoxStartsEditing) {
UIEditorNumberFieldSpec spec = { "queue", "Queue", 2.0, 1.0, 0.0, 5.0, true, false };
UIEditorNumberFieldInteractionState state = {};
@@ -57,20 +57,19 @@ TEST(UIEditorNumberFieldInteractionTest, ClickStepperButtonsAdjustValue) {
{
MakePointer(
UIInputEventType::PointerButtonDown,
frame.layout.incrementRect.x + 2.0f,
frame.layout.incrementRect.y + 2.0f,
frame.layout.valueRect.x + 2.0f,
frame.layout.valueRect.y + 2.0f,
UIPointerButton::Left),
MakePointer(
UIInputEventType::PointerButtonUp,
frame.layout.incrementRect.x + 2.0f,
frame.layout.incrementRect.y + 2.0f,
frame.layout.valueRect.x + 2.0f,
frame.layout.valueRect.y + 2.0f,
UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.stepApplied);
EXPECT_TRUE(frame.result.valueChanged);
EXPECT_DOUBLE_EQ(spec.value, 3.0);
EXPECT_DOUBLE_EQ(frame.result.valueAfter, 3.0);
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.numberFieldState.editing);
EXPECT_DOUBLE_EQ(spec.value, 2.0);
}
TEST(UIEditorNumberFieldInteractionTest, KeyboardStepAndBoundsWorkWhenFocused) {

View File

@@ -16,7 +16,9 @@ using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridFieldLocation;
using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridSectionIndex;
using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridVisibleFieldIndex;
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::UIEditorPropertyGridHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridState;
@@ -36,23 +38,80 @@ bool ContainsTextCommand(
return false;
}
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,
bool integerMode = true) {
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 = integerMode;
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 }
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
},
{
"rendering",
"Rendering",
"metadata",
"Metadata",
{
{ "material", "Material", "Metal", false, 0.0f },
{ "guid", "GUID", "asset-guid-001", true, 0.0f }
MakeTextField("guid", "GUID", "asset-guid-001", true)
},
0.0f
}
@@ -68,55 +127,64 @@ UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
TEST(UIEditorPropertyGridTest, FindSectionAndFieldLocationReturnStableIndices) {
const auto sections = BuildSections();
EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "transform"), 0u);
EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "rendering"), 1u);
EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "inspector"), 0u);
EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "metadata"), 1u);
EXPECT_EQ(
FindUIEditorPropertyGridSectionIndex(sections, "missing"),
static_cast<std::size_t>(-1));
const auto materialLocation =
FindUIEditorPropertyGridFieldLocation(sections, "material");
EXPECT_TRUE(materialLocation.IsValid());
EXPECT_EQ(materialLocation.sectionIndex, 1u);
EXPECT_EQ(materialLocation.fieldIndex, 0u);
const auto renderModeLocation =
FindUIEditorPropertyGridFieldLocation(sections, "render_mode");
EXPECT_TRUE(renderModeLocation.IsValid());
EXPECT_EQ(renderModeLocation.sectionIndex, 0u);
EXPECT_EQ(renderModeLocation.fieldIndex, 2u);
const auto missingLocation =
FindUIEditorPropertyGridFieldLocation(sections, "unknown");
EXPECT_FALSE(missingLocation.IsValid());
}
TEST(UIEditorPropertyGridTest, LayoutBuildsSectionHeadersAndVisibleFieldRects) {
TEST(UIEditorPropertyGridTest, LayoutBuildsTypedFieldRectsWithStableColumns) {
const auto sections = BuildSections();
UIExpansionModel expansionModel = {};
expansionModel.Expand("transform");
expansionModel.Expand("inspector");
const auto layout = BuildUIEditorPropertyGridLayout(
UIRect(10.0f, 20.0f, 420.0f, 240.0f),
UIRect(10.0f, 20.0f, 420.0f, 260.0f),
sections,
expansionModel);
ASSERT_EQ(layout.sectionHeaderRects.size(), sections.size());
EXPECT_EQ(layout.visibleFieldIndices.size(), 2u);
ASSERT_EQ(layout.visibleFieldIndices.size(), 4u);
EXPECT_EQ(layout.visibleFieldSectionIndices[0], 0u);
EXPECT_EQ(layout.visibleFieldIndices[1], 1u);
EXPECT_FLOAT_EQ(layout.sectionHeaderRects[0].x, 18.0f);
EXPECT_FLOAT_EQ(layout.sectionHeaderRects[0].y, 28.0f);
EXPECT_FLOAT_EQ(layout.fieldValueRects[0].x, 254.0f);
EXPECT_GT(layout.fieldValueRects[0].width, 0.0f);
EXPECT_EQ(
FindUIEditorPropertyGridVisibleFieldIndex(layout, "position", sections),
FindUIEditorPropertyGridVisibleFieldIndex(layout, "enabled", sections),
0u);
EXPECT_EQ(
FindUIEditorPropertyGridVisibleFieldIndex(layout, "material", sections),
FindUIEditorPropertyGridVisibleFieldIndex(layout, "guid", sections),
static_cast<std::size_t>(-1));
EXPECT_FLOAT_EQ(layout.fieldLabelRects[0].x, layout.fieldRowRects[0].x + 12.0f);
EXPECT_FLOAT_EQ(layout.fieldValueRects[0].x, layout.fieldRowRects[0].x + 236.0f);
EXPECT_FLOAT_EQ(layout.fieldValueRects[0].y, layout.fieldRowRects[0].y);
EXPECT_FLOAT_EQ(layout.fieldValueRects[1].x, layout.fieldRowRects[1].x + 236.0f);
EXPECT_FLOAT_EQ(layout.fieldValueRects[1].y, layout.fieldRowRects[1].y + 4.0f);
EXPECT_FLOAT_EQ(layout.fieldValueRects[2].x, layout.fieldRowRects[2].x + 236.0f);
EXPECT_FLOAT_EQ(layout.fieldValueRects[2].y, layout.fieldRowRects[2].y + 4.0f);
EXPECT_FLOAT_EQ(layout.fieldValueRects[3].x, layout.fieldRowRects[3].x + 236.0f);
EXPECT_FLOAT_EQ(layout.fieldValueRects[3].y, layout.fieldRowRects[3].y + 4.0f);
EXPECT_GT(layout.fieldValueRects[2].width, 0.0f);
EXPECT_GT(layout.fieldValueRects[3].width, 0.0f);
}
TEST(UIEditorPropertyGridTest, HitTestResolvesHeaderRowAndValueBox) {
TEST(UIEditorPropertyGridTest, HitTestResolvesHeaderRowAndTypedValueHosts) {
const auto sections = BuildSections();
UIExpansionModel expansionModel = {};
expansionModel.Expand("transform");
expansionModel.Expand("rendering");
expansionModel.Expand("inspector");
expansionModel.Expand("metadata");
const auto layout = BuildUIEditorPropertyGridLayout(
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
@@ -134,32 +202,46 @@ TEST(UIEditorPropertyGridTest, HitTestResolvesHeaderRowAndValueBox) {
layout.fieldRowRects[1].x + 16.0f,
layout.fieldRowRects[1].y + layout.fieldRowRects[1].height * 0.5f));
EXPECT_EQ(rowHit.kind, UIEditorPropertyGridHitTargetKind::FieldRow);
EXPECT_EQ(rowHit.sectionIndex, 0u);
EXPECT_EQ(rowHit.fieldIndex, 1u);
const auto valueHit =
const auto boolValueHit =
HitTestUIEditorPropertyGrid(layout, RectCenter(layout.fieldValueRects[0]));
EXPECT_EQ(boolValueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
EXPECT_EQ(boolValueHit.fieldIndex, 0u);
const auto boolTrailingHit = HitTestUIEditorPropertyGrid(
layout,
UIPoint(
layout.fieldValueRects[0].x + layout.fieldValueRects[0].width - 6.0f,
layout.fieldValueRects[0].y + layout.fieldValueRects[0].height * 0.5f));
EXPECT_EQ(boolTrailingHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
EXPECT_EQ(boolTrailingHit.fieldIndex, 0u);
const auto enumValueHit =
HitTestUIEditorPropertyGrid(layout, RectCenter(layout.fieldValueRects[2]));
EXPECT_EQ(valueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
EXPECT_EQ(valueHit.sectionIndex, 1u);
EXPECT_EQ(valueHit.fieldIndex, 0u);
EXPECT_EQ(enumValueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
EXPECT_EQ(enumValueHit.fieldIndex, 2u);
}
TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitStableCommands) {
TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitTypedCommandsAndPopupOverlay) {
const auto sections = BuildSections();
UISelectionModel selectionModel = {};
selectionModel.SetSelection("rotation");
selectionModel.SetSelection("render_mode");
UIExpansionModel expansionModel = {};
expansionModel.Expand("transform");
expansionModel.Expand("rendering");
expansionModel.Expand("inspector");
expansionModel.Expand("metadata");
UIPropertyEditModel propertyEditModel = {};
propertyEditModel.BeginEdit("material", "Metal");
propertyEditModel.UpdateStagedValue("Mat_Inst");
propertyEditModel.BeginEdit("tag", "Player");
propertyEditModel.UpdateStagedValue("Hero");
UIEditorPropertyGridState state = {};
state.focused = true;
state.hoveredFieldId = "position";
state.hoveredFieldId = "enabled";
state.hoveredHitTarget = UIEditorPropertyGridHitTargetKind::ValueBox;
state.popupFieldId = "render_mode";
state.popupHighlightedIndex = 1u;
const auto layout = BuildUIEditorPropertyGridLayout(
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
expansionModel);
@@ -176,14 +258,19 @@ TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitStableCommands) {
drawList,
layout,
sections,
state,
propertyEditModel);
const auto& commands = drawList.GetCommands();
ASSERT_GE(commands.size(), 12u);
ASSERT_GE(commands.size(), 16u);
EXPECT_EQ(commands[0].type, XCEngine::UI::UIDrawCommandType::FilledRect);
EXPECT_EQ(commands[1].type, XCEngine::UI::UIDrawCommandType::RectOutline);
EXPECT_TRUE(ContainsTextCommand(drawData, "Transform"));
EXPECT_TRUE(ContainsTextCommand(drawData, "Position"));
EXPECT_TRUE(ContainsTextCommand(drawData, "Mat_Inst"));
EXPECT_TRUE(ContainsTextCommand(drawData, "Inspector"));
EXPECT_TRUE(ContainsTextCommand(drawData, "Enabled"));
EXPECT_TRUE(ContainsTextCommand(drawData, "Render Queue"));
EXPECT_TRUE(ContainsTextCommand(drawData, "Opaque"));
EXPECT_TRUE(ContainsTextCommand(drawData, "Hero"));
EXPECT_TRUE(ContainsTextCommand(drawData, "EDIT"));
EXPECT_TRUE(ContainsTextCommand(drawData, "Cutout"));
EXPECT_EQ(ResolveUIEditorPropertyGridFieldValueText(sections[0].fields[1]), "2000");
}

View File

@@ -17,16 +17,75 @@ using XCEngine::UI::Widgets::UIPropertyEditModel;
using XCEngine::UI::Widgets::UISelectionModel;
using XCEngine::UI::Editor::UIEditorPropertyGridInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorPropertyGridInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind;
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection;
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", "10", false, 0.0f },
{ "rotation", "Rotation", "0", false, 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
},
@@ -34,8 +93,7 @@ std::vector<UIEditorPropertyGridSection> BuildSections() {
"metadata",
"Metadata",
{
{ "tag", "Tag", "", false, 0.0f },
{ "guid", "GUID", "asset-guid-001", true, 0.0f }
MakeTextField("guid", "GUID", "asset-guid-001", true)
},
0.0f
}
@@ -84,14 +142,14 @@ UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
}
void ExpandAll(UIExpansionModel& expansionModel) {
expansionModel.Expand("transform");
expansionModel.Expand("inspector");
expansionModel.Expand("metadata");
}
} // namespace
TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndField) {
const auto sections = BuildSections();
auto sections = BuildSections();
UISelectionModel selectionModel = {};
UIExpansionModel expansionModel = {};
ExpandAll(expansionModel);
@@ -103,7 +161,7 @@ TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndFie
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{});
@@ -112,12 +170,12 @@ TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndFie
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{ MakePointerMove(
initialFrame.layout.sectionHeaderRects[0].x + 24.0f,
initialFrame.layout.sectionHeaderRects[0].y + 16.0f) });
EXPECT_EQ(state.propertyGridState.hoveredSectionId, "transform");
EXPECT_EQ(state.propertyGridState.hoveredSectionId, "inspector");
EXPECT_TRUE(state.propertyGridState.hoveredFieldId.empty());
frame = UpdateUIEditorPropertyGridInteraction(
@@ -125,20 +183,20 @@ TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndFie
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{ MakePointerMove(
initialFrame.layout.fieldRowRects[1].x + 16.0f,
initialFrame.layout.fieldRowRects[1].y + 16.0f) });
EXPECT_EQ(frame.result.hitTarget.fieldIndex, 1u);
EXPECT_EQ(state.propertyGridState.hoveredFieldId, "rotation");
EXPECT_EQ(state.propertyGridState.hoveredFieldId, "render_queue");
}
TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion) {
const auto sections = BuildSections();
auto sections = BuildSections();
UISelectionModel selectionModel = {};
UIExpansionModel expansionModel = {};
expansionModel.Expand("transform");
expansionModel.Expand("inspector");
UIPropertyEditModel propertyEditModel = {};
UIEditorPropertyGridInteractionState state = {};
@@ -147,7 +205,7 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{});
const UIPoint metadataHeaderCenter = RectCenter(initialFrame.layout.sectionHeaderRects[1]);
@@ -157,7 +215,7 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{
MakePointerDown(metadataHeaderCenter.x, metadataHeaderCenter.y),
@@ -170,8 +228,8 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion
EXPECT_TRUE(state.propertyGridState.focused);
}
TEST(UIEditorPropertyGridInteractionTest, LeftClickFieldRowSelectsFieldAndFocusesGrid) {
const auto sections = BuildSections();
TEST(UIEditorPropertyGridInteractionTest, LeftClickBoolValueHostTogglesValueAndSelectsField) {
auto sections = BuildSections();
UISelectionModel selectionModel = {};
UIExpansionModel expansionModel = {};
ExpandAll(expansionModel);
@@ -183,32 +241,34 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickFieldRowSelectsFieldAndFocuse
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{});
const UIPoint rowCenter = RectCenter(initialFrame.layout.fieldRowRects[1]);
const UIPoint boolValueCenter = RectCenter(initialFrame.layout.fieldValueRects[0]);
const auto frame = UpdateUIEditorPropertyGridInteraction(
state,
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{
MakePointerDown(rowCenter.x, rowCenter.y),
MakePointerUp(rowCenter.x, rowCenter.y)
MakePointerDown(boolValueCenter.x, boolValueCenter.y),
MakePointerUp(boolValueCenter.x, boolValueCenter.y)
});
EXPECT_TRUE(frame.result.consumed);
EXPECT_TRUE(frame.result.selectionChanged);
EXPECT_EQ(frame.result.selectedFieldId, "rotation");
EXPECT_TRUE(selectionModel.IsSelected("rotation"));
EXPECT_TRUE(state.propertyGridState.focused);
EXPECT_TRUE(frame.result.fieldValueChanged);
EXPECT_EQ(frame.result.selectedFieldId, "enabled");
EXPECT_EQ(frame.result.changedFieldId, "enabled");
EXPECT_EQ(frame.result.changedValue, "false");
EXPECT_FALSE(sections[0].fields[0].boolValue);
EXPECT_TRUE(selectionModel.IsSelected("enabled"));
}
TEST(UIEditorPropertyGridInteractionTest, ValueBoxEditCanCommitWithEnter) {
const auto sections = BuildSections();
TEST(UIEditorPropertyGridInteractionTest, NumberValueBoxEditCanCommitWithEnter) {
auto sections = BuildSections();
UISelectionModel selectionModel = {};
UIExpansionModel expansionModel = {};
ExpandAll(expansionModel);
@@ -220,25 +280,25 @@ TEST(UIEditorPropertyGridInteractionTest, ValueBoxEditCanCommitWithEnter) {
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{});
const UIPoint tagValueCenter = RectCenter(initialFrame.layout.fieldValueRects[2]);
const UIPoint numberValueCenter = RectCenter(initialFrame.layout.fieldValueRects[1]);
auto frame = UpdateUIEditorPropertyGridInteraction(
state,
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{
MakePointerDown(tagValueCenter.x, tagValueCenter.y),
MakePointerUp(tagValueCenter.x, tagValueCenter.y)
MakePointerDown(numberValueCenter.x, numberValueCenter.y),
MakePointerUp(numberValueCenter.x, numberValueCenter.y)
});
EXPECT_TRUE(frame.result.consumed);
EXPECT_EQ(frame.result.selectedFieldId, "tag");
EXPECT_EQ(frame.result.activeFieldId, "tag");
EXPECT_EQ(frame.result.selectedFieldId, "render_queue");
EXPECT_EQ(frame.result.activeFieldId, "render_queue");
EXPECT_TRUE(propertyEditModel.HasActiveEdit());
frame = UpdateUIEditorPropertyGridInteraction(
@@ -246,22 +306,32 @@ TEST(UIEditorPropertyGridInteractionTest, ValueBoxEditCanCommitWithEnter) {
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{
MakeCharacter('A'),
MakeCharacter('B'),
MakeKeyDown(KeyCode::Backspace),
MakeKeyDown(KeyCode::Backspace),
MakeKeyDown(KeyCode::Backspace),
MakeKeyDown(KeyCode::Backspace),
MakeCharacter('2'),
MakeCharacter('5'),
MakeCharacter('0'),
MakeCharacter('0'),
MakeKeyDown(KeyCode::Enter)
});
EXPECT_TRUE(frame.result.editCommitted);
EXPECT_EQ(frame.result.committedFieldId, "tag");
EXPECT_EQ(frame.result.committedValue, "AB");
EXPECT_TRUE(frame.result.fieldValueChanged);
EXPECT_EQ(frame.result.committedFieldId, "render_queue");
EXPECT_EQ(frame.result.committedValue, "2500");
EXPECT_EQ(frame.result.changedFieldId, "render_queue");
EXPECT_EQ(frame.result.changedValue, "2500");
EXPECT_FALSE(propertyEditModel.HasActiveEdit());
EXPECT_DOUBLE_EQ(sections[0].fields[1].numberValue.value, 2500.0);
}
TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClearsFocus) {
const auto sections = BuildSections();
TEST(UIEditorPropertyGridInteractionTest, EnumPopupCanOpenNavigateSelectAndClose) {
auto sections = BuildSections();
UISelectionModel selectionModel = {};
UIExpansionModel expansionModel = {};
ExpandAll(expansionModel);
@@ -273,17 +343,70 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{});
const UIPoint tagValueCenter = RectCenter(initialFrame.layout.fieldValueRects[2]);
const UIPoint enumValueCenter = RectCenter(initialFrame.layout.fieldValueRects[2]);
auto frame = UpdateUIEditorPropertyGridInteraction(
state,
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{
MakePointerDown(enumValueCenter.x, enumValueCenter.y),
MakePointerUp(enumValueCenter.x, enumValueCenter.y)
});
EXPECT_TRUE(frame.result.popupOpened);
EXPECT_EQ(state.propertyGridState.popupFieldId, "render_mode");
EXPECT_TRUE(selectionModel.IsSelected("render_mode"));
frame = UpdateUIEditorPropertyGridInteraction(
state,
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{
MakeKeyDown(KeyCode::Down),
MakeKeyDown(KeyCode::Enter)
});
EXPECT_TRUE(frame.result.popupClosed);
EXPECT_TRUE(frame.result.fieldValueChanged);
EXPECT_EQ(frame.result.changedFieldId, "render_mode");
EXPECT_EQ(frame.result.changedValue, "Cutout");
EXPECT_TRUE(state.propertyGridState.popupFieldId.empty());
EXPECT_EQ(sections[0].fields[2].enumValue.selectedIndex, 1u);
}
TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClearsFocus) {
auto sections = BuildSections();
UISelectionModel selectionModel = {};
UIExpansionModel expansionModel = {};
ExpandAll(expansionModel);
UIPropertyEditModel propertyEditModel = {};
UIEditorPropertyGridInteractionState state = {};
const auto initialFrame = UpdateUIEditorPropertyGridInteraction(
state,
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{});
const UIPoint tagValueCenter = RectCenter(initialFrame.layout.fieldValueRects[3]);
UpdateUIEditorPropertyGridInteraction(
state,
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{
MakePointerDown(tagValueCenter.x, tagValueCenter.y),
@@ -295,7 +418,7 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{
MakeCharacter('A'),
@@ -303,14 +426,14 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears
});
EXPECT_TRUE(frame.result.editCanceled);
EXPECT_FALSE(propertyEditModel.HasActiveEdit());
EXPECT_TRUE(selectionModel.IsSelected("tag"));
EXPECT_EQ(sections[0].fields[3].valueText, "Player");
frame = UpdateUIEditorPropertyGridInteraction(
state,
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{
MakePointerDown(520.0f, 360.0f),
@@ -321,9 +444,9 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears
}
TEST(UIEditorPropertyGridInteractionTest, ArrowAndHomeEndKeysNavigateVisibleFields) {
const auto sections = BuildSections();
auto sections = BuildSections();
UISelectionModel selectionModel = {};
selectionModel.SetSelection("rotation");
selectionModel.SetSelection("render_queue");
UIExpansionModel expansionModel = {};
ExpandAll(expansionModel);
UIPropertyEditModel propertyEditModel = {};
@@ -335,31 +458,31 @@ TEST(UIEditorPropertyGridInteractionTest, ArrowAndHomeEndKeysNavigateVisibleFiel
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{ MakeKeyDown(KeyCode::Down) });
EXPECT_TRUE(frame.result.keyboardNavigated);
EXPECT_EQ(frame.result.selectedFieldId, "tag");
EXPECT_TRUE(selectionModel.IsSelected("tag"));
EXPECT_EQ(frame.result.selectedFieldId, "render_mode");
EXPECT_TRUE(selectionModel.IsSelected("render_mode"));
frame = UpdateUIEditorPropertyGridInteraction(
state,
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{ MakeKeyDown(KeyCode::Home) });
EXPECT_TRUE(frame.result.keyboardNavigated);
EXPECT_EQ(frame.result.selectedFieldId, "position");
EXPECT_TRUE(selectionModel.IsSelected("position"));
EXPECT_EQ(frame.result.selectedFieldId, "enabled");
EXPECT_TRUE(selectionModel.IsSelected("enabled"));
frame = UpdateUIEditorPropertyGridInteraction(
state,
selectionModel,
expansionModel,
propertyEditModel,
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
sections,
{ MakeKeyDown(KeyCode::End) });
EXPECT_TRUE(frame.result.keyboardNavigated);

View File

@@ -0,0 +1,36 @@
#include <gtest/gtest.h>
#include <XCEditor/Widgets/UIEditorTextField.h>
namespace {
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Widgets::BuildUIEditorTextFieldLayout;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec;
TEST(UIEditorTextFieldTest, LayoutBuildsValueRect) {
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
const auto layout = BuildUIEditorTextFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
EXPECT_GT(layout.labelRect.width, 0.0f);
EXPECT_GT(layout.controlRect.width, 0.0f);
EXPECT_GT(layout.valueRect.width, 0.0f);
EXPECT_FLOAT_EQ(layout.controlRect.width, layout.valueRect.width);
}
TEST(UIEditorTextFieldTest, HitTestResolvesValueBoxAndRow) {
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
const auto layout = BuildUIEditorTextFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
EXPECT_EQ(
HitTestUIEditorTextField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
UIEditorTextFieldHitTargetKind::ValueBox);
EXPECT_EQ(
HitTestUIEditorTextField(layout, UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f)).kind,
UIEditorTextFieldHitTargetKind::Row);
}
} // namespace

View File

@@ -0,0 +1,152 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorTextFieldInteraction.h>
#include <XCEngine/Input/InputTypes.h>
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::UIEditorTextFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorTextFieldInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec;
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = UIPoint(x, y);
event.pointerButton = button;
return event;
}
UIInputEvent MakeKey(KeyCode keyCode) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(keyCode);
return event;
}
UIInputEvent MakeCharacter(char character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = static_cast<std::uint32_t>(character);
return event;
}
} // namespace
TEST(UIEditorTextFieldInteractionTest, ClickValueBoxStartsEditing) {
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
UIEditorTextFieldInteractionState state = {};
auto frame = UpdateUIEditorTextFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{});
frame = UpdateUIEditorTextFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{
MakePointer(
UIInputEventType::PointerButtonDown,
frame.layout.valueRect.x + 2.0f,
frame.layout.valueRect.y + 2.0f,
UIPointerButton::Left),
MakePointer(
UIInputEventType::PointerButtonUp,
frame.layout.valueRect.x + 2.0f,
frame.layout.valueRect.y + 2.0f,
UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.textFieldState.editing);
EXPECT_EQ(spec.value, "Player");
}
TEST(UIEditorTextFieldInteractionTest, EnterStartsEditingAndCommitUpdatesValue) {
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
UIEditorTextFieldInteractionState state = {};
state.textFieldState.focused = true;
auto frame = UpdateUIEditorTextFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.textFieldState.editing);
frame = UpdateUIEditorTextFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeCharacter('X'), MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.editCommitted);
EXPECT_TRUE(frame.result.valueChanged);
EXPECT_FALSE(state.textFieldState.editing);
EXPECT_EQ(spec.value, "PlayerX");
EXPECT_EQ(frame.result.committedText, "PlayerX");
}
TEST(UIEditorTextFieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
UIEditorTextFieldInteractionState state = {};
state.textFieldState.focused = true;
auto frame = UpdateUIEditorTextFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeCharacter('N') });
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.textFieldState.editing);
EXPECT_EQ(state.textFieldState.displayText, "N");
frame = UpdateUIEditorTextFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeKey(KeyCode::Escape) });
EXPECT_TRUE(frame.result.editCanceled);
EXPECT_FALSE(state.textFieldState.editing);
EXPECT_EQ(spec.value, "Player");
EXPECT_EQ(state.textFieldState.displayText, "Player");
}
TEST(UIEditorTextFieldInteractionTest, FocusLostCommitsEdit) {
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
UIEditorTextFieldInteractionState state = {};
state.textFieldState.focused = true;
auto frame = UpdateUIEditorTextFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeKey(KeyCode::Enter), MakeCharacter('1') });
EXPECT_TRUE(state.textFieldState.editing);
frame = UpdateUIEditorTextFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{
UIInputEvent {
.type = UIInputEventType::FocusLost
}
});
EXPECT_TRUE(frame.result.editCommitted);
EXPECT_EQ(spec.value, "Player1");
EXPECT_FALSE(state.textFieldState.editing);
EXPECT_FALSE(state.textFieldState.focused);
}

View File

@@ -0,0 +1,371 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/UI/Style/StyleTypes.h>
#include <XCEngine/UI/Style/Theme.h>
namespace {
namespace Math = XCEngine::Math;
namespace Style = XCEngine::UI::Style;
namespace UI = XCEngine::UI;
namespace Editor = XCEngine::UI::Editor;
Style::UITheme BuildEditorFieldTheme() {
Style::UIThemeDefinition definition = {};
definition.SetToken("editor.size.field.row", Style::UIStyleValue(26.0f));
definition.SetToken("editor.space.field.padding_x", Style::UIStyleValue(8.0f));
definition.SetToken("editor.space.field.label_gap", Style::UIStyleValue(12.0f));
definition.SetToken("editor.layout.field.control_column", Style::UIStyleValue(220.0f));
definition.SetToken("editor.size.field.checkbox", Style::UIStyleValue(15.0f));
definition.SetToken("editor.space.field.control_inset_y", Style::UIStyleValue(3.0f));
definition.SetToken("editor.space.field.label_inset_y", Style::UIStyleValue(5.0f));
definition.SetToken("editor.space.field.value_inset_x", Style::UIStyleValue(6.0f));
definition.SetToken("editor.space.field.value_inset_y", Style::UIStyleValue(4.0f));
definition.SetToken("editor.space.field.checkbox_glyph_inset_x", Style::UIStyleValue(1.0f));
definition.SetToken("editor.space.field.checkbox_glyph_inset_y", Style::UIStyleValue(-2.0f));
definition.SetToken("editor.size.field.control_min_width", Style::UIStyleValue(88.0f));
definition.SetToken("editor.space.field.vector_component_gap", Style::UIStyleValue(6.0f));
definition.SetToken("editor.size.field.vector_component_min_width", Style::UIStyleValue(74.0f));
definition.SetToken("editor.size.field.vector_prefix_width", Style::UIStyleValue(18.0f));
definition.SetToken("editor.space.field.control_trailing_inset", Style::UIStyleValue(9.0f));
definition.SetToken("editor.space.field.vector_prefix_gap", Style::UIStyleValue(5.0f));
definition.SetToken("editor.space.field.vector_prefix_inset_x", Style::UIStyleValue(4.0f));
definition.SetToken("editor.space.field.vector_prefix_inset_y", Style::UIStyleValue(5.0f));
definition.SetToken("editor.size.field.dropdown_arrow_width", Style::UIStyleValue(14.0f));
definition.SetToken("editor.space.field.dropdown_arrow_inset_x", Style::UIStyleValue(4.0f));
definition.SetToken("editor.space.field.dropdown_arrow_inset_y", Style::UIStyleValue(4.0f));
definition.SetToken("editor.radius.field.row", Style::UIStyleValue(2.0f));
definition.SetToken("editor.radius.field.control", Style::UIStyleValue(2.0f));
definition.SetToken("editor.border.field", Style::UIStyleValue(1.0f));
definition.SetToken("editor.border.field.focus", Style::UIStyleValue(2.0f));
definition.SetToken("editor.font.field.label", Style::UIStyleValue(11.0f));
definition.SetToken("editor.font.field.value", Style::UIStyleValue(12.0f));
definition.SetToken("editor.font.field.glyph", Style::UIStyleValue(10.0f));
definition.SetToken("editor.space.menu_popup.padding_x", Style::UIStyleValue(6.0f));
definition.SetToken("editor.space.menu_popup.padding_y", Style::UIStyleValue(5.0f));
definition.SetToken("editor.size.menu_popup.item", Style::UIStyleValue(24.0f));
definition.SetToken("editor.size.menu_popup.separator", Style::UIStyleValue(8.0f));
definition.SetToken("editor.size.menu_popup.check_column", Style::UIStyleValue(16.0f));
definition.SetToken("editor.space.menu_popup.shortcut_gap", Style::UIStyleValue(18.0f));
definition.SetToken("editor.size.menu_popup.submenu_indicator", Style::UIStyleValue(12.0f));
definition.SetToken("editor.radius.menu_popup.row", Style::UIStyleValue(3.0f));
definition.SetToken("editor.radius.menu_popup.surface", Style::UIStyleValue(4.0f));
definition.SetToken("editor.space.menu_popup.label_inset_x", Style::UIStyleValue(10.0f));
definition.SetToken("editor.space.menu_popup.label_inset_y", Style::UIStyleValue(-1.0f));
definition.SetToken("editor.font.menu_popup.label", Style::UIStyleValue(11.0f));
definition.SetToken("editor.space.menu_popup.shortcut_inset_right", Style::UIStyleValue(18.0f));
definition.SetToken("editor.size.menu_popup.estimated_glyph_width", Style::UIStyleValue(6.0f));
definition.SetToken("editor.font.menu_popup.glyph", Style::UIStyleValue(10.0f));
definition.SetToken("editor.border.menu_popup.separator", Style::UIStyleValue(1.0f));
definition.SetToken("editor.border.menu_popup.surface", Style::UIStyleValue(1.0f));
definition.SetToken("editor.color.field.row", Style::UIStyleValue(Math::Color(0.16f, 0.16f, 0.16f, 1.0f)));
definition.SetToken("editor.color.field.row_hover", Style::UIStyleValue(Math::Color(0.20f, 0.20f, 0.20f, 1.0f)));
definition.SetToken("editor.color.field.row_active", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f)));
definition.SetToken("editor.color.field.border", Style::UIStyleValue(Math::Color(0.12f, 0.12f, 0.12f, 1.0f)));
definition.SetToken("editor.color.field.border_focus", Style::UIStyleValue(Math::Color(0.78f, 0.78f, 0.78f, 1.0f)));
definition.SetToken("editor.color.field.label", Style::UIStyleValue(Math::Color(0.84f, 0.84f, 0.84f, 1.0f)));
definition.SetToken("editor.color.field.value", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f)));
definition.SetToken("editor.color.field.value_readonly", Style::UIStyleValue(Math::Color(0.60f, 0.60f, 0.60f, 1.0f)));
definition.SetToken("editor.color.field.control", Style::UIStyleValue(Math::Color(0.18f, 0.18f, 0.18f, 1.0f)));
definition.SetToken("editor.color.field.control_hover", Style::UIStyleValue(Math::Color(0.21f, 0.21f, 0.21f, 1.0f)));
definition.SetToken("editor.color.field.control_editing", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f)));
definition.SetToken("editor.color.field.control_readonly", Style::UIStyleValue(Math::Color(0.15f, 0.15f, 0.15f, 1.0f)));
definition.SetToken("editor.color.field.control_border", Style::UIStyleValue(Math::Color(0.30f, 0.30f, 0.30f, 1.0f)));
definition.SetToken("editor.color.field.control_border_focus", Style::UIStyleValue(Math::Color(0.64f, 0.64f, 0.64f, 1.0f)));
definition.SetToken("editor.color.field.vector_prefix", Style::UIStyleValue(Math::Color(0.20f, 0.20f, 0.20f, 1.0f)));
definition.SetToken("editor.color.field.vector_prefix_border", Style::UIStyleValue(Math::Color(0.31f, 0.31f, 0.31f, 1.0f)));
definition.SetToken("editor.color.field.vector_axis_x", Style::UIStyleValue(Math::Color(0.78f, 0.42f, 0.42f, 1.0f)));
definition.SetToken("editor.color.field.vector_axis_y", Style::UIStyleValue(Math::Color(0.56f, 0.72f, 0.46f, 1.0f)));
definition.SetToken("editor.color.field.vector_axis_z", Style::UIStyleValue(Math::Color(0.45f, 0.62f, 0.82f, 1.0f)));
definition.SetToken("editor.color.field.checkbox", Style::UIStyleValue(Math::Color(0.19f, 0.19f, 0.19f, 1.0f)));
definition.SetToken("editor.color.field.checkbox_hover", Style::UIStyleValue(Math::Color(0.22f, 0.22f, 0.22f, 1.0f)));
definition.SetToken("editor.color.field.checkbox_border", Style::UIStyleValue(Math::Color(0.33f, 0.33f, 0.33f, 1.0f)));
definition.SetToken("editor.color.field.checkbox_mark", Style::UIStyleValue(Math::Color(0.90f, 0.90f, 0.90f, 1.0f)));
definition.SetToken("editor.color.field.dropdown_arrow", Style::UIStyleValue(Math::Color(0.88f, 0.88f, 0.88f, 1.0f)));
definition.SetToken("editor.color.menu_popup.surface", Style::UIStyleValue(Math::Color(0.15f, 0.15f, 0.15f, 1.0f)));
definition.SetToken("editor.color.menu_popup.border", Style::UIStyleValue(Math::Color(0.32f, 0.32f, 0.32f, 1.0f)));
definition.SetToken("editor.color.menu_popup.item_hover", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f)));
definition.SetToken("editor.color.menu_popup.item_open", Style::UIStyleValue(Math::Color(0.27f, 0.27f, 0.27f, 1.0f)));
definition.SetToken("editor.color.menu_popup.separator", Style::UIStyleValue(Math::Color(0.35f, 0.35f, 0.35f, 1.0f)));
definition.SetToken("editor.color.menu_popup.label", Style::UIStyleValue(Math::Color(0.91f, 0.91f, 0.91f, 1.0f)));
definition.SetToken("editor.color.menu_popup.shortcut", Style::UIStyleValue(Math::Color(0.76f, 0.76f, 0.76f, 1.0f)));
definition.SetToken("editor.color.menu_popup.text_disabled", Style::UIStyleValue(Math::Color(0.48f, 0.48f, 0.48f, 1.0f)));
definition.SetToken("editor.color.menu_popup.glyph", Style::UIStyleValue(Math::Color(0.86f, 0.86f, 0.86f, 1.0f)));
return Style::BuildTheme(definition);
}
Style::UITheme BuildPropertyGridTheme() {
Style::UIThemeDefinition definition = {};
definition.SetToken("editor.space.property.content_inset", Style::UIStyleValue(6.0f));
definition.SetToken("editor.space.property.section_gap", Style::UIStyleValue(4.0f));
definition.SetToken("editor.size.property.section_header", Style::UIStyleValue(24.0f));
definition.SetToken("editor.size.property.field_row", Style::UIStyleValue(26.0f));
definition.SetToken("editor.space.property.row_gap", Style::UIStyleValue(1.0f));
definition.SetToken("editor.size.property.disclosure", Style::UIStyleValue(10.0f));
definition.SetToken("editor.space.property.disclosure_label_gap", Style::UIStyleValue(6.0f));
definition.SetToken("editor.space.property.section_inset_y", Style::UIStyleValue(4.0f));
definition.SetToken("editor.space.property.disclosure_glyph_inset_x", Style::UIStyleValue(1.0f));
definition.SetToken("editor.space.property.disclosure_glyph_inset_y", Style::UIStyleValue(-1.0f));
definition.SetToken("editor.space.property.label_inset_y", Style::UIStyleValue(5.0f));
definition.SetToken("editor.space.property.value_inset_y", Style::UIStyleValue(4.0f));
definition.SetToken("editor.space.property.value_box_inset_y", Style::UIStyleValue(3.0f));
definition.SetToken("editor.space.property.value_box_inset_x", Style::UIStyleValue(6.0f));
definition.SetToken("editor.radius.property.panel", Style::UIStyleValue(0.0f));
definition.SetToken("editor.radius.property.value", Style::UIStyleValue(2.0f));
definition.SetToken("editor.border.property", Style::UIStyleValue(1.0f));
definition.SetToken("editor.border.property.focus", Style::UIStyleValue(1.0f));
definition.SetToken("editor.border.property.edit", Style::UIStyleValue(1.0f));
definition.SetToken("editor.font.property.section", Style::UIStyleValue(11.0f));
definition.SetToken("editor.font.property.disclosure", Style::UIStyleValue(10.0f));
definition.SetToken("editor.font.property.label", Style::UIStyleValue(11.0f));
definition.SetToken("editor.font.property.value", Style::UIStyleValue(12.0f));
definition.SetToken("editor.font.property.tag", Style::UIStyleValue(10.0f));
definition.SetToken("editor.color.property.surface", Style::UIStyleValue(Math::Color(0.17f, 0.17f, 0.17f, 1.0f)));
definition.SetToken("editor.color.property.border", Style::UIStyleValue(Math::Color(0.10f, 0.10f, 0.10f, 1.0f)));
definition.SetToken("editor.color.property.border_focus", Style::UIStyleValue(Math::Color(0.75f, 0.75f, 0.75f, 1.0f)));
definition.SetToken("editor.color.property.section", Style::UIStyleValue(Math::Color(0.21f, 0.21f, 0.21f, 1.0f)));
definition.SetToken("editor.color.property.section_hover", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f)));
definition.SetToken("editor.color.property.field_hover", Style::UIStyleValue(Math::Color(0.22f, 0.22f, 0.22f, 1.0f)));
definition.SetToken("editor.color.property.field_selected", Style::UIStyleValue(Math::Color(0.26f, 0.26f, 0.26f, 1.0f)));
definition.SetToken("editor.color.property.field_selected_focused", Style::UIStyleValue(Math::Color(0.30f, 0.30f, 0.30f, 1.0f)));
definition.SetToken("editor.color.property.value", Style::UIStyleValue(Math::Color(0.18f, 0.18f, 0.18f, 1.0f)));
definition.SetToken("editor.color.property.value_hover", Style::UIStyleValue(Math::Color(0.21f, 0.21f, 0.21f, 1.0f)));
definition.SetToken("editor.color.property.value_editing", Style::UIStyleValue(Math::Color(0.23f, 0.23f, 0.23f, 1.0f)));
definition.SetToken("editor.color.property.value_readonly", Style::UIStyleValue(Math::Color(0.15f, 0.15f, 0.15f, 1.0f)));
definition.SetToken("editor.color.property.value_border", Style::UIStyleValue(Math::Color(0.28f, 0.28f, 0.28f, 1.0f)));
definition.SetToken("editor.color.property.value_border_editing", Style::UIStyleValue(Math::Color(0.72f, 0.72f, 0.72f, 1.0f)));
definition.SetToken("editor.color.property.disclosure", Style::UIStyleValue(Math::Color(0.84f, 0.84f, 0.84f, 1.0f)));
definition.SetToken("editor.color.property.section_text", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f)));
definition.SetToken("editor.color.property.label", Style::UIStyleValue(Math::Color(0.80f, 0.80f, 0.80f, 1.0f)));
definition.SetToken("editor.color.property.value_text", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f)));
definition.SetToken("editor.color.property.value_text_readonly", Style::UIStyleValue(Math::Color(0.60f, 0.60f, 0.60f, 1.0f)));
definition.SetToken("editor.color.property.edit_tag", Style::UIStyleValue(Math::Color(0.55f, 0.70f, 0.96f, 1.0f)));
definition.SetToken("editor.space.menu.padding_x", Style::UIStyleValue(7.0f));
definition.SetToken("editor.space.menu.padding_y", Style::UIStyleValue(5.0f));
definition.SetToken("editor.size.menu.item", Style::UIStyleValue(24.0f));
definition.SetToken("editor.size.menu.separator", Style::UIStyleValue(8.0f));
definition.SetToken("editor.size.menu.check_column", Style::UIStyleValue(16.0f));
definition.SetToken("editor.space.menu.shortcut_gap", Style::UIStyleValue(18.0f));
definition.SetToken("editor.size.menu.submenu_indicator", Style::UIStyleValue(12.0f));
definition.SetToken("editor.radius.menu.row", Style::UIStyleValue(3.0f));
definition.SetToken("editor.radius.menu.surface", Style::UIStyleValue(6.0f));
definition.SetToken("editor.space.menu.label_inset_x", Style::UIStyleValue(11.0f));
definition.SetToken("editor.space.menu.label_inset_y", Style::UIStyleValue(1.0f));
definition.SetToken("editor.font.menu.label", Style::UIStyleValue(11.0f));
definition.SetToken("editor.space.menu.shortcut_inset_right", Style::UIStyleValue(18.0f));
definition.SetToken("editor.size.menu.glyph_width", Style::UIStyleValue(6.0f));
definition.SetToken("editor.font.menu.glyph", Style::UIStyleValue(10.0f));
definition.SetToken("editor.border.menu.separator", Style::UIStyleValue(1.0f));
definition.SetToken("editor.border.menu.surface", Style::UIStyleValue(1.0f));
definition.SetToken("editor.color.menu.surface", Style::UIStyleValue(Math::Color(0.16f, 0.16f, 0.16f, 1.0f)));
definition.SetToken("editor.color.menu.border", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f)));
definition.SetToken("editor.color.menu.item_hover", Style::UIStyleValue(Math::Color(0.23f, 0.23f, 0.23f, 1.0f)));
definition.SetToken("editor.color.menu.item_open", Style::UIStyleValue(Math::Color(0.28f, 0.28f, 0.28f, 1.0f)));
definition.SetToken("editor.color.menu.separator", Style::UIStyleValue(Math::Color(0.30f, 0.30f, 0.30f, 1.0f)));
definition.SetToken("editor.color.menu.text", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f)));
definition.SetToken("editor.color.menu.text_muted", Style::UIStyleValue(Math::Color(0.74f, 0.74f, 0.74f, 1.0f)));
definition.SetToken("editor.color.menu.text_disabled", Style::UIStyleValue(Math::Color(0.47f, 0.47f, 0.47f, 1.0f)));
definition.SetToken("editor.color.menu.glyph", Style::UIStyleValue(Math::Color(0.86f, 0.86f, 0.86f, 1.0f)));
return Style::BuildTheme(definition);
}
TEST(UIEditorThemeTest, FieldResolversReadEditorThemeTokens) {
const Style::UITheme theme = BuildEditorFieldTheme();
const auto boolMetrics = Editor::ResolveUIEditorBoolFieldMetrics(theme);
const auto boolPalette = Editor::ResolveUIEditorBoolFieldPalette(theme);
EXPECT_FLOAT_EQ(boolMetrics.rowHeight, 26.0f);
EXPECT_FLOAT_EQ(boolMetrics.horizontalPadding, 8.0f);
EXPECT_FLOAT_EQ(boolMetrics.controlTrailingInset, 9.0f);
EXPECT_FLOAT_EQ(boolMetrics.checkboxGlyphFontSize, 10.0f);
EXPECT_FLOAT_EQ(boolPalette.rowHoverColor.r, 0.20f);
EXPECT_FLOAT_EQ(boolPalette.checkboxBorderColor.r, 0.33f);
const auto numberMetrics = Editor::ResolveUIEditorNumberFieldMetrics(theme);
const auto numberPalette = Editor::ResolveUIEditorNumberFieldPalette(theme);
EXPECT_FLOAT_EQ(numberMetrics.controlTrailingInset, 9.0f);
EXPECT_FLOAT_EQ(numberMetrics.controlInsetY, 3.0f);
EXPECT_FLOAT_EQ(numberMetrics.valueTextInsetX, 6.0f);
EXPECT_FLOAT_EQ(numberMetrics.valueFontSize, 12.0f);
EXPECT_FLOAT_EQ(numberPalette.valueBoxEditingColor.r, 0.24f);
EXPECT_FLOAT_EQ(numberPalette.controlBorderColor.r, 0.30f);
EXPECT_FLOAT_EQ(numberPalette.controlFocusedBorderColor.r, 0.64f);
const auto textMetrics = Editor::ResolveUIEditorTextFieldMetrics(theme);
const auto textPalette = Editor::ResolveUIEditorTextFieldPalette(theme);
EXPECT_FLOAT_EQ(textMetrics.controlTrailingInset, 9.0f);
EXPECT_FLOAT_EQ(textMetrics.valueBoxMinWidth, 88.0f);
EXPECT_FLOAT_EQ(textMetrics.valueTextInsetY, 4.0f);
EXPECT_FLOAT_EQ(textMetrics.valueFontSize, 12.0f);
EXPECT_FLOAT_EQ(textPalette.valueBoxEditingColor.r, 0.24f);
EXPECT_FLOAT_EQ(textPalette.readOnlyValueColor.r, 0.60f);
EXPECT_FLOAT_EQ(textPalette.controlFocusedBorderColor.r, 0.64f);
const auto vector2Metrics = Editor::ResolveUIEditorVector2FieldMetrics(theme);
const auto vector2Palette = Editor::ResolveUIEditorVector2FieldPalette(theme);
EXPECT_FLOAT_EQ(vector2Metrics.componentGap, 6.0f);
EXPECT_FLOAT_EQ(vector2Metrics.componentPrefixWidth, 18.0f);
EXPECT_FLOAT_EQ(vector2Metrics.controlTrailingInset, 9.0f);
EXPECT_FLOAT_EQ(vector2Metrics.componentLabelGap, 5.0f);
EXPECT_FLOAT_EQ(vector2Metrics.prefixFontSize, 10.0f);
EXPECT_FLOAT_EQ(vector2Palette.componentFocusedBorderColor.r, 0.64f);
EXPECT_FLOAT_EQ(vector2Palette.axisXColor.r, 0.78f);
EXPECT_FLOAT_EQ(vector2Palette.axisYColor.g, 0.72f);
const auto vector3Metrics = Editor::ResolveUIEditorVector3FieldMetrics(theme);
const auto vector3Palette = Editor::ResolveUIEditorVector3FieldPalette(theme);
EXPECT_FLOAT_EQ(vector3Metrics.componentGap, 6.0f);
EXPECT_FLOAT_EQ(vector3Metrics.componentPrefixWidth, 18.0f);
EXPECT_FLOAT_EQ(vector3Metrics.controlTrailingInset, 9.0f);
EXPECT_FLOAT_EQ(vector3Metrics.componentLabelGap, 5.0f);
EXPECT_FLOAT_EQ(vector3Metrics.prefixFontSize, 10.0f);
EXPECT_FLOAT_EQ(vector3Palette.componentFocusedBorderColor.r, 0.64f);
EXPECT_FLOAT_EQ(vector3Palette.axisZColor.b, 0.82f);
const auto enumMetrics = Editor::ResolveUIEditorEnumFieldMetrics(theme);
const auto enumPalette = Editor::ResolveUIEditorEnumFieldPalette(theme);
EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 9.0f);
EXPECT_FLOAT_EQ(enumMetrics.valueBoxMinWidth, 88.0f);
EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowWidth, 14.0f);
EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowFontSize, 10.0f);
EXPECT_FLOAT_EQ(enumPalette.arrowColor.r, 0.88f);
const auto popupMetrics = Editor::ResolveUIEditorMenuPopupMetrics(theme);
const auto popupPalette = Editor::ResolveUIEditorMenuPopupPalette(theme);
EXPECT_FLOAT_EQ(popupMetrics.contentPaddingX, 6.0f);
EXPECT_FLOAT_EQ(popupMetrics.itemHeight, 24.0f);
EXPECT_FLOAT_EQ(popupMetrics.glyphFontSize, 10.0f);
EXPECT_FLOAT_EQ(popupPalette.popupColor.r, 0.15f);
EXPECT_FLOAT_EQ(popupPalette.textPrimary.r, 0.91f);
EXPECT_FLOAT_EQ(popupPalette.textMuted.r, 0.76f);
}
TEST(UIEditorThemeTest, PropertyGridResolversSupportOverridesAndFallbacks) {
const Style::UITheme theme = BuildPropertyGridTheme();
const Style::UITheme fallbackTheme = Style::UITheme();
UI::Editor::Widgets::UIEditorPropertyGridMetrics fallbackMetrics = {};
fallbackMetrics.sectionHeaderHeight = 40.0f;
fallbackMetrics.disclosureGlyphFontSize = 15.0f;
fallbackMetrics.tagFontSize = 13.0f;
const auto fallbackResolvedMetrics =
Editor::ResolveUIEditorPropertyGridMetrics(fallbackTheme, fallbackMetrics);
EXPECT_FLOAT_EQ(fallbackResolvedMetrics.sectionHeaderHeight, 40.0f);
EXPECT_FLOAT_EQ(fallbackResolvedMetrics.disclosureGlyphFontSize, 15.0f);
EXPECT_FLOAT_EQ(fallbackResolvedMetrics.tagFontSize, 13.0f);
UI::Editor::Widgets::UIEditorPropertyGridPalette fallbackPalette = {};
fallbackPalette.surfaceColor = UI::UIColor(0.5f, 0.4f, 0.3f, 1.0f);
const auto fallbackResolvedPalette =
Editor::ResolveUIEditorPropertyGridPalette(fallbackTheme, fallbackPalette);
EXPECT_FLOAT_EQ(fallbackResolvedPalette.surfaceColor.r, 0.5f);
const auto themedMetrics = Editor::ResolveUIEditorPropertyGridMetrics(theme);
const auto themedPalette = Editor::ResolveUIEditorPropertyGridPalette(theme);
const auto popupMetrics = Editor::ResolveUIEditorMenuPopupMetrics(theme);
const auto popupPalette = Editor::ResolveUIEditorMenuPopupPalette(theme);
EXPECT_FLOAT_EQ(themedMetrics.contentInset, 6.0f);
EXPECT_FLOAT_EQ(themedMetrics.sectionHeaderHeight, 24.0f);
EXPECT_FLOAT_EQ(themedMetrics.disclosureGlyphFontSize, 10.0f);
EXPECT_FLOAT_EQ(themedMetrics.tagFontSize, 10.0f);
EXPECT_FLOAT_EQ(themedPalette.sectionHeaderColor.r, 0.21f);
EXPECT_FLOAT_EQ(themedPalette.valueBoxEditingBorderColor.r, 0.72f);
EXPECT_FLOAT_EQ(themedPalette.valueTextColor.r, 0.92f);
EXPECT_FLOAT_EQ(themedPalette.readOnlyValueTextColor.r, 0.60f);
EXPECT_FLOAT_EQ(popupMetrics.contentPaddingX, 7.0f);
EXPECT_FLOAT_EQ(popupMetrics.itemHeight, 24.0f);
EXPECT_FLOAT_EQ(popupMetrics.popupCornerRounding, 6.0f);
EXPECT_FLOAT_EQ(popupMetrics.glyphFontSize, 10.0f);
EXPECT_FLOAT_EQ(popupPalette.popupColor.r, 0.16f);
EXPECT_FLOAT_EQ(popupPalette.itemHoverColor.r, 0.23f);
EXPECT_FLOAT_EQ(popupPalette.glyphColor.r, 0.86f);
}
TEST(UIEditorThemeTest, HostedFieldBuildersInheritPropertyGridMetricsAndPalette) {
UI::Editor::Widgets::UIEditorPropertyGridMetrics propertyMetrics = {};
propertyMetrics.fieldRowHeight = 25.0f;
propertyMetrics.horizontalPadding = 7.0f;
propertyMetrics.labelControlGap = 11.0f;
propertyMetrics.controlColumnStart = 210.0f;
propertyMetrics.labelTextInsetY = 4.0f;
propertyMetrics.labelFontSize = 10.0f;
propertyMetrics.valueTextInsetY = 3.0f;
propertyMetrics.valueFontSize = 12.0f;
propertyMetrics.valueBoxInsetY = 2.0f;
propertyMetrics.valueBoxInsetX = 5.0f;
propertyMetrics.cornerRounding = 1.0f;
propertyMetrics.valueBoxRounding = 2.0f;
propertyMetrics.borderThickness = 1.0f;
propertyMetrics.focusedBorderThickness = 2.0f;
UI::Editor::Widgets::UIEditorPropertyGridPalette propertyPalette = {};
propertyPalette.valueBoxColor = UI::UIColor(0.2f, 0.2f, 0.2f, 1.0f);
propertyPalette.valueBoxHoverColor = UI::UIColor(0.3f, 0.3f, 0.3f, 1.0f);
propertyPalette.valueBoxEditingColor = UI::UIColor(0.4f, 0.4f, 0.4f, 1.0f);
propertyPalette.valueBoxReadOnlyColor = UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f);
propertyPalette.valueBoxBorderColor = UI::UIColor(0.5f, 0.5f, 0.5f, 1.0f);
propertyPalette.valueBoxEditingBorderColor = UI::UIColor(0.55f, 0.55f, 0.55f, 1.0f);
propertyPalette.labelTextColor = UI::UIColor(0.8f, 0.8f, 0.8f, 1.0f);
propertyPalette.valueTextColor = UI::UIColor(0.9f, 0.9f, 0.9f, 1.0f);
propertyPalette.readOnlyValueTextColor = UI::UIColor(0.6f, 0.6f, 0.6f, 1.0f);
const auto boolMetrics = Editor::BuildUIEditorHostedBoolFieldMetrics(propertyMetrics);
const auto boolPalette = Editor::BuildUIEditorHostedBoolFieldPalette(propertyPalette);
EXPECT_FLOAT_EQ(boolMetrics.rowHeight, 25.0f);
EXPECT_FLOAT_EQ(boolMetrics.controlTrailingInset, 5.0f);
EXPECT_FLOAT_EQ(boolMetrics.labelFontSize, 10.0f);
EXPECT_FLOAT_EQ(boolMetrics.checkboxGlyphFontSize, 12.0f);
EXPECT_FLOAT_EQ(boolPalette.checkboxColor.r, 0.2f);
EXPECT_FLOAT_EQ(boolPalette.labelColor.r, 0.8f);
const auto numberMetrics = Editor::BuildUIEditorHostedNumberFieldMetrics(propertyMetrics);
const auto numberPalette = Editor::BuildUIEditorHostedNumberFieldPalette(propertyPalette);
EXPECT_FLOAT_EQ(numberMetrics.controlTrailingInset, 5.0f);
EXPECT_FLOAT_EQ(numberMetrics.controlInsetY, 2.0f);
EXPECT_FLOAT_EQ(numberMetrics.valueTextInsetX, 5.0f);
EXPECT_FLOAT_EQ(numberMetrics.valueFontSize, 12.0f);
EXPECT_FLOAT_EQ(numberPalette.valueBoxEditingColor.r, 0.4f);
EXPECT_FLOAT_EQ(numberPalette.readOnlyValueColor.r, 0.6f);
EXPECT_FLOAT_EQ(numberPalette.controlFocusedBorderColor.r, 0.55f);
const auto textMetrics = Editor::BuildUIEditorHostedTextFieldMetrics(propertyMetrics);
const auto textPalette = Editor::BuildUIEditorHostedTextFieldPalette(propertyPalette);
EXPECT_FLOAT_EQ(textMetrics.controlTrailingInset, 5.0f);
EXPECT_FLOAT_EQ(textMetrics.controlInsetY, 2.0f);
EXPECT_FLOAT_EQ(textMetrics.valueTextInsetX, 5.0f);
EXPECT_FLOAT_EQ(textMetrics.valueFontSize, 12.0f);
EXPECT_FLOAT_EQ(textPalette.valueBoxEditingColor.r, 0.4f);
EXPECT_FLOAT_EQ(textPalette.readOnlyValueColor.r, 0.6f);
EXPECT_FLOAT_EQ(textPalette.controlFocusedBorderColor.r, 0.55f);
const auto vector2Metrics = Editor::BuildUIEditorHostedVector2FieldMetrics(propertyMetrics);
const auto vector2Palette = Editor::BuildUIEditorHostedVector2FieldPalette(propertyPalette);
EXPECT_FLOAT_EQ(vector2Metrics.controlInsetY, 2.0f);
EXPECT_FLOAT_EQ(vector2Metrics.controlTrailingInset, 5.0f);
EXPECT_FLOAT_EQ(vector2Metrics.valueTextInsetX, 5.0f);
EXPECT_FLOAT_EQ(vector2Metrics.componentRounding, 2.0f);
EXPECT_FLOAT_EQ(vector2Palette.componentEditingColor.r, 0.4f);
EXPECT_FLOAT_EQ(vector2Palette.componentFocusedBorderColor.r, 0.55f);
const auto vector3Metrics = Editor::BuildUIEditorHostedVector3FieldMetrics(propertyMetrics);
const auto vector3Palette = Editor::BuildUIEditorHostedVector3FieldPalette(propertyPalette);
EXPECT_FLOAT_EQ(vector3Metrics.controlInsetY, 2.0f);
EXPECT_FLOAT_EQ(vector3Metrics.controlTrailingInset, 5.0f);
EXPECT_FLOAT_EQ(vector3Metrics.valueTextInsetX, 5.0f);
EXPECT_FLOAT_EQ(vector3Metrics.componentRounding, 2.0f);
EXPECT_FLOAT_EQ(vector3Palette.componentEditingColor.r, 0.4f);
EXPECT_FLOAT_EQ(vector3Palette.componentFocusedBorderColor.r, 0.55f);
const auto enumMetrics = Editor::BuildUIEditorHostedEnumFieldMetrics(propertyMetrics);
const auto enumPalette = Editor::BuildUIEditorHostedEnumFieldPalette(propertyPalette);
EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 5.0f);
EXPECT_FLOAT_EQ(enumMetrics.controlInsetY, 2.0f);
EXPECT_FLOAT_EQ(enumMetrics.valueTextInsetX, 5.0f);
EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowFontSize, 12.0f);
EXPECT_FLOAT_EQ(enumPalette.arrowColor.r, 0.9f);
}
} // namespace

View File

@@ -0,0 +1,89 @@
#include <gtest/gtest.h>
#include <XCEditor/Widgets/UIEditorVector2Field.h>
namespace {
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Widgets::BuildUIEditorVector2FieldLayout;
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector2FieldComponentValue;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector2Field;
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec;
UIRect MakeInspectorBounds() {
return UIRect(0.0f, 0.0f, 392.0f, 22.0f);
}
TEST(UIEditorVector2FieldTest, FormatSupportsPerComponentDisplay) {
UIEditorVector2FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.25, -3.5 };
EXPECT_EQ(FormatUIEditorVector2FieldComponentValue(spec, 0u), "1.25");
EXPECT_EQ(FormatUIEditorVector2FieldComponentValue(spec, 1u), "-3.5");
}
TEST(UIEditorVector2FieldTest, LayoutBuildsTwoComponentRects) {
UIEditorVector2FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec);
EXPECT_GT(layout.labelRect.width, 0.0f);
EXPECT_GT(layout.controlRect.width, 0.0f);
EXPECT_GT(layout.componentRects[0].width, 0.0f);
EXPECT_GT(layout.componentRects[1].width, 0.0f);
EXPECT_LT(layout.componentRects[0].x + layout.componentRects[0].width, layout.componentRects[1].x);
EXPECT_GT(layout.componentPrefixRects[0].width, 0.0f);
EXPECT_GT(layout.componentValueRects[0].width, 0.0f);
EXPECT_GT(layout.componentValueRects[0].x, layout.componentPrefixRects[0].x + layout.componentPrefixRects[0].width);
EXPECT_GT(layout.componentValueRects[1].x, layout.componentPrefixRects[1].x + layout.componentPrefixRects[1].width);
EXPECT_EQ(layout.componentValueRects[0].height, layout.componentRects[0].height);
EXPECT_EQ(layout.componentValueRects[1].height, layout.componentRects[1].height);
}
TEST(UIEditorVector2FieldTest, HitTestTreatsAxisLabelAreaAsComponentHost) {
UIEditorVector2FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec);
const auto prefixHit = HitTestUIEditorVector2Field(
layout,
UIPoint(
layout.componentPrefixRects[0].x + layout.componentPrefixRects[0].width * 0.5f,
layout.componentPrefixRects[0].y + layout.componentPrefixRects[0].height * 0.5f));
EXPECT_EQ(prefixHit.kind, UIEditorVector2FieldHitTargetKind::Component);
EXPECT_EQ(prefixHit.componentIndex, 0u);
const auto valueHit = HitTestUIEditorVector2Field(
layout,
UIPoint(
layout.componentValueRects[1].x + 4.0f,
layout.componentValueRects[1].y + 4.0f));
EXPECT_EQ(valueHit.kind, UIEditorVector2FieldHitTargetKind::Component);
EXPECT_EQ(valueHit.componentIndex, 1u);
}
TEST(UIEditorVector2FieldTest, HitTestResolvesComponentAndRow) {
UIEditorVector2FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec);
const auto firstHit = HitTestUIEditorVector2Field(
layout,
UIPoint(layout.componentRects[0].x + 4.0f, layout.componentRects[0].y + 4.0f));
EXPECT_EQ(firstHit.kind, UIEditorVector2FieldHitTargetKind::Component);
EXPECT_EQ(firstHit.componentIndex, 0u);
const auto rowHit = HitTestUIEditorVector2Field(
layout,
UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f));
EXPECT_EQ(rowHit.kind, UIEditorVector2FieldHitTargetKind::Row);
}
} // namespace

View File

@@ -0,0 +1,199 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorVector2FieldInteraction.h>
#include <XCEngine/Input/InputTypes.h>
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::UIEditorVector2FieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorVector2FieldInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec;
UIRect MakeInspectorBounds() {
return UIRect(0.0f, 0.0f, 392.0f, 22.0f);
}
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = UIPoint(x, y);
event.pointerButton = button;
return event;
}
UIInputEvent MakeKey(KeyCode keyCode) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(keyCode);
return event;
}
UIInputEvent MakeCharacter(char character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = static_cast<std::uint32_t>(character);
return event;
}
} // namespace
TEST(UIEditorVector2FieldInteractionTest, ClickSecondComponentStartsEditing) {
UIEditorVector2FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.0, 2.0 };
UIEditorVector2FieldInteractionState state = {};
auto frame = UpdateUIEditorVector2FieldInteraction(
state,
spec,
MakeInspectorBounds(),
{});
frame = UpdateUIEditorVector2FieldInteraction(
state,
spec,
MakeInspectorBounds(),
{
MakePointer(
UIInputEventType::PointerButtonDown,
frame.layout.componentRects[1].x + 4.0f,
frame.layout.componentRects[1].y + 4.0f,
UIPointerButton::Left),
MakePointer(
UIInputEventType::PointerButtonUp,
frame.layout.componentRects[1].x + 4.0f,
frame.layout.componentRects[1].y + 4.0f,
UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.vector2FieldState.editing);
EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 1u);
}
TEST(UIEditorVector2FieldInteractionTest, ClickAxisLabelAreaAlsoStartsEditing) {
UIEditorVector2FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.0, 2.0 };
UIEditorVector2FieldInteractionState state = {};
auto frame = UpdateUIEditorVector2FieldInteraction(
state,
spec,
MakeInspectorBounds(),
{});
const float clickX =
frame.layout.componentPrefixRects[0].x + frame.layout.componentPrefixRects[0].width * 0.5f;
const float clickY =
frame.layout.componentPrefixRects[0].y + frame.layout.componentPrefixRects[0].height * 0.5f;
frame = UpdateUIEditorVector2FieldInteraction(
state,
spec,
MakeInspectorBounds(),
{
MakePointer(UIInputEventType::PointerButtonDown, clickX, clickY, UIPointerButton::Left),
MakePointer(UIInputEventType::PointerButtonUp, clickX, clickY, UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.vector2FieldState.editing);
EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 0u);
}
TEST(UIEditorVector2FieldInteractionTest, TabSelectsNextComponentAndArrowAppliesStep) {
UIEditorVector2FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.0, 2.0 };
spec.step = 0.5;
UIEditorVector2FieldInteractionState state = {};
state.vector2FieldState.focused = true;
state.vector2FieldState.selectedComponentIndex = 0u;
auto frame = UpdateUIEditorVector2FieldInteraction(
state,
spec,
MakeInspectorBounds(),
{ MakeKey(KeyCode::Tab) });
EXPECT_TRUE(frame.result.selectionChanged);
EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 1u);
frame = UpdateUIEditorVector2FieldInteraction(
state,
spec,
MakeInspectorBounds(),
{ MakeKey(KeyCode::Up) });
EXPECT_TRUE(frame.result.stepApplied);
EXPECT_EQ(frame.result.changedComponentIndex, 1u);
EXPECT_DOUBLE_EQ(spec.values[1], 2.5);
}
TEST(UIEditorVector2FieldInteractionTest, EnterStartsEditingAndCommitUpdatesSelectedComponent) {
UIEditorVector2FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.0, 2.0 };
spec.integerMode = true;
UIEditorVector2FieldInteractionState state = {};
state.vector2FieldState.focused = true;
state.vector2FieldState.selectedComponentIndex = 0u;
auto frame = UpdateUIEditorVector2FieldInteraction(
state,
spec,
MakeInspectorBounds(),
{ MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.vector2FieldState.editing);
frame = UpdateUIEditorVector2FieldInteraction(
state,
spec,
MakeInspectorBounds(),
{ MakeCharacter('4'), MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.editCommitted);
EXPECT_TRUE(frame.result.valueChanged);
EXPECT_EQ(frame.result.changedComponentIndex, 0u);
EXPECT_DOUBLE_EQ(spec.values[0], 14.0);
EXPECT_DOUBLE_EQ(spec.values[1], 2.0);
}
TEST(UIEditorVector2FieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
UIEditorVector2FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.0, 2.0 };
UIEditorVector2FieldInteractionState state = {};
state.vector2FieldState.focused = true;
state.vector2FieldState.selectedComponentIndex = 1u;
auto frame = UpdateUIEditorVector2FieldInteraction(
state,
spec,
MakeInspectorBounds(),
{ MakeCharacter('9') });
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.vector2FieldState.editing);
EXPECT_EQ(state.vector2FieldState.displayTexts[1], "9");
frame = UpdateUIEditorVector2FieldInteraction(
state,
spec,
MakeInspectorBounds(),
{ MakeKey(KeyCode::Escape) });
EXPECT_TRUE(frame.result.editCanceled);
EXPECT_FALSE(state.vector2FieldState.editing);
EXPECT_DOUBLE_EQ(spec.values[1], 2.0);
}

View File

@@ -0,0 +1,58 @@
#include <gtest/gtest.h>
#include <XCEditor/Widgets/UIEditorVector3Field.h>
namespace {
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Widgets::BuildUIEditorVector3FieldLayout;
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector3FieldComponentValue;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector3Field;
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec;
TEST(UIEditorVector3FieldTest, FormatSupportsPerComponentDisplay) {
UIEditorVector3FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.25, -3.5, 8.0 };
EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 0u), "1.25");
EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 1u), "-3.5");
EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 2u), "8");
}
TEST(UIEditorVector3FieldTest, LayoutBuildsThreeComponentRects) {
UIEditorVector3FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
const auto layout = BuildUIEditorVector3FieldLayout(UIRect(0.0f, 0.0f, 520.0f, 32.0f), spec);
EXPECT_GT(layout.labelRect.width, 0.0f);
EXPECT_GT(layout.componentRects[0].width, 0.0f);
EXPECT_GT(layout.componentRects[1].width, 0.0f);
EXPECT_GT(layout.componentRects[2].width, 0.0f);
EXPECT_LT(layout.componentRects[0].x + layout.componentRects[0].width, layout.componentRects[1].x);
EXPECT_LT(layout.componentRects[1].x + layout.componentRects[1].width, layout.componentRects[2].x);
}
TEST(UIEditorVector3FieldTest, HitTestResolvesComponentAndRow) {
UIEditorVector3FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
const auto layout = BuildUIEditorVector3FieldLayout(UIRect(0.0f, 0.0f, 520.0f, 32.0f), spec);
const auto thirdHit = HitTestUIEditorVector3Field(
layout,
UIPoint(layout.componentRects[2].x + 4.0f, layout.componentRects[2].y + 4.0f));
EXPECT_EQ(thirdHit.kind, UIEditorVector3FieldHitTargetKind::Component);
EXPECT_EQ(thirdHit.componentIndex, 2u);
const auto rowHit = HitTestUIEditorVector3Field(
layout,
UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f));
EXPECT_EQ(rowHit.kind, UIEditorVector3FieldHitTargetKind::Row);
}
} // namespace

View File

@@ -0,0 +1,164 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorVector3FieldInteraction.h>
#include <XCEngine/Input/InputTypes.h>
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::UIEditorVector3FieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorVector3FieldInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec;
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = UIPoint(x, y);
event.pointerButton = button;
return event;
}
UIInputEvent MakeKey(KeyCode keyCode) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(keyCode);
return event;
}
UIInputEvent MakeCharacter(char character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = static_cast<std::uint32_t>(character);
return event;
}
} // namespace
TEST(UIEditorVector3FieldInteractionTest, ClickThirdComponentStartsEditing) {
UIEditorVector3FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.0, 2.0, 3.0 };
UIEditorVector3FieldInteractionState state = {};
auto frame = UpdateUIEditorVector3FieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
{});
frame = UpdateUIEditorVector3FieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
{
MakePointer(
UIInputEventType::PointerButtonDown,
frame.layout.componentRects[2].x + 4.0f,
frame.layout.componentRects[2].y + 4.0f,
UIPointerButton::Left),
MakePointer(
UIInputEventType::PointerButtonUp,
frame.layout.componentRects[2].x + 4.0f,
frame.layout.componentRects[2].y + 4.0f,
UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.vector3FieldState.editing);
EXPECT_EQ(state.vector3FieldState.selectedComponentIndex, 2u);
}
TEST(UIEditorVector3FieldInteractionTest, TabSelectsNextComponentAndArrowAppliesStep) {
UIEditorVector3FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.0, 2.0, 3.0 };
spec.step = 0.5;
UIEditorVector3FieldInteractionState state = {};
state.vector3FieldState.focused = true;
state.vector3FieldState.selectedComponentIndex = 1u;
auto frame = UpdateUIEditorVector3FieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
{ MakeKey(KeyCode::Tab) });
EXPECT_TRUE(frame.result.selectionChanged);
EXPECT_EQ(state.vector3FieldState.selectedComponentIndex, 2u);
frame = UpdateUIEditorVector3FieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
{ MakeKey(KeyCode::Up) });
EXPECT_TRUE(frame.result.stepApplied);
EXPECT_EQ(frame.result.changedComponentIndex, 2u);
EXPECT_DOUBLE_EQ(spec.values[2], 3.5);
}
TEST(UIEditorVector3FieldInteractionTest, EnterStartsEditingAndCommitUpdatesSelectedComponent) {
UIEditorVector3FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.0, 2.0, 3.0 };
spec.integerMode = true;
UIEditorVector3FieldInteractionState state = {};
state.vector3FieldState.focused = true;
state.vector3FieldState.selectedComponentIndex = 1u;
auto frame = UpdateUIEditorVector3FieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
{ MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.vector3FieldState.editing);
frame = UpdateUIEditorVector3FieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
{ MakeCharacter('4'), MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.editCommitted);
EXPECT_TRUE(frame.result.valueChanged);
EXPECT_EQ(frame.result.changedComponentIndex, 1u);
EXPECT_DOUBLE_EQ(spec.values[0], 1.0);
EXPECT_DOUBLE_EQ(spec.values[1], 24.0);
EXPECT_DOUBLE_EQ(spec.values[2], 3.0);
}
TEST(UIEditorVector3FieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
UIEditorVector3FieldSpec spec = {};
spec.fieldId = "position";
spec.label = "Position";
spec.values = { 1.0, 2.0, 3.0 };
UIEditorVector3FieldInteractionState state = {};
state.vector3FieldState.focused = true;
state.vector3FieldState.selectedComponentIndex = 0u;
auto frame = UpdateUIEditorVector3FieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
{ MakeCharacter('9') });
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.vector3FieldState.editing);
EXPECT_EQ(state.vector3FieldState.displayTexts[0], "9");
frame = UpdateUIEditorVector3FieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
{ MakeKey(KeyCode::Escape) });
EXPECT_TRUE(frame.result.editCanceled);
EXPECT_FALSE(state.vector3FieldState.editing);
EXPECT_DOUBLE_EQ(spec.values[0], 1.0);
}