From 7be3b2cc45edb9b27260ac9cc6170276f5de6055 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Wed, 8 Apr 2026 03:23:15 +0800 Subject: [PATCH] ui: add vector4 editor field validation --- new_editor/CMakeLists.txt | 2 + .../include/XCEditor/Core/UIEditorTheme.h | 17 + .../Core/UIEditorVector4FieldInteraction.h | 53 ++ .../XCEditor/Widgets/UIEditorPropertyGrid.h | 45 +- .../XCEditor/Widgets/UIEditorVector4Field.h | 186 ++++++ new_editor/src/Core/UIEditorTheme.cpp | 184 ++++++ .../Core/UIEditorVector4FieldInteraction.cpp | 563 ++++++++++++++++++ .../src/Widgets/UIEditorPropertyGrid.cpp | 195 ++++++ .../src/Widgets/UIEditorVector4Field.cpp | 285 +++++++++ .../Editor/integration/shell/CMakeLists.txt | 3 + .../shell/property_grid_basic/main.cpp | 16 +- .../shell/vector4_field_basic/CMakeLists.txt | 31 + .../vector4_field_basic/captures/.gitkeep | 1 + .../shell/vector4_field_basic/main.cpp | 456 ++++++++++++++ tests/UI/Editor/unit/CMakeLists.txt | 2 + .../unit/test_ui_editor_property_grid.cpp | 63 ++ tests/UI/Editor/unit/test_ui_editor_theme.cpp | 20 + .../unit/test_ui_editor_vector4_field.cpp | 61 ++ ...st_ui_editor_vector4_field_interaction.cpp | 165 +++++ 19 files changed, 2346 insertions(+), 2 deletions(-) create mode 100644 new_editor/include/XCEditor/Core/UIEditorVector4FieldInteraction.h create mode 100644 new_editor/include/XCEditor/Widgets/UIEditorVector4Field.h create mode 100644 new_editor/src/Core/UIEditorVector4FieldInteraction.cpp create mode 100644 new_editor/src/Widgets/UIEditorVector4Field.cpp create mode 100644 tests/UI/Editor/integration/shell/vector4_field_basic/CMakeLists.txt create mode 100644 tests/UI/Editor/integration/shell/vector4_field_basic/captures/.gitkeep create mode 100644 tests/UI/Editor/integration/shell/vector4_field_basic/main.cpp create mode 100644 tests/UI/Editor/unit/test_ui_editor_vector4_field.cpp create mode 100644 tests/UI/Editor/unit/test_ui_editor_vector4_field_interaction.cpp diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index 7a2a139d..b4c6e8bb 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -35,6 +35,7 @@ add_library(XCUIEditorLib STATIC src/Core/UIEditorTreeViewInteraction.cpp src/Core/UIEditorVector2FieldInteraction.cpp src/Core/UIEditorVector3FieldInteraction.cpp + src/Core/UIEditorVector4FieldInteraction.cpp src/Core/UIEditorViewportInputBridge.cpp src/Core/UIEditorViewportShell.cpp src/Core/UIEditorWorkspaceCompose.cpp @@ -61,6 +62,7 @@ add_library(XCUIEditorLib STATIC src/Widgets/UIEditorTreeView.cpp src/Widgets/UIEditorVector2Field.cpp src/Widgets/UIEditorVector3Field.cpp + src/Widgets/UIEditorVector4Field.cpp src/Widgets/UIEditorViewportSlot.cpp ) diff --git a/new_editor/include/XCEditor/Core/UIEditorTheme.h b/new_editor/include/XCEditor/Core/UIEditorTheme.h index 2e92c9c5..281e52ae 100644 --- a/new_editor/include/XCEditor/Core/UIEditorTheme.h +++ b/new_editor/include/XCEditor/Core/UIEditorTheme.h @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -65,6 +66,14 @@ Widgets::UIEditorVector3FieldPalette ResolveUIEditorVector3FieldPalette( const ::XCEngine::UI::Style::UITheme& theme, const Widgets::UIEditorVector3FieldPalette& fallback = {}); +Widgets::UIEditorVector4FieldMetrics ResolveUIEditorVector4FieldMetrics( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorVector4FieldMetrics& fallback = {}); + +Widgets::UIEditorVector4FieldPalette ResolveUIEditorVector4FieldPalette( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorVector4FieldPalette& fallback = {}); + Widgets::UIEditorEnumFieldMetrics ResolveUIEditorEnumFieldMetrics( const ::XCEngine::UI::Style::UITheme& theme, const Widgets::UIEditorEnumFieldMetrics& fallback = {}); @@ -129,6 +138,14 @@ Widgets::UIEditorVector3FieldPalette BuildUIEditorHostedVector3FieldPalette( const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, const Widgets::UIEditorVector3FieldPalette& fallback = {}); +Widgets::UIEditorVector4FieldMetrics BuildUIEditorHostedVector4FieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorVector4FieldMetrics& fallback = {}); + +Widgets::UIEditorVector4FieldPalette BuildUIEditorHostedVector4FieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorVector4FieldPalette& fallback = {}); + Widgets::UIEditorEnumFieldMetrics BuildUIEditorHostedEnumFieldMetrics( const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, const Widgets::UIEditorEnumFieldMetrics& fallback = {}); diff --git a/new_editor/include/XCEditor/Core/UIEditorVector4FieldInteraction.h b/new_editor/include/XCEditor/Core/UIEditorVector4FieldInteraction.h new file mode 100644 index 00000000..6150977d --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorVector4FieldInteraction.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor { + +struct UIEditorVector4FieldInteractionState { + Widgets::UIEditorVector4FieldState vector4FieldState = {}; + ::XCEngine::UI::Text::UITextInputState textInputState = {}; + ::XCEngine::UI::Widgets::UIPropertyEditModel editModel = {}; + ::XCEngine::UI::UIPoint pointerPosition = {}; + bool hasPointerPosition = false; +}; + +struct UIEditorVector4FieldInteractionResult { + bool consumed = false; + bool focusChanged = false; + bool valueChanged = false; + bool stepApplied = false; + bool selectionChanged = false; + bool editStarted = false; + bool editCommitted = false; + bool editCommitRejected = false; + bool editCanceled = false; + Widgets::UIEditorVector4FieldHitTarget hitTarget = {}; + std::size_t selectedComponentIndex = Widgets::UIEditorVector4FieldInvalidComponentIndex; + std::size_t changedComponentIndex = Widgets::UIEditorVector4FieldInvalidComponentIndex; + std::array valuesBefore = { 0.0, 0.0, 0.0, 0.0 }; + std::array valuesAfter = { 0.0, 0.0, 0.0, 0.0 }; + double stepDelta = 0.0; + std::string committedText = {}; +}; + +struct UIEditorVector4FieldInteractionFrame { + Widgets::UIEditorVector4FieldLayout layout = {}; + UIEditorVector4FieldInteractionResult result = {}; +}; + +UIEditorVector4FieldInteractionFrame UpdateUIEditorVector4FieldInteraction( + UIEditorVector4FieldInteractionState& state, + Widgets::UIEditorVector4FieldSpec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorVector4FieldMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Widgets/UIEditorPropertyGrid.h b/new_editor/include/XCEditor/Widgets/UIEditorPropertyGrid.h index b69fcd03..52d5e641 100644 --- a/new_editor/include/XCEditor/Widgets/UIEditorPropertyGrid.h +++ b/new_editor/include/XCEditor/Widgets/UIEditorPropertyGrid.h @@ -7,6 +7,7 @@ #include +#include #include #include #include @@ -21,7 +22,10 @@ enum class UIEditorPropertyGridFieldKind : std::uint8_t { Text = 0, Bool, Number, - Enum + Enum, + Vector2, + Vector3, + Vector4 }; enum class UIEditorPropertyGridHitTargetKind : std::uint8_t { @@ -54,6 +58,42 @@ struct UIEditorPropertyGridEnumFieldValue { std::size_t selectedIndex = 0u; }; +struct UIEditorPropertyGridVector2FieldValue { + std::array values = { 0.0, 0.0 }; + std::array componentLabels = { std::string("X"), std::string("Y") }; + double step = 0.1; + double minValue = -1000000.0; + double maxValue = 1000000.0; + bool integerMode = false; +}; + +struct UIEditorPropertyGridVector3FieldValue { + std::array values = { 0.0, 0.0, 0.0 }; + std::array componentLabels = { + std::string("X"), + std::string("Y"), + std::string("Z") + }; + double step = 0.1; + double minValue = -1000000.0; + double maxValue = 1000000.0; + bool integerMode = false; +}; + +struct UIEditorPropertyGridVector4FieldValue { + std::array values = { 0.0, 0.0, 0.0, 0.0 }; + std::array componentLabels = { + std::string("X"), + std::string("Y"), + std::string("Z"), + std::string("W") + }; + double step = 0.1; + double minValue = -1000000.0; + double maxValue = 1000000.0; + bool integerMode = false; +}; + struct UIEditorPropertyGridField { std::string fieldId = {}; std::string label = {}; @@ -64,6 +104,9 @@ struct UIEditorPropertyGridField { bool boolValue = false; UIEditorPropertyGridNumberFieldValue numberValue = {}; UIEditorPropertyGridEnumFieldValue enumValue = {}; + UIEditorPropertyGridVector2FieldValue vector2Value = {}; + UIEditorPropertyGridVector3FieldValue vector3Value = {}; + UIEditorPropertyGridVector4FieldValue vector4Value = {}; }; struct UIEditorPropertyGridSection { diff --git a/new_editor/include/XCEditor/Widgets/UIEditorVector4Field.h b/new_editor/include/XCEditor/Widgets/UIEditorVector4Field.h new file mode 100644 index 00000000..451258e7 --- /dev/null +++ b/new_editor/include/XCEditor/Widgets/UIEditorVector4Field.h @@ -0,0 +1,186 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Widgets { + +inline constexpr std::size_t UIEditorVector4FieldInvalidComponentIndex = static_cast(-1); + +enum class UIEditorVector4FieldHitTargetKind : std::uint8_t { + None = 0, + Row, + Component +}; + +struct UIEditorVector4FieldSpec { + std::string fieldId = {}; + std::string label = {}; + std::array values = { 0.0, 0.0, 0.0, 0.0 }; + std::array componentLabels = { + std::string("X"), + std::string("Y"), + std::string("Z"), + std::string("W") + }; + double step = 0.1; + double minValue = -1000000.0; + double maxValue = 1000000.0; + bool integerMode = false; + bool readOnly = false; +}; + +struct UIEditorVector4FieldState { + UIEditorVector4FieldHitTargetKind hoveredTarget = UIEditorVector4FieldHitTargetKind::None; + UIEditorVector4FieldHitTargetKind activeTarget = UIEditorVector4FieldHitTargetKind::None; + std::size_t hoveredComponentIndex = UIEditorVector4FieldInvalidComponentIndex; + std::size_t activeComponentIndex = UIEditorVector4FieldInvalidComponentIndex; + std::size_t selectedComponentIndex = UIEditorVector4FieldInvalidComponentIndex; + bool focused = false; + bool editing = false; + std::array displayTexts = { + std::string(), + std::string(), + std::string(), + std::string() + }; +}; + +struct UIEditorVector4FieldMetrics { + float rowHeight = 22.0f; + float horizontalPadding = 12.0f; + float labelControlGap = 20.0f; + float controlColumnStart = 236.0f; + float controlTrailingInset = 8.0f; + float controlInsetY = 1.0f; + float componentGap = 6.0f; + float componentMinWidth = 72.0f; + float componentPrefixWidth = 9.0f; + float componentLabelGap = 4.0f; + float labelTextInsetY = 0.0f; + float labelFontSize = 11.0f; + float valueTextInsetX = 5.0f; + float valueTextInsetY = 0.0f; + float valueFontSize = 12.0f; + float prefixTextInsetX = 0.0f; + float prefixTextInsetY = -1.0f; + float prefixFontSize = 11.0f; + float cornerRounding = 0.0f; + float componentRounding = 2.0f; + float borderThickness = 1.0f; + float focusedBorderThickness = 1.0f; +}; + +struct UIEditorVector4FieldPalette { + ::XCEngine::UI::UIColor surfaceColor = + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); + ::XCEngine::UI::UIColor borderColor = + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); + ::XCEngine::UI::UIColor focusedBorderColor = + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); + ::XCEngine::UI::UIColor rowHoverColor = + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); + ::XCEngine::UI::UIColor rowActiveColor = + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); + ::XCEngine::UI::UIColor componentColor = + ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f); + ::XCEngine::UI::UIColor componentHoverColor = + ::XCEngine::UI::UIColor(0.21f, 0.21f, 0.21f, 1.0f); + ::XCEngine::UI::UIColor componentEditingColor = + ::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f); + ::XCEngine::UI::UIColor readOnlyColor = + ::XCEngine::UI::UIColor(0.17f, 0.17f, 0.17f, 1.0f); + ::XCEngine::UI::UIColor componentBorderColor = + ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); + ::XCEngine::UI::UIColor componentFocusedBorderColor = + ::XCEngine::UI::UIColor(0.20f, 0.20f, 0.20f, 1.0f); + ::XCEngine::UI::UIColor prefixColor = + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); + ::XCEngine::UI::UIColor prefixBorderColor = + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); + ::XCEngine::UI::UIColor labelColor = + ::XCEngine::UI::UIColor(0.80f, 0.80f, 0.80f, 1.0f); + ::XCEngine::UI::UIColor valueColor = + ::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f); + ::XCEngine::UI::UIColor readOnlyValueColor = + ::XCEngine::UI::UIColor(0.62f, 0.62f, 0.62f, 1.0f); + ::XCEngine::UI::UIColor axisXColor = + ::XCEngine::UI::UIColor(0.67f, 0.67f, 0.67f, 1.0f); + ::XCEngine::UI::UIColor axisYColor = + ::XCEngine::UI::UIColor(0.67f, 0.67f, 0.67f, 1.0f); + ::XCEngine::UI::UIColor axisZColor = + ::XCEngine::UI::UIColor(0.67f, 0.67f, 0.67f, 1.0f); + ::XCEngine::UI::UIColor axisWColor = + ::XCEngine::UI::UIColor(0.67f, 0.67f, 0.67f, 1.0f); +}; + +struct UIEditorVector4FieldLayout { + ::XCEngine::UI::UIRect bounds = {}; + ::XCEngine::UI::UIRect labelRect = {}; + ::XCEngine::UI::UIRect controlRect = {}; + std::array<::XCEngine::UI::UIRect, 4u> componentRects = {}; + std::array<::XCEngine::UI::UIRect, 4u> componentPrefixRects = {}; + std::array<::XCEngine::UI::UIRect, 4u> componentValueRects = {}; +}; + +struct UIEditorVector4FieldHitTarget { + UIEditorVector4FieldHitTargetKind kind = UIEditorVector4FieldHitTargetKind::None; + std::size_t componentIndex = UIEditorVector4FieldInvalidComponentIndex; +}; + +bool IsUIEditorVector4FieldPointInside( + const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIPoint& point); + +double NormalizeUIEditorVector4FieldComponentValue( + const UIEditorVector4FieldSpec& spec, + double value); + +bool TryParseUIEditorVector4FieldComponentValue( + const UIEditorVector4FieldSpec& spec, + std::string_view text, + double& outValue); + +std::string FormatUIEditorVector4FieldComponentValue( + const UIEditorVector4FieldSpec& spec, + std::size_t componentIndex); + +UIEditorVector4FieldLayout BuildUIEditorVector4FieldLayout( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorVector4FieldSpec& spec, + const UIEditorVector4FieldMetrics& metrics = {}); + +UIEditorVector4FieldHitTarget HitTestUIEditorVector4Field( + const UIEditorVector4FieldLayout& layout, + const ::XCEngine::UI::UIPoint& point); + +void AppendUIEditorVector4FieldBackground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorVector4FieldLayout& layout, + const UIEditorVector4FieldSpec& spec, + const UIEditorVector4FieldState& state, + const UIEditorVector4FieldPalette& palette = {}, + const UIEditorVector4FieldMetrics& metrics = {}); + +void AppendUIEditorVector4FieldForeground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorVector4FieldLayout& layout, + const UIEditorVector4FieldSpec& spec, + const UIEditorVector4FieldState& state, + const UIEditorVector4FieldPalette& palette = {}, + const UIEditorVector4FieldMetrics& metrics = {}); + +void AppendUIEditorVector4Field( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorVector4FieldSpec& spec, + const UIEditorVector4FieldState& state, + const UIEditorVector4FieldPalette& palette = {}, + const UIEditorVector4FieldMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Core/UIEditorTheme.cpp b/new_editor/src/Core/UIEditorTheme.cpp index 0ba98877..1e1745eb 100644 --- a/new_editor/src/Core/UIEditorTheme.cpp +++ b/new_editor/src/Core/UIEditorTheme.cpp @@ -636,6 +636,145 @@ Widgets::UIEditorVector3FieldPalette ResolveUIEditorVector3FieldPalette( return palette; } +Widgets::UIEditorVector4FieldMetrics ResolveUIEditorVector4FieldMetrics( + const UITheme& theme, + const Widgets::UIEditorVector4FieldMetrics& fallback) { + Widgets::UIEditorVector4FieldMetrics metrics = fallback; + metrics.rowHeight = ResolveUIEditorThemeFloat(theme, "editor.size.field.row", metrics.rowHeight); + metrics.horizontalPadding = + ResolveUIEditorThemeFloat(theme, "editor.space.field.padding_x", metrics.horizontalPadding); + metrics.labelControlGap = + ResolveUIEditorThemeFloat(theme, "editor.space.field.label_gap", metrics.labelControlGap); + metrics.controlColumnStart = + ResolveUIEditorThemeFloat(theme, "editor.layout.field.control_column", metrics.controlColumnStart); + metrics.controlTrailingInset = ResolveThemeFloatAliases( + theme, + { "editor.space.field.control_trailing_inset", "editor.space.field.padding_x" }, + metrics.controlTrailingInset); + metrics.controlInsetY = + ResolveUIEditorThemeFloat(theme, "editor.space.field.control_inset_y", metrics.controlInsetY); + metrics.componentGap = ResolveThemeFloatAliases( + theme, + { "editor.space.field.vector_component_gap", "editor.space.field.control_gap" }, + metrics.componentGap); + metrics.componentMinWidth = ResolveThemeFloatAliases( + theme, + { "editor.size.field.vector_component_min_width", "editor.size.field.control_min_width" }, + metrics.componentMinWidth); + metrics.componentPrefixWidth = ResolveThemeFloatAliases( + theme, + { "editor.size.field.vector_prefix_width", "editor.size.field.inline_prefix_width" }, + metrics.componentPrefixWidth); + metrics.componentLabelGap = ResolveThemeFloatAliases( + theme, + { "editor.space.field.vector_prefix_gap", "editor.space.field.inline_prefix_gap" }, + metrics.componentLabelGap); + metrics.labelTextInsetY = + ResolveUIEditorThemeFloat(theme, "editor.space.field.label_inset_y", metrics.labelTextInsetY); + metrics.labelFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.label", "editor.font.field.label" }, + metrics.labelFontSize); + metrics.valueTextInsetX = + ResolveUIEditorThemeFloat(theme, "editor.space.field.value_inset_x", metrics.valueTextInsetX); + metrics.valueTextInsetY = + ResolveUIEditorThemeFloat(theme, "editor.space.field.value_inset_y", metrics.valueTextInsetY); + metrics.valueFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.value", "editor.font.field.value" }, + metrics.valueFontSize); + metrics.prefixTextInsetX = ResolveThemeFloatAliases( + theme, + { "editor.space.field.vector_prefix_inset_x", "editor.space.field.inline_prefix_inset_x" }, + metrics.prefixTextInsetX); + metrics.prefixTextInsetY = ResolveThemeFloatAliases( + theme, + { "editor.space.field.vector_prefix_inset_y", "editor.space.field.inline_prefix_inset_y" }, + metrics.prefixTextInsetY); + metrics.prefixFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.vector_prefix", "editor.font.field.vector_prefix", "editor.font.field.glyph" }, + metrics.prefixFontSize); + metrics.cornerRounding = + ResolveUIEditorThemeFloat(theme, "editor.radius.field.row", metrics.cornerRounding); + metrics.componentRounding = ResolveThemeFloatAliases( + theme, + { "editor.radius.field.vector_component", "editor.radius.field.control" }, + metrics.componentRounding); + metrics.borderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.field", metrics.borderThickness); + metrics.focusedBorderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.field.focus", metrics.focusedBorderThickness); + return metrics; +} + +Widgets::UIEditorVector4FieldPalette ResolveUIEditorVector4FieldPalette( + const UITheme& theme, + const Widgets::UIEditorVector4FieldPalette& fallback) { + Widgets::UIEditorVector4FieldPalette palette = fallback; + palette.surfaceColor = ResolveUIEditorThemeColor(theme, "editor.color.field.row", palette.surfaceColor); + palette.borderColor = ResolveUIEditorThemeColor(theme, "editor.color.field.border", palette.borderColor); + palette.focusedBorderColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.border_focus", palette.focusedBorderColor); + palette.rowHoverColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.row_hover", palette.rowHoverColor); + palette.rowActiveColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.row_active", palette.rowActiveColor); + palette.componentColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.control", palette.componentColor); + palette.componentHoverColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.control_hover", palette.componentHoverColor); + palette.componentEditingColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_editing", + palette.componentEditingColor); + palette.readOnlyColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_readonly", + palette.readOnlyColor); + palette.componentBorderColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_border", + palette.componentBorderColor); + palette.componentFocusedBorderColor = ResolveThemeColorAliases( + theme, + { "editor.color.field.control_border_focus", "editor.color.field.border_focus" }, + palette.componentFocusedBorderColor); + palette.prefixColor = ResolveThemeColorAliases( + theme, + { "editor.color.field.vector_prefix", "editor.color.field.control_hover" }, + palette.prefixColor); + palette.prefixBorderColor = ResolveThemeColorAliases( + theme, + { "editor.color.field.vector_prefix_border", "editor.color.field.control_border" }, + palette.prefixBorderColor); + palette.labelColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.label", palette.labelColor); + palette.valueColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.value", palette.valueColor); + palette.readOnlyValueColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.value_readonly", + palette.readOnlyValueColor); + palette.axisXColor = ResolveThemeColorAliases( + theme, + { "editor.color.field.vector_axis_x", "editor.color.field.value" }, + palette.axisXColor); + palette.axisYColor = ResolveThemeColorAliases( + theme, + { "editor.color.field.vector_axis_y", "editor.color.field.value" }, + palette.axisYColor); + palette.axisZColor = ResolveThemeColorAliases( + theme, + { "editor.color.field.vector_axis_z", "editor.color.field.value" }, + palette.axisZColor); + palette.axisWColor = ResolveThemeColorAliases( + theme, + { "editor.color.field.vector_axis_w", "editor.color.field.value" }, + palette.axisWColor); + return palette; +} + Widgets::UIEditorEnumFieldMetrics ResolveUIEditorEnumFieldMetrics( const UITheme& theme, const Widgets::UIEditorEnumFieldMetrics& fallback) { @@ -1386,6 +1525,51 @@ Widgets::UIEditorVector3FieldPalette BuildUIEditorHostedVector3FieldPalette( return hosted; } +Widgets::UIEditorVector4FieldMetrics BuildUIEditorHostedVector4FieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorVector4FieldMetrics& fallback) { + Widgets::UIEditorVector4FieldMetrics hosted = fallback; + hosted.rowHeight = propertyGridMetrics.fieldRowHeight; + hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; + hosted.labelControlGap = propertyGridMetrics.labelControlGap; + hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; + hosted.controlInsetY = propertyGridMetrics.valueBoxInsetY; + hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; + hosted.labelFontSize = propertyGridMetrics.labelFontSize; + hosted.valueTextInsetX = propertyGridMetrics.valueBoxInsetX; + hosted.valueTextInsetY = propertyGridMetrics.valueTextInsetY; + hosted.valueFontSize = propertyGridMetrics.valueFontSize; + hosted.cornerRounding = propertyGridMetrics.cornerRounding; + hosted.componentRounding = propertyGridMetrics.valueBoxRounding; + hosted.borderThickness = propertyGridMetrics.borderThickness; + hosted.focusedBorderThickness = propertyGridMetrics.focusedBorderThickness; + return hosted; +} + +Widgets::UIEditorVector4FieldPalette BuildUIEditorHostedVector4FieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorVector4FieldPalette& fallback) { + Widgets::UIEditorVector4FieldPalette hosted = fallback; + hosted.surfaceColor = kTransparent; + hosted.borderColor = kTransparent; + hosted.focusedBorderColor = kTransparent; + hosted.rowHoverColor = kTransparent; + hosted.rowActiveColor = kTransparent; + hosted.componentColor = propertyGridPalette.valueBoxColor; + hosted.componentHoverColor = propertyGridPalette.valueBoxHoverColor; + hosted.componentEditingColor = propertyGridPalette.valueBoxEditingColor; + hosted.readOnlyColor = propertyGridPalette.valueBoxReadOnlyColor; + hosted.componentBorderColor = propertyGridPalette.valueBoxBorderColor; + hosted.componentFocusedBorderColor = propertyGridPalette.valueBoxEditingBorderColor; + hosted.prefixColor = propertyGridPalette.valueBoxHoverColor; + hosted.prefixBorderColor = propertyGridPalette.valueBoxBorderColor; + hosted.labelColor = propertyGridPalette.labelTextColor; + hosted.valueColor = propertyGridPalette.valueTextColor; + hosted.readOnlyValueColor = propertyGridPalette.readOnlyValueTextColor; + return hosted; +} + Widgets::UIEditorEnumFieldMetrics BuildUIEditorHostedEnumFieldMetrics( const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, const Widgets::UIEditorEnumFieldMetrics& fallback) { diff --git a/new_editor/src/Core/UIEditorVector4FieldInteraction.cpp b/new_editor/src/Core/UIEditorVector4FieldInteraction.cpp new file mode 100644 index 00000000..f78678d0 --- /dev/null +++ b/new_editor/src/Core/UIEditorVector4FieldInteraction.cpp @@ -0,0 +1,563 @@ +#include + +#include + +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPointerButton; +using ::XCEngine::UI::Text::HandleKeyDown; +using ::XCEngine::UI::Text::InsertCharacter; +using ::XCEngine::UI::Editor::Widgets::BuildUIEditorVector4FieldLayout; +using ::XCEngine::UI::Editor::Widgets::FormatUIEditorVector4FieldComponentValue; +using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorVector4Field; +using ::XCEngine::UI::Editor::Widgets::IsUIEditorVector4FieldPointInside; +using ::XCEngine::UI::Editor::Widgets::NormalizeUIEditorVector4FieldComponentValue; +using ::XCEngine::UI::Editor::Widgets::TryParseUIEditorVector4FieldComponentValue; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTarget; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTargetKind; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldInvalidComponentIndex; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldLayout; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldMetrics; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector4FieldSpec; + +bool ShouldUsePointerPosition(const UIInputEvent& event) { + switch (event.type) { + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerButtonDown: + case UIInputEventType::PointerButtonUp: + case UIInputEventType::PointerWheel: + return true; + default: + return false; + } +} + +std::size_t ResolveFallbackSelectedComponentIndex( + const UIEditorVector4FieldInteractionState& state) { + return state.vector4FieldState.selectedComponentIndex == + UIEditorVector4FieldInvalidComponentIndex + ? 0u + : state.vector4FieldState.selectedComponentIndex; +} + +std::string BuildComponentEditFieldId( + const UIEditorVector4FieldSpec& spec, + std::size_t componentIndex) { + return spec.fieldId + "." + std::to_string(componentIndex); +} + +bool IsPermittedCharacter( + const UIEditorVector4FieldSpec& spec, + std::uint32_t character) { + if (character >= static_cast('0') && + character <= static_cast('9')) { + return true; + } + + if (character == static_cast('-') || + character == static_cast('+')) { + return true; + } + + return !spec.integerMode && character == static_cast('.'); +} + +void SyncDisplayTexts( + UIEditorVector4FieldInteractionState& state, + const UIEditorVector4FieldSpec& spec) { + for (std::size_t componentIndex = 0u; + componentIndex < state.vector4FieldState.displayTexts.size(); + ++componentIndex) { + if (state.vector4FieldState.editing && + state.vector4FieldState.selectedComponentIndex == componentIndex) { + continue; + } + + state.vector4FieldState.displayTexts[componentIndex] = + FormatUIEditorVector4FieldComponentValue(spec, componentIndex); + } +} + +void SyncHoverTarget( + UIEditorVector4FieldInteractionState& state, + const UIEditorVector4FieldLayout& layout) { + if (!state.hasPointerPosition) { + state.vector4FieldState.hoveredTarget = UIEditorVector4FieldHitTargetKind::None; + state.vector4FieldState.hoveredComponentIndex = UIEditorVector4FieldInvalidComponentIndex; + return; + } + + const UIEditorVector4FieldHitTarget hitTarget = + HitTestUIEditorVector4Field(layout, state.pointerPosition); + state.vector4FieldState.hoveredTarget = hitTarget.kind; + state.vector4FieldState.hoveredComponentIndex = hitTarget.componentIndex; +} + +bool MoveSelection( + UIEditorVector4FieldInteractionState& state, + int direction, + UIEditorVector4FieldInteractionResult& result) { + const std::size_t before = ResolveFallbackSelectedComponentIndex(state); + const std::size_t after = + direction < 0 + ? (before == 0u ? 0u : before - 1u) + : (before >= 3u ? 3u : before + 1u); + state.vector4FieldState.selectedComponentIndex = after; + result.selectionChanged = before != after; + result.selectedComponentIndex = after; + result.consumed = true; + return true; +} + +bool SelectComponent( + UIEditorVector4FieldInteractionState& state, + std::size_t componentIndex, + UIEditorVector4FieldInteractionResult& result) { + if (componentIndex >= 4u) { + return false; + } + + const std::size_t before = ResolveFallbackSelectedComponentIndex(state); + state.vector4FieldState.selectedComponentIndex = componentIndex; + result.selectionChanged = before != componentIndex; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool BeginEdit( + UIEditorVector4FieldInteractionState& state, + const UIEditorVector4FieldSpec& spec, + std::size_t componentIndex, + bool clearText) { + if (spec.readOnly || componentIndex >= spec.values.size()) { + return false; + } + + const std::string baseline = + FormatUIEditorVector4FieldComponentValue(spec, componentIndex); + const std::string editFieldId = + BuildComponentEditFieldId(spec, componentIndex); + const bool changed = state.editModel.BeginEdit(editFieldId, baseline); + if (!changed && + state.editModel.HasActiveEdit() && + state.editModel.GetActiveFieldId() != editFieldId) { + return false; + } + if (!changed && + state.vector4FieldState.editing && + state.vector4FieldState.selectedComponentIndex == componentIndex) { + return false; + } + + state.vector4FieldState.selectedComponentIndex = componentIndex; + state.vector4FieldState.editing = true; + state.textInputState.value = clearText ? std::string() : baseline; + state.textInputState.caret = state.textInputState.value.size(); + state.editModel.UpdateStagedValue(state.textInputState.value); + state.vector4FieldState.displayTexts[componentIndex] = state.textInputState.value; + return true; +} + +bool CommitEdit( + UIEditorVector4FieldInteractionState& state, + UIEditorVector4FieldSpec& spec, + UIEditorVector4FieldInteractionResult& result) { + if (!state.vector4FieldState.editing || + !state.editModel.HasActiveEdit() || + state.vector4FieldState.selectedComponentIndex >= spec.values.size()) { + return false; + } + + const std::size_t componentIndex = state.vector4FieldState.selectedComponentIndex; + double parsedValue = spec.values[componentIndex]; + if (!TryParseUIEditorVector4FieldComponentValue( + spec, + state.textInputState.value, + parsedValue)) { + result.consumed = true; + result.editCommitRejected = true; + return false; + } + + result.valuesBefore = spec.values; + spec.values[componentIndex] = NormalizeUIEditorVector4FieldComponentValue(spec, parsedValue); + result.valuesAfter = spec.values; + result.valueChanged = result.valuesBefore != result.valuesAfter; + result.editCommitted = true; + result.consumed = true; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.committedText = + FormatUIEditorVector4FieldComponentValue(spec, componentIndex); + + state.editModel.CommitEdit(); + state.textInputState = {}; + state.vector4FieldState.editing = false; + state.vector4FieldState.displayTexts[componentIndex] = result.committedText; + return true; +} + +bool CancelEdit( + UIEditorVector4FieldInteractionState& state, + const UIEditorVector4FieldSpec& spec, + UIEditorVector4FieldInteractionResult& result) { + if (!state.vector4FieldState.editing || + !state.editModel.HasActiveEdit() || + state.vector4FieldState.selectedComponentIndex >= spec.values.size()) { + return false; + } + + const std::size_t componentIndex = state.vector4FieldState.selectedComponentIndex; + state.editModel.CancelEdit(); + state.textInputState = {}; + state.vector4FieldState.editing = false; + state.vector4FieldState.displayTexts[componentIndex] = + FormatUIEditorVector4FieldComponentValue(spec, componentIndex); + result.consumed = true; + result.editCanceled = true; + result.valuesBefore = spec.values; + result.valuesAfter = spec.values; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool ApplyStep( + UIEditorVector4FieldInteractionState& state, + UIEditorVector4FieldSpec& spec, + double direction, + bool snapToEdge, + UIEditorVector4FieldInteractionResult& result) { + if (spec.readOnly) { + return false; + } + + const std::size_t componentIndex = ResolveFallbackSelectedComponentIndex(state); + state.vector4FieldState.selectedComponentIndex = componentIndex; + + if (state.vector4FieldState.editing && + !CommitEdit(state, spec, result)) { + return result.editCommitRejected; + } + + result.valuesBefore = spec.values; + if (snapToEdge) { + spec.values[componentIndex] = + direction < 0.0 + ? NormalizeUIEditorVector4FieldComponentValue(spec, spec.minValue) + : NormalizeUIEditorVector4FieldComponentValue(spec, spec.maxValue); + } else { + const double step = spec.step == 0.0 ? 1.0 : spec.step; + spec.values[componentIndex] = NormalizeUIEditorVector4FieldComponentValue( + spec, + spec.values[componentIndex] + step * direction); + result.stepDelta = step * direction; + } + + result.valuesAfter = spec.values; + result.stepApplied = true; + result.valueChanged = result.valuesBefore != result.valuesAfter || result.valueChanged; + result.changedComponentIndex = componentIndex; + result.selectedComponentIndex = componentIndex; + result.consumed = true; + state.vector4FieldState.displayTexts[componentIndex] = + FormatUIEditorVector4FieldComponentValue(spec, componentIndex); + return true; +} + +} // namespace + +UIEditorVector4FieldInteractionFrame UpdateUIEditorVector4FieldInteraction( + UIEditorVector4FieldInteractionState& state, + UIEditorVector4FieldSpec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector& inputEvents, + const UIEditorVector4FieldMetrics& metrics) { + UIEditorVector4FieldLayout layout = BuildUIEditorVector4FieldLayout(bounds, spec, metrics); + SyncDisplayTexts(state, spec); + SyncHoverTarget(state, layout); + + UIEditorVector4FieldInteractionResult interactionResult = {}; + interactionResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; + for (const UIInputEvent& event : inputEvents) { + if (ShouldUsePointerPosition(event)) { + state.pointerPosition = event.position; + state.hasPointerPosition = true; + } else if (event.type == UIInputEventType::PointerLeave) { + state.hasPointerPosition = false; + } + + UIEditorVector4FieldInteractionResult eventResult = {}; + eventResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; + switch (event.type) { + case UIInputEventType::FocusGained: + eventResult.focusChanged = !state.vector4FieldState.focused; + state.vector4FieldState.focused = true; + if (state.vector4FieldState.selectedComponentIndex == + UIEditorVector4FieldInvalidComponentIndex) { + state.vector4FieldState.selectedComponentIndex = 0u; + } + eventResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; + break; + + case UIInputEventType::FocusLost: + eventResult.focusChanged = state.vector4FieldState.focused; + state.vector4FieldState.focused = false; + state.vector4FieldState.activeTarget = UIEditorVector4FieldHitTargetKind::None; + state.vector4FieldState.activeComponentIndex = UIEditorVector4FieldInvalidComponentIndex; + state.hasPointerPosition = false; + if (state.vector4FieldState.editing) { + CommitEdit(state, spec, eventResult); + if (eventResult.editCommitRejected) { + CancelEdit(state, spec, eventResult); + } + } + break; + + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerLeave: + break; + + case UIInputEventType::PointerButtonDown: { + const UIEditorVector4FieldHitTarget hitTarget = + state.hasPointerPosition + ? HitTestUIEditorVector4Field(layout, state.pointerPosition) + : UIEditorVector4FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + const bool insideField = + state.hasPointerPosition && + IsUIEditorVector4FieldPointInside(layout.bounds, state.pointerPosition); + if (insideField) { + eventResult.focusChanged = !state.vector4FieldState.focused; + state.vector4FieldState.focused = true; + state.vector4FieldState.activeTarget = hitTarget.kind == UIEditorVector4FieldHitTargetKind::None + ? UIEditorVector4FieldHitTargetKind::Row + : hitTarget.kind; + state.vector4FieldState.activeComponentIndex = hitTarget.componentIndex; + if (hitTarget.kind == UIEditorVector4FieldHitTargetKind::Component) { + SelectComponent(state, hitTarget.componentIndex, eventResult); + } else if (state.vector4FieldState.selectedComponentIndex == + UIEditorVector4FieldInvalidComponentIndex) { + state.vector4FieldState.selectedComponentIndex = 0u; + eventResult.selectedComponentIndex = 0u; + } + eventResult.consumed = true; + } else { + if (state.vector4FieldState.editing) { + CommitEdit(state, spec, eventResult); + if (!eventResult.editCommitRejected) { + eventResult.focusChanged = state.vector4FieldState.focused; + state.vector4FieldState.focused = false; + } + } else if (state.vector4FieldState.focused) { + eventResult.focusChanged = true; + state.vector4FieldState.focused = false; + } + state.vector4FieldState.activeTarget = UIEditorVector4FieldHitTargetKind::None; + state.vector4FieldState.activeComponentIndex = UIEditorVector4FieldInvalidComponentIndex; + } + break; + } + + case UIInputEventType::PointerButtonUp: { + const UIEditorVector4FieldHitTarget hitTarget = + state.hasPointerPosition + ? HitTestUIEditorVector4Field(layout, state.pointerPosition) + : UIEditorVector4FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton == UIPointerButton::Left) { + const UIEditorVector4FieldHitTargetKind activeTarget = state.vector4FieldState.activeTarget; + const std::size_t activeComponentIndex = state.vector4FieldState.activeComponentIndex; + state.vector4FieldState.activeTarget = UIEditorVector4FieldHitTargetKind::None; + state.vector4FieldState.activeComponentIndex = UIEditorVector4FieldInvalidComponentIndex; + + if (activeTarget == UIEditorVector4FieldHitTargetKind::Component && + hitTarget.kind == UIEditorVector4FieldHitTargetKind::Component && + activeComponentIndex == hitTarget.componentIndex) { + SelectComponent(state, hitTarget.componentIndex, eventResult); + if (!state.vector4FieldState.editing) { + eventResult.editStarted = + BeginEdit(state, spec, hitTarget.componentIndex, false); + } + eventResult.consumed = true; + } else if (hitTarget.kind == UIEditorVector4FieldHitTargetKind::Row) { + eventResult.consumed = true; + } + } + break; + } + + case UIInputEventType::KeyDown: + if (!state.vector4FieldState.focused) { + break; + } + + if (state.vector4FieldState.editing) { + if (event.keyCode == static_cast(KeyCode::Escape)) { + CancelEdit(state, spec, eventResult); + break; + } + + if (event.keyCode == static_cast(KeyCode::Tab)) { + if (CommitEdit(state, spec, eventResult)) { + MoveSelection( + state, + event.modifiers.shift ? -1 : 1, + eventResult); + } + eventResult.consumed = true; + break; + } + + const auto textResult = + HandleKeyDown(state.textInputState, event.keyCode, event.modifiers); + if (textResult.handled) { + state.editModel.UpdateStagedValue(state.textInputState.value); + state.vector4FieldState.displayTexts[state.vector4FieldState.selectedComponentIndex] = + state.textInputState.value; + eventResult.consumed = true; + eventResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; + if (textResult.submitRequested) { + CommitEdit(state, spec, eventResult); + } + break; + } + } else { + switch (static_cast(event.keyCode)) { + case KeyCode::Left: + MoveSelection(state, -1, eventResult); + break; + + case KeyCode::Right: + case KeyCode::Tab: + MoveSelection( + state, + static_cast(event.keyCode) == KeyCode::Tab && event.modifiers.shift ? -1 : 1, + eventResult); + break; + + case KeyCode::Up: + ApplyStep(state, spec, 1.0, false, eventResult); + break; + + case KeyCode::Down: + ApplyStep(state, spec, -1.0, false, eventResult); + break; + + case KeyCode::Home: + ApplyStep(state, spec, -1.0, true, eventResult); + break; + + case KeyCode::End: + ApplyStep(state, spec, 1.0, true, eventResult); + break; + + case KeyCode::Enter: + eventResult.selectedComponentIndex = ResolveFallbackSelectedComponentIndex(state); + eventResult.editStarted = BeginEdit( + state, + spec, + eventResult.selectedComponentIndex, + false); + eventResult.consumed = eventResult.editStarted; + break; + + default: + break; + } + } + break; + + case UIInputEventType::Character: + if (!state.vector4FieldState.focused || + spec.readOnly || + event.modifiers.control || + event.modifiers.alt || + event.modifiers.super || + !IsPermittedCharacter(spec, event.character)) { + break; + } + + if (state.vector4FieldState.selectedComponentIndex == + UIEditorVector4FieldInvalidComponentIndex) { + state.vector4FieldState.selectedComponentIndex = 0u; + } + eventResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; + + if (!state.vector4FieldState.editing) { + eventResult.editStarted = BeginEdit( + state, + spec, + state.vector4FieldState.selectedComponentIndex, + true); + } + + if (InsertCharacter(state.textInputState, event.character)) { + state.editModel.UpdateStagedValue(state.textInputState.value); + state.vector4FieldState.displayTexts[state.vector4FieldState.selectedComponentIndex] = + state.textInputState.value; + eventResult.consumed = true; + } + break; + + default: + break; + } + + layout = BuildUIEditorVector4FieldLayout(bounds, spec, metrics); + SyncDisplayTexts(state, spec); + SyncHoverTarget(state, layout); + if (eventResult.hitTarget.kind == UIEditorVector4FieldHitTargetKind::None && + state.hasPointerPosition) { + eventResult.hitTarget = HitTestUIEditorVector4Field(layout, state.pointerPosition); + } + if (eventResult.selectedComponentIndex == UIEditorVector4FieldInvalidComponentIndex) { + eventResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; + } + + if (eventResult.consumed || + eventResult.focusChanged || + eventResult.valueChanged || + eventResult.stepApplied || + eventResult.selectionChanged || + eventResult.editStarted || + eventResult.editCommitted || + eventResult.editCommitRejected || + eventResult.editCanceled || + eventResult.hitTarget.kind != UIEditorVector4FieldHitTargetKind::None) { + interactionResult = std::move(eventResult); + } + } + + layout = BuildUIEditorVector4FieldLayout(bounds, spec, metrics); + SyncDisplayTexts(state, spec); + SyncHoverTarget(state, layout); + if (interactionResult.hitTarget.kind == UIEditorVector4FieldHitTargetKind::None && + state.hasPointerPosition) { + interactionResult.hitTarget = HitTestUIEditorVector4Field(layout, state.pointerPosition); + } + if (interactionResult.selectedComponentIndex == UIEditorVector4FieldInvalidComponentIndex) { + interactionResult.selectedComponentIndex = state.vector4FieldState.selectedComponentIndex; + } + + return { + std::move(layout), + std::move(interactionResult) + }; +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Widgets/UIEditorPropertyGrid.cpp b/new_editor/src/Widgets/UIEditorPropertyGrid.cpp index bc3e524d..01f2cc3b 100644 --- a/new_editor/src/Widgets/UIEditorPropertyGrid.cpp +++ b/new_editor/src/Widgets/UIEditorPropertyGrid.cpp @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include #include @@ -94,6 +97,48 @@ UIEditorTextFieldSpec BuildTextFieldSpec(const UIEditorPropertyGridField& field) return spec; } +UIEditorVector2FieldSpec BuildVector2FieldSpec(const UIEditorPropertyGridField& field) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.values = field.vector2Value.values; + spec.componentLabels = field.vector2Value.componentLabels; + spec.step = field.vector2Value.step; + spec.minValue = field.vector2Value.minValue; + spec.maxValue = field.vector2Value.maxValue; + spec.integerMode = field.vector2Value.integerMode; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorVector3FieldSpec BuildVector3FieldSpec(const UIEditorPropertyGridField& field) { + UIEditorVector3FieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.values = field.vector3Value.values; + spec.componentLabels = field.vector3Value.componentLabels; + spec.step = field.vector3Value.step; + spec.minValue = field.vector3Value.minValue; + spec.maxValue = field.vector3Value.maxValue; + spec.integerMode = field.vector3Value.integerMode; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorVector4FieldSpec BuildVector4FieldSpec(const UIEditorPropertyGridField& field) { + UIEditorVector4FieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.values = field.vector4Value.values; + spec.componentLabels = field.vector4Value.componentLabels; + spec.step = field.vector4Value.step; + spec.minValue = field.vector4Value.minValue; + spec.maxValue = field.vector4Value.maxValue; + spec.integerMode = field.vector4Value.integerMode; + spec.readOnly = field.readOnly; + return spec; +} + struct UIEditorPropertyGridFieldRects { UIRect labelRect = {}; UIRect valueRect = {}; @@ -128,6 +173,30 @@ UIEditorPropertyGridFieldRects ResolveFieldRects( return { fieldLayout.labelRect, fieldLayout.valueRect }; } + case UIEditorPropertyGridFieldKind::Vector2: { + const UIEditorVector2FieldLayout fieldLayout = BuildUIEditorVector2FieldLayout( + rowRect, + BuildVector2FieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorHostedVector2FieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.controlRect }; + } + + case UIEditorPropertyGridFieldKind::Vector3: { + const UIEditorVector3FieldLayout fieldLayout = BuildUIEditorVector3FieldLayout( + rowRect, + BuildVector3FieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorHostedVector3FieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.controlRect }; + } + + case UIEditorPropertyGridFieldKind::Vector4: { + const UIEditorVector4FieldLayout fieldLayout = BuildUIEditorVector4FieldLayout( + rowRect, + BuildVector4FieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorHostedVector4FieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.controlRect }; + } + case UIEditorPropertyGridFieldKind::Text: default: { const UIEditorTextFieldLayout fieldLayout = BuildUIEditorTextFieldLayout( @@ -199,6 +268,36 @@ UIEditorTextFieldHitTargetKind ResolveTextHoveredTarget( : UIEditorTextFieldHitTargetKind::Row; } +UIEditorVector2FieldHitTargetKind ResolveVector2HoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorVector2FieldHitTargetKind::None; + } + + return UIEditorVector2FieldHitTargetKind::Row; +} + +UIEditorVector3FieldHitTargetKind ResolveVector3HoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorVector3FieldHitTargetKind::None; + } + + return UIEditorVector3FieldHitTargetKind::Row; +} + +UIEditorVector4FieldHitTargetKind ResolveVector4HoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorVector4FieldHitTargetKind::None; + } + + return UIEditorVector4FieldHitTargetKind::Row; +} + std::vector BuildEnumPopupItems( const UIEditorPropertyGridField& field) { std::vector items = {}; @@ -280,6 +379,27 @@ bool BuildEnumPopupRuntime( return true; } +std::string FormatVector2ValueText(const UIEditorPropertyGridField& field) { + const UIEditorVector2FieldSpec spec = BuildVector2FieldSpec(field); + return FormatUIEditorVector2FieldComponentValue(spec, 0u) + ", " + + FormatUIEditorVector2FieldComponentValue(spec, 1u); +} + +std::string FormatVector3ValueText(const UIEditorPropertyGridField& field) { + const UIEditorVector3FieldSpec spec = BuildVector3FieldSpec(field); + return FormatUIEditorVector3FieldComponentValue(spec, 0u) + ", " + + FormatUIEditorVector3FieldComponentValue(spec, 1u) + ", " + + FormatUIEditorVector3FieldComponentValue(spec, 2u); +} + +std::string FormatVector4ValueText(const UIEditorPropertyGridField& field) { + const UIEditorVector4FieldSpec spec = BuildVector4FieldSpec(field); + return FormatUIEditorVector4FieldComponentValue(spec, 0u) + ", " + + FormatUIEditorVector4FieldComponentValue(spec, 1u) + ", " + + FormatUIEditorVector4FieldComponentValue(spec, 2u) + ", " + + FormatUIEditorVector4FieldComponentValue(spec, 3u); +} + } // namespace bool IsUIEditorPropertyGridPointInside( @@ -330,6 +450,15 @@ std::string ResolveUIEditorPropertyGridFieldValueText( case UIEditorPropertyGridFieldKind::Enum: return ResolveUIEditorEnumFieldValueText(BuildEnumFieldSpec(field)); + case UIEditorPropertyGridFieldKind::Vector2: + return FormatVector2ValueText(field); + + case UIEditorPropertyGridFieldKind::Vector3: + return FormatVector3ValueText(field); + + case UIEditorPropertyGridFieldKind::Vector4: + return FormatVector4ValueText(field); + case UIEditorPropertyGridFieldKind::Text: default: return field.valueText; @@ -571,6 +700,18 @@ void AppendUIEditorPropertyGridForeground( ::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(metrics); const UIEditorTextFieldPalette textPalette = ::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette(palette); + const UIEditorVector2FieldMetrics vector2Metrics = + ::XCEngine::UI::Editor::BuildUIEditorHostedVector2FieldMetrics(metrics); + const UIEditorVector2FieldPalette vector2Palette = + ::XCEngine::UI::Editor::BuildUIEditorHostedVector2FieldPalette(palette); + const UIEditorVector3FieldMetrics vector3Metrics = + ::XCEngine::UI::Editor::BuildUIEditorHostedVector3FieldMetrics(metrics); + const UIEditorVector3FieldPalette vector3Palette = + ::XCEngine::UI::Editor::BuildUIEditorHostedVector3FieldPalette(palette); + const UIEditorVector4FieldMetrics vector4Metrics = + ::XCEngine::UI::Editor::BuildUIEditorHostedVector4FieldMetrics(metrics); + const UIEditorVector4FieldPalette vector4Palette = + ::XCEngine::UI::Editor::BuildUIEditorHostedVector4FieldPalette(palette); const UIEditorEnumFieldMetrics enumMetrics = ::XCEngine::UI::Editor::BuildUIEditorHostedEnumFieldMetrics(metrics); const UIEditorEnumFieldPalette enumPalette = @@ -640,6 +781,60 @@ void AppendUIEditorPropertyGridForeground( break; } + case UIEditorPropertyGridFieldKind::Vector2: { + UIEditorVector2FieldState fieldState = {}; + fieldState.hoveredTarget = ResolveVector2HoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorVector2FieldHitTargetKind::Row + : UIEditorVector2FieldHitTargetKind::None; + fieldState.focused = state.focused; + AppendUIEditorVector2Field( + drawList, + layout.fieldRowRects[visibleFieldIndex], + BuildVector2FieldSpec(field), + fieldState, + vector2Palette, + vector2Metrics); + break; + } + + case UIEditorPropertyGridFieldKind::Vector3: { + UIEditorVector3FieldState fieldState = {}; + fieldState.hoveredTarget = ResolveVector3HoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorVector3FieldHitTargetKind::Row + : UIEditorVector3FieldHitTargetKind::None; + fieldState.focused = state.focused; + AppendUIEditorVector3Field( + drawList, + layout.fieldRowRects[visibleFieldIndex], + BuildVector3FieldSpec(field), + fieldState, + vector3Palette, + vector3Metrics); + break; + } + + case UIEditorPropertyGridFieldKind::Vector4: { + UIEditorVector4FieldState fieldState = {}; + fieldState.hoveredTarget = ResolveVector4HoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorVector4FieldHitTargetKind::Row + : UIEditorVector4FieldHitTargetKind::None; + fieldState.focused = state.focused; + AppendUIEditorVector4Field( + drawList, + layout.fieldRowRects[visibleFieldIndex], + BuildVector4FieldSpec(field), + fieldState, + vector4Palette, + vector4Metrics); + break; + } + case UIEditorPropertyGridFieldKind::Text: default: { const std::string& displayedValue = diff --git a/new_editor/src/Widgets/UIEditorVector4Field.cpp b/new_editor/src/Widgets/UIEditorVector4Field.cpp new file mode 100644 index 00000000..47c778c5 --- /dev/null +++ b/new_editor/src/Widgets/UIEditorVector4Field.cpp @@ -0,0 +1,285 @@ +#include + +#include +#include +#include + +#include + +namespace XCEngine::UI::Editor::Widgets { + +namespace { + +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; + +float ClampNonNegative(float value) { + return (std::max)(0.0f, value); +} + +float ApproximateTextWidth(float fontSize, std::size_t characterCount) { + return fontSize * 0.55f * static_cast(characterCount); +} + +UIEditorNumberFieldSpec BuildComponentNumberSpec( + const UIEditorVector4FieldSpec& spec, + std::size_t componentIndex) { + UIEditorNumberFieldSpec numberSpec = {}; + numberSpec.fieldId = spec.fieldId + "." + std::to_string(componentIndex); + numberSpec.label.clear(); + numberSpec.value = componentIndex < spec.values.size() ? spec.values[componentIndex] : 0.0; + numberSpec.step = spec.step; + numberSpec.minValue = spec.minValue; + numberSpec.maxValue = spec.maxValue; + numberSpec.integerMode = spec.integerMode; + numberSpec.readOnly = spec.readOnly; + return numberSpec; +} + +::XCEngine::UI::UIColor ResolveRowFillColor( + const UIEditorVector4FieldState& state, + const UIEditorVector4FieldPalette& palette) { + if (state.activeTarget != UIEditorVector4FieldHitTargetKind::None) { + return palette.rowActiveColor; + } + if (state.hoveredTarget != UIEditorVector4FieldHitTargetKind::None) { + return palette.rowHoverColor; + } + return palette.surfaceColor; +} + +::XCEngine::UI::UIColor ResolveComponentFillColor( + const UIEditorVector4FieldSpec& spec, + const UIEditorVector4FieldState& state, + const UIEditorVector4FieldPalette& palette, + std::size_t componentIndex) { + if (spec.readOnly) { + return palette.readOnlyColor; + } + if (state.editing && state.selectedComponentIndex == componentIndex) { + return palette.componentEditingColor; + } + if (state.hoveredComponentIndex == componentIndex) { + return palette.componentHoverColor; + } + return palette.componentColor; +} + +::XCEngine::UI::UIColor ResolveComponentBorderColor( + const UIEditorVector4FieldState& state, + const UIEditorVector4FieldPalette& palette, + std::size_t componentIndex) { + if (state.editing && state.selectedComponentIndex == componentIndex) { + return palette.componentFocusedBorderColor; + } + return palette.componentBorderColor; +} + +::XCEngine::UI::UIColor ResolveAxisColor( + const UIEditorVector4FieldPalette& palette, + std::size_t componentIndex) { + switch (componentIndex) { + case 0u: + return palette.axisXColor; + case 1u: + return palette.axisYColor; + case 2u: + return palette.axisZColor; + case 3u: + default: + return palette.axisWColor; + } +} + +} // namespace + +bool IsUIEditorVector4FieldPointInside( + const UIRect& rect, + const UIPoint& point) { + return point.x >= rect.x && + point.x <= rect.x + rect.width && + point.y >= rect.y && + point.y <= rect.y + rect.height; +} + +double NormalizeUIEditorVector4FieldComponentValue( + const UIEditorVector4FieldSpec& spec, + double value) { + return NormalizeUIEditorNumberFieldValue(BuildComponentNumberSpec(spec, 0u), value); +} + +bool TryParseUIEditorVector4FieldComponentValue( + const UIEditorVector4FieldSpec& spec, + std::string_view text, + double& outValue) { + return TryParseUIEditorNumberFieldValue(BuildComponentNumberSpec(spec, 0u), text, outValue); +} + +std::string FormatUIEditorVector4FieldComponentValue( + const UIEditorVector4FieldSpec& spec, + std::size_t componentIndex) { + return FormatUIEditorNumberFieldValue(BuildComponentNumberSpec(spec, componentIndex)); +} + +UIEditorVector4FieldLayout BuildUIEditorVector4FieldLayout( + const UIRect& bounds, + const UIEditorVector4FieldSpec&, + const UIEditorVector4FieldMetrics& metrics) { + const float requiredControlWidth = metrics.componentMinWidth * 4.0f + metrics.componentGap * 3.0f; + const UIEditorFieldRowLayout hostLayout = BuildUIEditorFieldRowLayout( + bounds, + requiredControlWidth, + UIEditorFieldRowLayoutMetrics { + metrics.rowHeight, + metrics.horizontalPadding, + metrics.labelControlGap, + metrics.controlColumnStart, + metrics.controlTrailingInset, + metrics.controlInsetY, + }); + const float componentWidth = + ClampNonNegative((hostLayout.controlRect.width - metrics.componentGap * 3.0f) / 4.0f); + + UIEditorVector4FieldLayout layout = {}; + layout.bounds = hostLayout.bounds; + layout.labelRect = hostLayout.labelRect; + layout.controlRect = hostLayout.controlRect; + + for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { + const float componentX = + layout.controlRect.x + (componentWidth + metrics.componentGap) * static_cast(componentIndex); + const UIRect componentRect(componentX, layout.controlRect.y, componentWidth, layout.controlRect.height); + const float prefixWidth = (std::min)(metrics.componentPrefixWidth, componentRect.width); + const float labelGap = ClampNonNegative(metrics.componentLabelGap); + const float valueX = componentRect.x + prefixWidth + labelGap; + layout.componentRects[componentIndex] = componentRect; + layout.componentPrefixRects[componentIndex] = + UIRect(componentRect.x, componentRect.y, prefixWidth, componentRect.height); + layout.componentValueRects[componentIndex] = + UIRect( + valueX, + componentRect.y, + ClampNonNegative(componentRect.width - prefixWidth - labelGap), + componentRect.height); + } + + return layout; +} + +UIEditorVector4FieldHitTarget HitTestUIEditorVector4Field( + const UIEditorVector4FieldLayout& layout, + const UIPoint& point) { + for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { + if (IsUIEditorVector4FieldPointInside(layout.componentRects[componentIndex], point)) { + return { UIEditorVector4FieldHitTargetKind::Component, componentIndex }; + } + } + if (IsUIEditorVector4FieldPointInside(layout.bounds, point)) { + return { UIEditorVector4FieldHitTargetKind::Row, UIEditorVector4FieldInvalidComponentIndex }; + } + return {}; +} + +void AppendUIEditorVector4FieldBackground( + UIDrawList& drawList, + const UIEditorVector4FieldLayout& layout, + const UIEditorVector4FieldSpec& spec, + const UIEditorVector4FieldState& state, + const UIEditorVector4FieldPalette& palette, + const UIEditorVector4FieldMetrics& metrics) { + if (ResolveRowFillColor(state, palette).a > 0.0f) { + drawList.AddFilledRect(layout.bounds, ResolveRowFillColor(state, palette), metrics.cornerRounding); + } + if ((state.focused ? palette.focusedBorderColor.a : palette.borderColor.a) > 0.0f) { + drawList.AddRectOutline( + layout.bounds, + state.focused ? palette.focusedBorderColor : palette.borderColor, + state.focused ? metrics.focusedBorderThickness : metrics.borderThickness, + metrics.cornerRounding); + } + + for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { + drawList.AddFilledRect( + layout.componentValueRects[componentIndex], + ResolveComponentFillColor(spec, state, palette, componentIndex), + metrics.componentRounding); + drawList.AddRectOutline( + layout.componentValueRects[componentIndex], + ResolveComponentBorderColor(state, palette, componentIndex), + metrics.borderThickness, + metrics.componentRounding); + } +} + +void AppendUIEditorVector4FieldForeground( + UIDrawList& drawList, + const UIEditorVector4FieldLayout& layout, + const UIEditorVector4FieldSpec& spec, + const UIEditorVector4FieldState& state, + const UIEditorVector4FieldPalette& palette, + const UIEditorVector4FieldMetrics& metrics) { + drawList.PushClipRect(ResolveUIEditorTextClipRect(layout.labelRect, metrics.labelFontSize)); + drawList.AddText( + UIPoint( + layout.labelRect.x, + ResolveUIEditorTextTop(layout.labelRect, metrics.labelFontSize, metrics.labelTextInsetY)), + spec.label, + palette.labelColor, + metrics.labelFontSize); + drawList.PopClipRect(); + + for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { + drawList.PushClipRect( + ResolveUIEditorTextClipRect(layout.componentPrefixRects[componentIndex], metrics.prefixFontSize)); + const std::string& componentLabel = spec.componentLabels[componentIndex]; + const float prefixTextX = + layout.componentPrefixRects[componentIndex].x + + ClampNonNegative( + (layout.componentPrefixRects[componentIndex].width - + ApproximateTextWidth(metrics.prefixFontSize, componentLabel.size())) * + 0.5f) + + metrics.prefixTextInsetX; + drawList.AddText( + UIPoint( + prefixTextX, + ResolveUIEditorTextTop( + layout.componentPrefixRects[componentIndex], + metrics.prefixFontSize, + metrics.prefixTextInsetY)), + componentLabel, + ResolveAxisColor(palette, componentIndex), + metrics.prefixFontSize); + drawList.PopClipRect(); + + drawList.PushClipRect( + ResolveUIEditorTextClipRect(layout.componentValueRects[componentIndex], metrics.valueFontSize)); + drawList.AddText( + UIPoint( + layout.componentValueRects[componentIndex].x + metrics.valueTextInsetX, + ResolveUIEditorTextTop( + layout.componentValueRects[componentIndex], + metrics.valueFontSize, + metrics.valueTextInsetY)), + state.editing && state.selectedComponentIndex == componentIndex + ? state.displayTexts[componentIndex] + : FormatUIEditorVector4FieldComponentValue(spec, componentIndex), + spec.readOnly ? palette.readOnlyValueColor : palette.valueColor, + metrics.valueFontSize); + drawList.PopClipRect(); + } +} + +void AppendUIEditorVector4Field( + UIDrawList& drawList, + const UIRect& bounds, + const UIEditorVector4FieldSpec& spec, + const UIEditorVector4FieldState& state, + const UIEditorVector4FieldPalette& palette, + const UIEditorVector4FieldMetrics& metrics) { + const UIEditorVector4FieldLayout layout = BuildUIEditorVector4FieldLayout(bounds, spec, metrics); + AppendUIEditorVector4FieldBackground(drawList, layout, spec, state, palette, metrics); + AppendUIEditorVector4FieldForeground(drawList, layout, spec, state, palette, metrics); +} + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/tests/UI/Editor/integration/shell/CMakeLists.txt b/tests/UI/Editor/integration/shell/CMakeLists.txt index 21445ab8..c929477c 100644 --- a/tests/UI/Editor/integration/shell/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/CMakeLists.txt @@ -34,6 +34,9 @@ endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vector3_field_basic/CMakeLists.txt") add_subdirectory(vector3_field_basic) endif() +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vector4_field_basic/CMakeLists.txt") + add_subdirectory(vector4_field_basic) +endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/enum_field_basic/CMakeLists.txt") add_subdirectory(enum_field_basic) endif() diff --git a/tests/UI/Editor/integration/shell/property_grid_basic/main.cpp b/tests/UI/Editor/integration/shell/property_grid_basic/main.cpp index cea7a6ef..80efd6a9 100644 --- a/tests/UI/Editor/integration/shell/property_grid_basic/main.cpp +++ b/tests/UI/Editor/integration/shell/property_grid_basic/main.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -262,6 +263,18 @@ UIEditorPropertyGridField MakeEnumField( return field; } +UIEditorPropertyGridField MakeVector4Field( + std::string id, + std::string label, + std::array values) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Vector4; + field.vector4Value.values = values; + return field; +} + std::vector BuildSections() { return { { @@ -269,6 +282,7 @@ std::vector BuildSections() { "Inspector", { MakeBoolField("enabled", "Enabled", true), + MakeVector4Field("rotation", "Rotation", { 0.0, 15.0, 0.0, 1.0 }), MakeNumberField("render_queue", "Render Queue", 2000.0), MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u), MakeTextField("tag", "Tag", "Player") @@ -871,7 +885,7 @@ private: shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), - "2. 点击 value host:Bool toggle、Number/Text edit、Enum popup。", + "2. 点击 value host:Bool toggle、Vector4 宿主排版、Number/Text edit、Enum popup。", shellPalette.textPrimary, shellMetrics.bodyFontSize); drawList.AddText( diff --git a/tests/UI/Editor/integration/shell/vector4_field_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/vector4_field_basic/CMakeLists.txt new file mode 100644 index 00000000..6ed0b4c9 --- /dev/null +++ b/tests/UI/Editor/integration/shell/vector4_field_basic/CMakeLists.txt @@ -0,0 +1,31 @@ +add_executable(editor_ui_vector4_field_basic_validation WIN32 + main.cpp +) + +target_include_directories(editor_ui_vector4_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_vector4_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_vector4_field_basic_validation PRIVATE /utf-8 /FS) + set_property(TARGET editor_ui_vector4_field_basic_validation PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +target_link_libraries(editor_ui_vector4_field_basic_validation PRIVATE + XCUIEditorLib + XCUIEditorHost +) + +set_target_properties(editor_ui_vector4_field_basic_validation PROPERTIES + OUTPUT_NAME "XCUIEditorVector4FieldBasicValidation" +) diff --git a/tests/UI/Editor/integration/shell/vector4_field_basic/captures/.gitkeep b/tests/UI/Editor/integration/shell/vector4_field_basic/captures/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/UI/Editor/integration/shell/vector4_field_basic/captures/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/UI/Editor/integration/shell/vector4_field_basic/main.cpp b/tests/UI/Editor/integration/shell/vector4_field_basic/main.cpp new file mode 100644 index 00000000..c6b482d8 --- /dev/null +++ b/tests/UI/Editor/integration/shell/vector4_field_basic/main.cpp @@ -0,0 +1,456 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include +#include "EditorValidationTheme.h" +#include "Host/AutoScreenshot.h" +#include "Host/NativeRenderer.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT +#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "." +#endif + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Host::AutoScreenshotController; +using XCEngine::UI::Editor::Host::NativeRenderer; +using XCEngine::UI::Editor::UIEditorVector4FieldInteractionFrame; +using XCEngine::UI::Editor::UIEditorVector4FieldInteractionResult; +using XCEngine::UI::Editor::UIEditorVector4FieldInteractionState; +using XCEngine::UI::Editor::UpdateUIEditorVector4FieldInteraction; +using XCEngine::UI::Editor::Widgets::AppendUIEditorVector4Field; +using XCEngine::UI::Editor::Widgets::FormatUIEditorVector4FieldComponentValue; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector4Field; +using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTarget; +using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldSpec; +namespace Style = XCEngine::UI::Style; + +constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector4FieldBasicValidation"; +constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector4Field Basic"; + +struct ScenarioLayout { + UIRect introRect = {}; + UIRect stateRect = {}; + UIRect previewRect = {}; + UIRect inspectorRect = {}; + UIRect inspectorHeaderRect = {}; + UIRect sectionRect = {}; + UIRect fieldRect = {}; +}; + +std::filesystem::path ResolveRepoRootPath() { + std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT; + if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { + root = root.substr(1u, root.size() - 2u); + } + return std::filesystem::path(root).lexically_normal(); +} + +std::filesystem::path ResolveValidationThemePath() { + return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme") + .lexically_normal(); +} + +std::int32_t MapVectorKey(UINT keyCode) { + switch (keyCode) { + case VK_LEFT: return static_cast(KeyCode::Left); + case VK_RIGHT: return static_cast(KeyCode::Right); + case VK_UP: return static_cast(KeyCode::Up); + case VK_DOWN: return static_cast(KeyCode::Down); + case VK_HOME: return static_cast(KeyCode::Home); + case VK_END: return static_cast(KeyCode::End); + case VK_TAB: return static_cast(KeyCode::Tab); + case VK_RETURN: return static_cast(KeyCode::Enter); + case VK_ESCAPE: return static_cast(KeyCode::Escape); + default: return static_cast(KeyCode::None); + } +} + +ScenarioLayout BuildScenarioLayout( + float width, + float height, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) { + const float margin = shellMetrics.margin; + constexpr float leftWidth = 470.0f; + const float gap = shellMetrics.gap; + + ScenarioLayout layout = {}; + layout.introRect = UIRect(margin, margin, leftWidth, 230.0f); + layout.stateRect = UIRect( + margin, + layout.introRect.y + layout.introRect.height + gap, + leftWidth, + (std::max)(280.0f, height - (layout.introRect.y + layout.introRect.height + gap) - margin)); + layout.previewRect = UIRect( + leftWidth + margin * 2.0f, + margin, + (std::max)(480.0f, width - leftWidth - margin * 3.0f), + height - margin * 2.0f); + layout.inspectorRect = UIRect(layout.previewRect.x + 18.0f, layout.previewRect.y + 54.0f, 460.0f, 174.0f); + layout.inspectorHeaderRect = UIRect(layout.inspectorRect.x, layout.inspectorRect.y, layout.inspectorRect.width, 24.0f); + layout.sectionRect = UIRect(layout.inspectorRect.x, layout.inspectorRect.y + 24.0f, layout.inspectorRect.width, 24.0f); + layout.fieldRect = UIRect(layout.inspectorRect.x, layout.sectionRect.y + 26.0f, layout.inspectorRect.width, 22.0f); + return layout; +} + +UIInputEvent MakePointerEvent( + UIInputEventType type, + const UIPoint& position, + UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = position; + event.pointerButton = button; + return event; +} + +UIInputEvent MakeKeyEvent(std::int32_t keyCode, bool shift = false) { + UIInputEvent event = {}; + event.type = UIInputEventType::KeyDown; + event.keyCode = keyCode; + event.modifiers.shift = shift; + return event; +} + +UIInputEvent MakeCharacterEvent(wchar_t character) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(character); + return event; +} + +std::string DescribeHitTarget(const UIEditorVector4FieldHitTarget& hitTarget) { + if (hitTarget.kind == UIEditorVector4FieldHitTargetKind::Component) { + return std::string("component_") + std::to_string(hitTarget.componentIndex); + } + if (hitTarget.kind == UIEditorVector4FieldHitTargetKind::Row) { + return "row"; + } + return "none"; +} + +std::string DescribeSelectedComponent(std::size_t componentIndex) { + switch (componentIndex) { + case 0u: return "X"; + case 1u: return "Y"; + case 2u: return "Z"; + case 3u: return "W"; + default: return "none"; + } +} + +class ScenarioApp { +public: + int Run(HINSTANCE hInstance, int nCmdShow) { + if (!Initialize(hInstance, nCmdShow)) { + Shutdown(); + return 1; + } + MSG message = {}; + while (message.message != WM_QUIT) { + if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + TranslateMessage(&message); + DispatchMessageW(&message); + continue; + } + RenderFrame(); + Sleep(8); + } + Shutdown(); + return static_cast(message.wParam); + } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->m_renderer.Resize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + case WM_MOUSEMOVE: + if (app != nullptr) { + app->m_mousePosition = UIPoint(static_cast(GET_X_LPARAM(lParam)), static_cast(GET_Y_LPARAM(lParam))); + TRACKMOUSEEVENT trackEvent = { sizeof(trackEvent), TME_LEAVE, hwnd, 0 }; + TrackMouseEvent(&trackEvent); + app->PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, app->m_mousePosition) }); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + break; + case WM_MOUSELEAVE: + if (app != nullptr) { + app->m_mousePosition = UIPoint(-1000.0f, -1000.0f); + app->PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, app->m_mousePosition) }); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + break; + case WM_LBUTTONDOWN: + case WM_LBUTTONUP: + if (app != nullptr) { + app->m_mousePosition = UIPoint(static_cast(GET_X_LPARAM(lParam)), static_cast(GET_Y_LPARAM(lParam))); + const UIInputEventType type = + message == WM_LBUTTONDOWN ? UIInputEventType::PointerButtonDown : UIInputEventType::PointerButtonUp; + app->UpdateResultText(app->PumpEvents({ MakePointerEvent(type, app->m_mousePosition, UIPointerButton::Left) })); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + break; + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr) { + if (wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + app->m_lastResult = "已请求截图,输出到 captures/latest.png"; + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + if (wParam == 'R') { + app->ResetScenario(); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + if (wParam == VK_F6) { + app->UpdateResultText(app->PumpEvents({ MakePointerEvent(UIInputEventType::FocusLost, app->m_mousePosition) })); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + const std::int32_t keyCode = MapVectorKey(static_cast(wParam)); + if (keyCode != static_cast(KeyCode::None)) { + app->UpdateResultText(app->PumpEvents({ MakeKeyEvent(keyCode, (GetKeyState(VK_SHIFT) & 0x8000) != 0) })); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + } + break; + case WM_CHAR: + if (app != nullptr && wParam >= 32) { + app->UpdateResultText(app->PumpEvents({ MakeCharacterEvent(static_cast(wParam)) })); + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + break; + case WM_PAINT: + if (app != nullptr) { + PAINTSTRUCT paintStruct = {}; + BeginPaint(hwnd, &paintStruct); + app->RenderFrame(); + EndPaint(hwnd, &paintStruct); + return 0; + } + break; + case WM_ERASEBKGND: + return 1; + case WM_DESTROY: + if (app != nullptr) { + app->m_hwnd = nullptr; + } + PostQuitMessage(0); + return 0; + default: + break; + } + return DefWindowProcW(hwnd, message, wParam, lParam); + } + + bool Initialize(HINSTANCE hInstance, int nCmdShow) { + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(windowClass); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = &ScenarioApp::WndProc; + windowClass.hInstance = hInstance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.lpszClassName = kWindowClassName; + m_windowClassAtom = RegisterClassExW(&windowClass); + if (m_windowClassAtom == 0) { + return false; + } + m_hwnd = CreateWindowExW(0, kWindowClassName, kWindowTitle, WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, 1560, 920, nullptr, nullptr, hInstance, this); + if (m_hwnd == nullptr || !m_renderer.Initialize(m_hwnd)) { + return false; + } + ShowWindow(m_hwnd, nCmdShow); + UpdateWindow(m_hwnd); + m_captureRoot = ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/vector4_field_basic/captures"; + m_autoScreenshot.Initialize(m_captureRoot); + const auto themeLoad = XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath()); + m_theme = themeLoad.theme; + m_themeStatus = themeLoad.succeeded ? "loaded" : (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); + } + if (m_windowClassAtom != 0) { + UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr)); + } + } + + void ResetScenario() { + m_spec = {}; + m_spec.fieldId = "rotation"; + m_spec.label = "Rotation"; + m_spec.values = { 1.25, -2.5, 4.75, 0.5 }; + m_spec.step = 0.25; + m_spec.minValue = -10.0; + m_spec.maxValue = 10.0; + m_interactionState = {}; + m_interactionState.vector4FieldState.focused = true; + m_interactionState.vector4FieldState.selectedComponentIndex = 0u; + m_mousePosition = UIPoint(-1000.0f, -1000.0f); + m_lastResult = "已重置到默认 Vector4Field 状态"; + PumpEvents({}); + } + + UIEditorVector4FieldInteractionResult PumpEvents(std::vector events) { + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const auto layout = BuildScenarioLayout( + static_cast((std::max)(1L, clientRect.right - clientRect.left)), + static_cast((std::max)(1L, clientRect.bottom - clientRect.top)), + XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme)); + const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector4FieldMetrics(m_theme); + m_frame = UpdateUIEditorVector4FieldInteraction( + m_interactionState, + m_spec, + layout.fieldRect, + std::move(events), + metrics); + return m_frame.result; + } + + void UpdateResultText(const UIEditorVector4FieldInteractionResult& result) { + if (result.editCommitRejected) { + m_lastResult = "提交失败,当前文本不是合法数字"; + } else if (result.editCommitted) { + m_lastResult = "已提交 " + DescribeSelectedComponent(result.changedComponentIndex) + " = " + result.committedText; + } else if (result.editCanceled) { + m_lastResult = "已取消编辑"; + } else if (result.editStarted) { + m_lastResult = "开始编辑 component " + DescribeSelectedComponent(result.selectedComponentIndex); + } else if (result.stepApplied || result.valueChanged) { + m_lastResult = "数值已更新,当前 component = " + DescribeSelectedComponent(result.changedComponentIndex); + } else if (result.selectionChanged) { + m_lastResult = "已切换选中 component: " + DescribeSelectedComponent(result.selectedComponentIndex); + } else if (result.focusChanged) { + m_lastResult = std::string("焦点变化: ") + (m_interactionState.vector4FieldState.focused ? "focused" : "lost"); + } else if (result.consumed) { + m_lastResult = "控件已消费输入"; + } + } + + void RenderFrame() { + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((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 auto layout = BuildScenarioLayout(width, height, shellMetrics); + PumpEvents({}); + + const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector4FieldMetrics(m_theme); + const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector4FieldPalette(m_theme); + const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme); + const auto currentHit = HitTestUIEditorVector4Field(m_frame.layout, m_mousePosition); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector4FieldBasic"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground); + + drawList.AddFilledRect(layout.introRect, shellPalette.cardBackground, shellMetrics.cardRadius); + drawList.AddRectOutline(layout.introRect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius); + drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 14.0f), "这个测试验证什么功能?", shellPalette.textPrimary, shellMetrics.titleFontSize); + drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 40.0f), "只验证 UIEditorVector4Field 的四通道编辑契约。", shellPalette.textMuted, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f), "1. 点击 X/Y/Z/W 的 value box,检查 selected component 和 editing。", shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), "2. 按 Tab/Shift+Tab 切换 component;Up/Down/Home/End 检查 step 与边界。", shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), "3. Enter 开始编辑;直接输入字符也应开始编辑;Enter commit,Escape cancel。", shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), "4. F6 模拟 FocusLost;F12 截图;R 重置当前测试。", shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f), "5. 重点检查 Hover / Selected / Editing / Values / Result 是否同步。", shellPalette.textPrimary, shellMetrics.bodyFontSize); + + drawList.AddFilledRect(layout.stateRect, shellPalette.cardBackground, shellMetrics.cardRadius); + drawList.AddRectOutline(layout.stateRect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius); + drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 14.0f), "状态摘要", shellPalette.textPrimary, shellMetrics.titleFontSize); + drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 46.0f), "Hover: " + DescribeHitTarget(currentHit), shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f), "Selected: " + DescribeSelectedComponent(m_interactionState.vector4FieldState.selectedComponentIndex), shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f), std::string("Focused: ") + (m_interactionState.vector4FieldState.focused ? "是" : "否"), shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f), std::string("Editing: ") + (m_interactionState.vector4FieldState.editing ? "是" : "否"), shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f), "Values: X=" + FormatUIEditorVector4FieldComponentValue(m_spec, 0u) + " Y=" + FormatUIEditorVector4FieldComponentValue(m_spec, 1u) + " Z=" + FormatUIEditorVector4FieldComponentValue(m_spec, 2u) + " W=" + FormatUIEditorVector4FieldComponentValue(m_spec, 3u), shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f), "Display: X=" + m_interactionState.vector4FieldState.displayTexts[0] + " Y=" + m_interactionState.vector4FieldState.displayTexts[1] + " Z=" + m_interactionState.vector4FieldState.displayTexts[2] + " W=" + m_interactionState.vector4FieldState.displayTexts[3], shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f), "Result: " + m_lastResult, shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f), "Capture: " + (m_autoScreenshot.GetLastCaptureSummary().empty() ? std::string("F12 -> captures/") : m_autoScreenshot.GetLastCaptureSummary()), shellPalette.textWeak, shellMetrics.bodyFontSize); + drawList.AddText(UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f), "Theme: " + m_themeStatus, shellPalette.textWeak, shellMetrics.bodyFontSize); + + drawList.AddFilledRect(layout.previewRect, shellPalette.cardBackground, shellMetrics.cardRadius); + drawList.AddRectOutline(layout.previewRect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius); + drawList.AddText(UIPoint(layout.previewRect.x + 16.0f, layout.previewRect.y + 14.0f), "Vector4Field 预览", shellPalette.textPrimary, shellMetrics.titleFontSize); + drawList.AddText(UIPoint(layout.previewRect.x + 16.0f, layout.previewRect.y + 40.0f), "这里只放一个 Unity 风格的四通道字段。", shellPalette.textMuted, shellMetrics.bodyFontSize); + drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor); + drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f); + drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground); + drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f); + drawList.AddText(UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f), "Inspector", shellPalette.textPrimary, shellMetrics.bodyFontSize); + drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor); + drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f); + drawList.AddText(UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f), "v Transform", propertyPalette.sectionTextColor, shellMetrics.bodyFontSize); + AppendUIEditorVector4Field(drawList, layout.fieldRect, m_spec, m_interactionState.vector4FieldState, vectorPalette, vectorMetrics); + + const bool framePresented = m_renderer.Render(drawData); + m_autoScreenshot.CaptureIfRequested(m_renderer, drawData, static_cast(width), static_cast(height), framePresented); + } + + HWND m_hwnd = nullptr; + ATOM m_windowClassAtom = 0; + NativeRenderer m_renderer = {}; + AutoScreenshotController m_autoScreenshot = {}; + std::filesystem::path m_captureRoot = {}; + UIEditorVector4FieldSpec m_spec = {}; + UIEditorVector4FieldInteractionState m_interactionState = {}; + UIEditorVector4FieldInteractionFrame m_frame = {}; + Style::UITheme m_theme = {}; + UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); + std::string m_lastResult = "等待交互"; + std::string m_themeStatus = "fallback"; +}; + +} // namespace + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) { + return ScenarioApp().Run(hInstance, nCmdShow); +} diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index 1368446b..b76ab9f0 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -36,6 +36,8 @@ set(EDITOR_UI_UNIT_TEST_SOURCES test_ui_editor_vector2_field_interaction.cpp test_ui_editor_vector3_field.cpp test_ui_editor_vector3_field_interaction.cpp + test_ui_editor_vector4_field.cpp + test_ui_editor_vector4_field_interaction.cpp test_ui_editor_scroll_view.cpp test_ui_editor_scroll_view_interaction.cpp test_ui_editor_status_bar.cpp diff --git a/tests/UI/Editor/unit/test_ui_editor_property_grid.cpp b/tests/UI/Editor/unit/test_ui_editor_property_grid.cpp index 50047444..63344b21 100644 --- a/tests/UI/Editor/unit/test_ui_editor_property_grid.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_property_grid.cpp @@ -94,6 +94,18 @@ UIEditorPropertyGridField MakeEnumField( return field; } +UIEditorPropertyGridField MakeVector4Field( + std::string id, + std::string label, + std::array values) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Vector4; + field.vector4Value.values = values; + return field; +} + std::vector BuildSections() { return { { @@ -274,3 +286,54 @@ TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitTypedCommandsAndPopupO EXPECT_TRUE(ContainsTextCommand(drawData, "Cutout")); EXPECT_EQ(ResolveUIEditorPropertyGridFieldValueText(sections[0].fields[1]), "2000"); } + +TEST(UIEditorPropertyGridTest, Vector4FieldUsesHostedLayoutAndForegroundText) { + std::vector sections = { + { + "transform", + "Transform", + { + MakeVector4Field("rotation", "Rotation", { 1.0, 2.0, 3.0, 4.0 }) + }, + 0.0f + } + }; + UISelectionModel selectionModel = {}; + UIExpansionModel expansionModel = {}; + expansionModel.Expand("transform"); + UIPropertyEditModel propertyEditModel = {}; + UIEditorPropertyGridState state = {}; + + const auto layout = BuildUIEditorPropertyGridLayout( + UIRect(0.0f, 0.0f, 520.0f, 220.0f), + sections, + expansionModel); + + ASSERT_EQ(layout.visibleFieldIndices.size(), 1u); + EXPECT_GT(layout.fieldValueRects[0].x, layout.fieldLabelRects[0].x + layout.fieldLabelRects[0].width); + EXPECT_GT(layout.fieldValueRects[0].width, 200.0f); + + XCEngine::UI::UIDrawData drawData = {}; + auto& drawList = drawData.EmplaceDrawList("PropertyGridVector4"); + AppendUIEditorPropertyGridBackground( + drawList, + layout, + sections, + selectionModel, + propertyEditModel, + state); + AppendUIEditorPropertyGridForeground( + drawList, + layout, + sections, + state, + propertyEditModel); + + EXPECT_TRUE(ContainsTextCommand(drawData, "Rotation")); + EXPECT_TRUE(ContainsTextCommand(drawData, "X")); + EXPECT_TRUE(ContainsTextCommand(drawData, "Y")); + EXPECT_TRUE(ContainsTextCommand(drawData, "Z")); + EXPECT_TRUE(ContainsTextCommand(drawData, "W")); + EXPECT_TRUE(ContainsTextCommand(drawData, "4")); + EXPECT_EQ(ResolveUIEditorPropertyGridFieldValueText(sections[0].fields[0]), "1, 2, 3, 4"); +} diff --git a/tests/UI/Editor/unit/test_ui_editor_theme.cpp b/tests/UI/Editor/unit/test_ui_editor_theme.cpp index 93592156..1e578bbd 100644 --- a/tests/UI/Editor/unit/test_ui_editor_theme.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_theme.cpp @@ -80,6 +80,7 @@ Style::UITheme BuildEditorFieldTheme() { 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.vector_axis_w", Style::UIStyleValue(Math::Color(0.76f, 0.66f, 0.42f, 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))); @@ -225,6 +226,16 @@ TEST(UIEditorThemeTest, FieldResolversReadEditorThemeTokens) { EXPECT_FLOAT_EQ(vector3Palette.componentFocusedBorderColor.r, 0.64f); EXPECT_FLOAT_EQ(vector3Palette.axisZColor.b, 0.82f); + const auto vector4Metrics = Editor::ResolveUIEditorVector4FieldMetrics(theme); + const auto vector4Palette = Editor::ResolveUIEditorVector4FieldPalette(theme); + EXPECT_FLOAT_EQ(vector4Metrics.componentGap, 6.0f); + EXPECT_FLOAT_EQ(vector4Metrics.componentPrefixWidth, 18.0f); + EXPECT_FLOAT_EQ(vector4Metrics.controlTrailingInset, 9.0f); + EXPECT_FLOAT_EQ(vector4Metrics.componentLabelGap, 5.0f); + EXPECT_FLOAT_EQ(vector4Metrics.prefixFontSize, 10.0f); + EXPECT_FLOAT_EQ(vector4Palette.componentFocusedBorderColor.r, 0.64f); + EXPECT_FLOAT_EQ(vector4Palette.axisWColor.r, 0.76f); + const auto enumMetrics = Editor::ResolveUIEditorEnumFieldMetrics(theme); const auto enumPalette = Editor::ResolveUIEditorEnumFieldPalette(theme); EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 9.0f); @@ -359,6 +370,15 @@ TEST(UIEditorThemeTest, HostedFieldBuildersInheritPropertyGridMetricsAndPalette) EXPECT_FLOAT_EQ(vector3Palette.componentEditingColor.r, 0.4f); EXPECT_FLOAT_EQ(vector3Palette.componentFocusedBorderColor.r, 0.55f); + const auto vector4Metrics = Editor::BuildUIEditorHostedVector4FieldMetrics(propertyMetrics); + const auto vector4Palette = Editor::BuildUIEditorHostedVector4FieldPalette(propertyPalette); + EXPECT_FLOAT_EQ(vector4Metrics.controlInsetY, 2.0f); + EXPECT_FLOAT_EQ(vector4Metrics.controlTrailingInset, 5.0f); + EXPECT_FLOAT_EQ(vector4Metrics.valueTextInsetX, 5.0f); + EXPECT_FLOAT_EQ(vector4Metrics.componentRounding, 2.0f); + EXPECT_FLOAT_EQ(vector4Palette.componentEditingColor.r, 0.4f); + EXPECT_FLOAT_EQ(vector4Palette.componentFocusedBorderColor.r, 0.55f); + const auto enumMetrics = Editor::BuildUIEditorHostedEnumFieldMetrics(propertyMetrics); const auto enumPalette = Editor::BuildUIEditorHostedEnumFieldPalette(propertyPalette); EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 5.0f); diff --git a/tests/UI/Editor/unit/test_ui_editor_vector4_field.cpp b/tests/UI/Editor/unit/test_ui_editor_vector4_field.cpp new file mode 100644 index 00000000..3db23c79 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_vector4_field.cpp @@ -0,0 +1,61 @@ +#include + +#include + +namespace { + +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Widgets::BuildUIEditorVector4FieldLayout; +using XCEngine::UI::Editor::Widgets::FormatUIEditorVector4FieldComponentValue; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector4Field; +using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldSpec; + +TEST(UIEditorVector4FieldTest, FormatSupportsPerComponentDisplay) { + UIEditorVector4FieldSpec spec = {}; + spec.fieldId = "rotation"; + spec.label = "Rotation"; + spec.values = { 1.25, -3.5, 8.0, 0.125 }; + + EXPECT_EQ(FormatUIEditorVector4FieldComponentValue(spec, 0u), "1.25"); + EXPECT_EQ(FormatUIEditorVector4FieldComponentValue(spec, 1u), "-3.5"); + EXPECT_EQ(FormatUIEditorVector4FieldComponentValue(spec, 2u), "8"); + EXPECT_EQ(FormatUIEditorVector4FieldComponentValue(spec, 3u), "0.125"); +} + +TEST(UIEditorVector4FieldTest, LayoutBuildsFourComponentRects) { + UIEditorVector4FieldSpec spec = {}; + spec.fieldId = "rotation"; + spec.label = "Rotation"; + const auto layout = BuildUIEditorVector4FieldLayout(UIRect(0.0f, 0.0f, 620.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_GT(layout.componentRects[3].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); + EXPECT_LT(layout.componentRects[2].x + layout.componentRects[2].width, layout.componentRects[3].x); +} + +TEST(UIEditorVector4FieldTest, HitTestResolvesComponentAndRow) { + UIEditorVector4FieldSpec spec = {}; + spec.fieldId = "rotation"; + spec.label = "Rotation"; + const auto layout = BuildUIEditorVector4FieldLayout(UIRect(0.0f, 0.0f, 620.0f, 32.0f), spec); + + const auto fourthHit = HitTestUIEditorVector4Field( + layout, + UIPoint(layout.componentRects[3].x + 4.0f, layout.componentRects[3].y + 4.0f)); + EXPECT_EQ(fourthHit.kind, UIEditorVector4FieldHitTargetKind::Component); + EXPECT_EQ(fourthHit.componentIndex, 3u); + + const auto rowHit = HitTestUIEditorVector4Field( + layout, + UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f)); + EXPECT_EQ(rowHit.kind, UIEditorVector4FieldHitTargetKind::Row); +} + +} // namespace diff --git a/tests/UI/Editor/unit/test_ui_editor_vector4_field_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_vector4_field_interaction.cpp new file mode 100644 index 00000000..25cc6405 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_vector4_field_interaction.cpp @@ -0,0 +1,165 @@ +#include + +#include + +#include + +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::UIEditorVector4FieldInteractionState; +using XCEngine::UI::Editor::UpdateUIEditorVector4FieldInteraction; +using XCEngine::UI::Editor::Widgets::UIEditorVector4FieldSpec; + +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(keyCode); + return event; +} + +UIInputEvent MakeCharacter(char character) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(character); + return event; +} + +} // namespace + +TEST(UIEditorVector4FieldInteractionTest, ClickFourthComponentStartsEditing) { + UIEditorVector4FieldSpec spec = {}; + spec.fieldId = "rotation"; + spec.label = "Rotation"; + spec.values = { 1.0, 2.0, 3.0, 4.0 }; + UIEditorVector4FieldInteractionState state = {}; + + auto frame = UpdateUIEditorVector4FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 620.0f, 32.0f), + {}); + + frame = UpdateUIEditorVector4FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 620.0f, 32.0f), + { + MakePointer( + UIInputEventType::PointerButtonDown, + frame.layout.componentRects[3].x + 4.0f, + frame.layout.componentRects[3].y + 4.0f, + UIPointerButton::Left), + MakePointer( + UIInputEventType::PointerButtonUp, + frame.layout.componentRects[3].x + 4.0f, + frame.layout.componentRects[3].y + 4.0f, + UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.vector4FieldState.editing); + EXPECT_EQ(state.vector4FieldState.selectedComponentIndex, 3u); +} + +TEST(UIEditorVector4FieldInteractionTest, TabSelectsNextComponentAndArrowAppliesStep) { + UIEditorVector4FieldSpec spec = {}; + spec.fieldId = "rotation"; + spec.label = "Rotation"; + spec.values = { 1.0, 2.0, 3.0, 4.0 }; + spec.step = 0.5; + UIEditorVector4FieldInteractionState state = {}; + state.vector4FieldState.focused = true; + state.vector4FieldState.selectedComponentIndex = 2u; + + auto frame = UpdateUIEditorVector4FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 620.0f, 32.0f), + { MakeKey(KeyCode::Tab) }); + EXPECT_TRUE(frame.result.selectionChanged); + EXPECT_EQ(state.vector4FieldState.selectedComponentIndex, 3u); + + frame = UpdateUIEditorVector4FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 620.0f, 32.0f), + { MakeKey(KeyCode::Up) }); + EXPECT_TRUE(frame.result.stepApplied); + EXPECT_EQ(frame.result.changedComponentIndex, 3u); + EXPECT_DOUBLE_EQ(spec.values[3], 4.5); +} + +TEST(UIEditorVector4FieldInteractionTest, EnterStartsEditingAndCommitUpdatesSelectedComponent) { + UIEditorVector4FieldSpec spec = {}; + spec.fieldId = "rotation"; + spec.label = "Rotation"; + spec.values = { 1.0, 2.0, 3.0, 4.0 }; + spec.integerMode = true; + UIEditorVector4FieldInteractionState state = {}; + state.vector4FieldState.focused = true; + state.vector4FieldState.selectedComponentIndex = 2u; + + auto frame = UpdateUIEditorVector4FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 620.0f, 32.0f), + { MakeKey(KeyCode::Enter) }); + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.vector4FieldState.editing); + + frame = UpdateUIEditorVector4FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 620.0f, 32.0f), + { MakeCharacter('7'), MakeKey(KeyCode::Enter) }); + + EXPECT_TRUE(frame.result.editCommitted); + EXPECT_TRUE(frame.result.valueChanged); + EXPECT_EQ(frame.result.changedComponentIndex, 2u); + EXPECT_DOUBLE_EQ(spec.values[0], 1.0); + EXPECT_DOUBLE_EQ(spec.values[1], 2.0); + EXPECT_DOUBLE_EQ(spec.values[2], 37.0); + EXPECT_DOUBLE_EQ(spec.values[3], 4.0); +} + +TEST(UIEditorVector4FieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) { + UIEditorVector4FieldSpec spec = {}; + spec.fieldId = "rotation"; + spec.label = "Rotation"; + spec.values = { 1.0, 2.0, 3.0, 4.0 }; + UIEditorVector4FieldInteractionState state = {}; + state.vector4FieldState.focused = true; + state.vector4FieldState.selectedComponentIndex = 0u; + + auto frame = UpdateUIEditorVector4FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 620.0f, 32.0f), + { MakeCharacter('9') }); + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.vector4FieldState.editing); + EXPECT_EQ(state.vector4FieldState.displayTexts[0], "9"); + + frame = UpdateUIEditorVector4FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 620.0f, 32.0f), + { MakeKey(KeyCode::Escape) }); + EXPECT_TRUE(frame.result.editCanceled); + EXPECT_FALSE(state.vector4FieldState.editing); + EXPECT_DOUBLE_EQ(spec.values[0], 1.0); +}