ui: add vector4 editor field validation
This commit is contained in:
@@ -35,6 +35,7 @@ add_library(XCUIEditorLib STATIC
|
|||||||
src/Core/UIEditorTreeViewInteraction.cpp
|
src/Core/UIEditorTreeViewInteraction.cpp
|
||||||
src/Core/UIEditorVector2FieldInteraction.cpp
|
src/Core/UIEditorVector2FieldInteraction.cpp
|
||||||
src/Core/UIEditorVector3FieldInteraction.cpp
|
src/Core/UIEditorVector3FieldInteraction.cpp
|
||||||
|
src/Core/UIEditorVector4FieldInteraction.cpp
|
||||||
src/Core/UIEditorViewportInputBridge.cpp
|
src/Core/UIEditorViewportInputBridge.cpp
|
||||||
src/Core/UIEditorViewportShell.cpp
|
src/Core/UIEditorViewportShell.cpp
|
||||||
src/Core/UIEditorWorkspaceCompose.cpp
|
src/Core/UIEditorWorkspaceCompose.cpp
|
||||||
@@ -61,6 +62,7 @@ add_library(XCUIEditorLib STATIC
|
|||||||
src/Widgets/UIEditorTreeView.cpp
|
src/Widgets/UIEditorTreeView.cpp
|
||||||
src/Widgets/UIEditorVector2Field.cpp
|
src/Widgets/UIEditorVector2Field.cpp
|
||||||
src/Widgets/UIEditorVector3Field.cpp
|
src/Widgets/UIEditorVector3Field.cpp
|
||||||
|
src/Widgets/UIEditorVector4Field.cpp
|
||||||
src/Widgets/UIEditorViewportSlot.cpp
|
src/Widgets/UIEditorViewportSlot.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <XCEditor/Widgets/UIEditorTextField.h>
|
#include <XCEditor/Widgets/UIEditorTextField.h>
|
||||||
#include <XCEditor/Widgets/UIEditorVector2Field.h>
|
#include <XCEditor/Widgets/UIEditorVector2Field.h>
|
||||||
#include <XCEditor/Widgets/UIEditorVector3Field.h>
|
#include <XCEditor/Widgets/UIEditorVector3Field.h>
|
||||||
|
#include <XCEditor/Widgets/UIEditorVector4Field.h>
|
||||||
|
|
||||||
#include <XCEngine/UI/Style/Theme.h>
|
#include <XCEngine/UI/Style/Theme.h>
|
||||||
|
|
||||||
@@ -65,6 +66,14 @@ Widgets::UIEditorVector3FieldPalette ResolveUIEditorVector3FieldPalette(
|
|||||||
const ::XCEngine::UI::Style::UITheme& theme,
|
const ::XCEngine::UI::Style::UITheme& theme,
|
||||||
const Widgets::UIEditorVector3FieldPalette& fallback = {});
|
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(
|
Widgets::UIEditorEnumFieldMetrics ResolveUIEditorEnumFieldMetrics(
|
||||||
const ::XCEngine::UI::Style::UITheme& theme,
|
const ::XCEngine::UI::Style::UITheme& theme,
|
||||||
const Widgets::UIEditorEnumFieldMetrics& fallback = {});
|
const Widgets::UIEditorEnumFieldMetrics& fallback = {});
|
||||||
@@ -129,6 +138,14 @@ Widgets::UIEditorVector3FieldPalette BuildUIEditorHostedVector3FieldPalette(
|
|||||||
const Widgets::UIEditorPropertyGridPalette& propertyGridPalette,
|
const Widgets::UIEditorPropertyGridPalette& propertyGridPalette,
|
||||||
const Widgets::UIEditorVector3FieldPalette& fallback = {});
|
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(
|
Widgets::UIEditorEnumFieldMetrics BuildUIEditorHostedEnumFieldMetrics(
|
||||||
const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics,
|
const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics,
|
||||||
const Widgets::UIEditorEnumFieldMetrics& fallback = {});
|
const Widgets::UIEditorEnumFieldMetrics& fallback = {});
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEditor/Widgets/UIEditorVector4Field.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Text/UITextInputController.h>
|
||||||
|
#include <XCEngine/UI/Types.h>
|
||||||
|
#include <XCEngine/UI/Widgets/UIPropertyEditModel.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<double, 4u> valuesBefore = { 0.0, 0.0, 0.0, 0.0 };
|
||||||
|
std::array<double, 4u> 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
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
|
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -21,7 +22,10 @@ enum class UIEditorPropertyGridFieldKind : std::uint8_t {
|
|||||||
Text = 0,
|
Text = 0,
|
||||||
Bool,
|
Bool,
|
||||||
Number,
|
Number,
|
||||||
Enum
|
Enum,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
Vector4
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class UIEditorPropertyGridHitTargetKind : std::uint8_t {
|
enum class UIEditorPropertyGridHitTargetKind : std::uint8_t {
|
||||||
@@ -54,6 +58,42 @@ struct UIEditorPropertyGridEnumFieldValue {
|
|||||||
std::size_t selectedIndex = 0u;
|
std::size_t selectedIndex = 0u;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct UIEditorPropertyGridVector2FieldValue {
|
||||||
|
std::array<double, 2u> values = { 0.0, 0.0 };
|
||||||
|
std::array<std::string, 2u> 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<double, 3u> values = { 0.0, 0.0, 0.0 };
|
||||||
|
std::array<std::string, 3u> 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<double, 4u> values = { 0.0, 0.0, 0.0, 0.0 };
|
||||||
|
std::array<std::string, 4u> 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 {
|
struct UIEditorPropertyGridField {
|
||||||
std::string fieldId = {};
|
std::string fieldId = {};
|
||||||
std::string label = {};
|
std::string label = {};
|
||||||
@@ -64,6 +104,9 @@ struct UIEditorPropertyGridField {
|
|||||||
bool boolValue = false;
|
bool boolValue = false;
|
||||||
UIEditorPropertyGridNumberFieldValue numberValue = {};
|
UIEditorPropertyGridNumberFieldValue numberValue = {};
|
||||||
UIEditorPropertyGridEnumFieldValue enumValue = {};
|
UIEditorPropertyGridEnumFieldValue enumValue = {};
|
||||||
|
UIEditorPropertyGridVector2FieldValue vector2Value = {};
|
||||||
|
UIEditorPropertyGridVector3FieldValue vector3Value = {};
|
||||||
|
UIEditorPropertyGridVector4FieldValue vector4Value = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UIEditorPropertyGridSection {
|
struct UIEditorPropertyGridSection {
|
||||||
|
|||||||
186
new_editor/include/XCEditor/Widgets/UIEditorVector4Field.h
Normal file
186
new_editor/include/XCEditor/Widgets/UIEditorVector4Field.h
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::Widgets {
|
||||||
|
|
||||||
|
inline constexpr std::size_t UIEditorVector4FieldInvalidComponentIndex = static_cast<std::size_t>(-1);
|
||||||
|
|
||||||
|
enum class UIEditorVector4FieldHitTargetKind : std::uint8_t {
|
||||||
|
None = 0,
|
||||||
|
Row,
|
||||||
|
Component
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorVector4FieldSpec {
|
||||||
|
std::string fieldId = {};
|
||||||
|
std::string label = {};
|
||||||
|
std::array<double, 4u> values = { 0.0, 0.0, 0.0, 0.0 };
|
||||||
|
std::array<std::string, 4u> 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<std::string, 4u> 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
|
||||||
@@ -636,6 +636,145 @@ Widgets::UIEditorVector3FieldPalette ResolveUIEditorVector3FieldPalette(
|
|||||||
return palette;
|
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(
|
Widgets::UIEditorEnumFieldMetrics ResolveUIEditorEnumFieldMetrics(
|
||||||
const UITheme& theme,
|
const UITheme& theme,
|
||||||
const Widgets::UIEditorEnumFieldMetrics& fallback) {
|
const Widgets::UIEditorEnumFieldMetrics& fallback) {
|
||||||
@@ -1386,6 +1525,51 @@ Widgets::UIEditorVector3FieldPalette BuildUIEditorHostedVector3FieldPalette(
|
|||||||
return hosted;
|
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(
|
Widgets::UIEditorEnumFieldMetrics BuildUIEditorHostedEnumFieldMetrics(
|
||||||
const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics,
|
const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics,
|
||||||
const Widgets::UIEditorEnumFieldMetrics& fallback) {
|
const Widgets::UIEditorEnumFieldMetrics& fallback) {
|
||||||
|
|||||||
563
new_editor/src/Core/UIEditorVector4FieldInteraction.cpp
Normal file
563
new_editor/src/Core/UIEditorVector4FieldInteraction.cpp
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
#include <XCEditor/Core/UIEditorVector4FieldInteraction.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
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<std::uint32_t>('0') &&
|
||||||
|
character <= static_cast<std::uint32_t>('9')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character == static_cast<std::uint32_t>('-') ||
|
||||||
|
character == static_cast<std::uint32_t>('+')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !spec.integerMode && character == static_cast<std::uint32_t>('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
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<UIInputEvent>& 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<std::int32_t>(KeyCode::Escape)) {
|
||||||
|
CancelEdit(state, spec, eventResult);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.keyCode == static_cast<std::int32_t>(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<KeyCode>(event.keyCode)) {
|
||||||
|
case KeyCode::Left:
|
||||||
|
MoveSelection(state, -1, eventResult);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyCode::Right:
|
||||||
|
case KeyCode::Tab:
|
||||||
|
MoveSelection(
|
||||||
|
state,
|
||||||
|
static_cast<KeyCode>(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
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||||
#include <XCEditor/Widgets/UIEditorTextField.h>
|
#include <XCEditor/Widgets/UIEditorTextField.h>
|
||||||
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
||||||
|
#include <XCEditor/Widgets/UIEditorVector2Field.h>
|
||||||
|
#include <XCEditor/Widgets/UIEditorVector3Field.h>
|
||||||
|
#include <XCEditor/Widgets/UIEditorVector4Field.h>
|
||||||
|
|
||||||
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
|
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
|
||||||
|
|
||||||
@@ -94,6 +97,48 @@ UIEditorTextFieldSpec BuildTextFieldSpec(const UIEditorPropertyGridField& field)
|
|||||||
return spec;
|
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 {
|
struct UIEditorPropertyGridFieldRects {
|
||||||
UIRect labelRect = {};
|
UIRect labelRect = {};
|
||||||
UIRect valueRect = {};
|
UIRect valueRect = {};
|
||||||
@@ -128,6 +173,30 @@ UIEditorPropertyGridFieldRects ResolveFieldRects(
|
|||||||
return { fieldLayout.labelRect, fieldLayout.valueRect };
|
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:
|
case UIEditorPropertyGridFieldKind::Text:
|
||||||
default: {
|
default: {
|
||||||
const UIEditorTextFieldLayout fieldLayout = BuildUIEditorTextFieldLayout(
|
const UIEditorTextFieldLayout fieldLayout = BuildUIEditorTextFieldLayout(
|
||||||
@@ -199,6 +268,36 @@ UIEditorTextFieldHitTargetKind ResolveTextHoveredTarget(
|
|||||||
: UIEditorTextFieldHitTargetKind::Row;
|
: 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<UIEditorMenuPopupItem> BuildEnumPopupItems(
|
std::vector<UIEditorMenuPopupItem> BuildEnumPopupItems(
|
||||||
const UIEditorPropertyGridField& field) {
|
const UIEditorPropertyGridField& field) {
|
||||||
std::vector<UIEditorMenuPopupItem> items = {};
|
std::vector<UIEditorMenuPopupItem> items = {};
|
||||||
@@ -280,6 +379,27 @@ bool BuildEnumPopupRuntime(
|
|||||||
return true;
|
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
|
} // namespace
|
||||||
|
|
||||||
bool IsUIEditorPropertyGridPointInside(
|
bool IsUIEditorPropertyGridPointInside(
|
||||||
@@ -330,6 +450,15 @@ std::string ResolveUIEditorPropertyGridFieldValueText(
|
|||||||
case UIEditorPropertyGridFieldKind::Enum:
|
case UIEditorPropertyGridFieldKind::Enum:
|
||||||
return ResolveUIEditorEnumFieldValueText(BuildEnumFieldSpec(field));
|
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:
|
case UIEditorPropertyGridFieldKind::Text:
|
||||||
default:
|
default:
|
||||||
return field.valueText;
|
return field.valueText;
|
||||||
@@ -571,6 +700,18 @@ void AppendUIEditorPropertyGridForeground(
|
|||||||
::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(metrics);
|
::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(metrics);
|
||||||
const UIEditorTextFieldPalette textPalette =
|
const UIEditorTextFieldPalette textPalette =
|
||||||
::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette(palette);
|
::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 =
|
const UIEditorEnumFieldMetrics enumMetrics =
|
||||||
::XCEngine::UI::Editor::BuildUIEditorHostedEnumFieldMetrics(metrics);
|
::XCEngine::UI::Editor::BuildUIEditorHostedEnumFieldMetrics(metrics);
|
||||||
const UIEditorEnumFieldPalette enumPalette =
|
const UIEditorEnumFieldPalette enumPalette =
|
||||||
@@ -640,6 +781,60 @@ void AppendUIEditorPropertyGridForeground(
|
|||||||
break;
|
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:
|
case UIEditorPropertyGridFieldKind::Text:
|
||||||
default: {
|
default: {
|
||||||
const std::string& displayedValue =
|
const std::string& displayedValue =
|
||||||
|
|||||||
285
new_editor/src/Widgets/UIEditorVector4Field.cpp
Normal file
285
new_editor/src/Widgets/UIEditorVector4Field.cpp
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
#include <XCEditor/Widgets/UIEditorVector4Field.h>
|
||||||
|
|
||||||
|
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||||||
|
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||||
|
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
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<float>(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<float>(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
|
||||||
@@ -34,6 +34,9 @@ endif()
|
|||||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vector3_field_basic/CMakeLists.txt")
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vector3_field_basic/CMakeLists.txt")
|
||||||
add_subdirectory(vector3_field_basic)
|
add_subdirectory(vector3_field_basic)
|
||||||
endif()
|
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")
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/enum_field_basic/CMakeLists.txt")
|
||||||
add_subdirectory(enum_field_basic)
|
add_subdirectory(enum_field_basic)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include <windowsx.h>
|
#include <windowsx.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -262,6 +263,18 @@ UIEditorPropertyGridField MakeEnumField(
|
|||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UIEditorPropertyGridField MakeVector4Field(
|
||||||
|
std::string id,
|
||||||
|
std::string label,
|
||||||
|
std::array<double, 4u> 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<UIEditorPropertyGridSection> BuildSections() {
|
std::vector<UIEditorPropertyGridSection> BuildSections() {
|
||||||
return {
|
return {
|
||||||
{
|
{
|
||||||
@@ -269,6 +282,7 @@ std::vector<UIEditorPropertyGridSection> BuildSections() {
|
|||||||
"Inspector",
|
"Inspector",
|
||||||
{
|
{
|
||||||
MakeBoolField("enabled", "Enabled", true),
|
MakeBoolField("enabled", "Enabled", true),
|
||||||
|
MakeVector4Field("rotation", "Rotation", { 0.0, 15.0, 0.0, 1.0 }),
|
||||||
MakeNumberField("render_queue", "Render Queue", 2000.0),
|
MakeNumberField("render_queue", "Render Queue", 2000.0),
|
||||||
MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u),
|
MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u),
|
||||||
MakeTextField("tag", "Tag", "Player")
|
MakeTextField("tag", "Tag", "Player")
|
||||||
@@ -871,7 +885,7 @@ private:
|
|||||||
shellMetrics.bodyFontSize);
|
shellMetrics.bodyFontSize);
|
||||||
drawList.AddText(
|
drawList.AddText(
|
||||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
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,
|
shellPalette.textPrimary,
|
||||||
shellMetrics.bodyFontSize);
|
shellMetrics.bodyFontSize);
|
||||||
drawList.AddText(
|
drawList.AddText(
|
||||||
|
|||||||
@@ -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$<$<CONFIG:Debug>: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"
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
456
tests/UI/Editor/integration/shell/vector4_field_basic/main.cpp
Normal file
456
tests/UI/Editor/integration/shell/vector4_field_basic/main.cpp
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <XCEditor/Core/UIEditorTheme.h>
|
||||||
|
#include <XCEditor/Core/UIEditorVector4FieldInteraction.h>
|
||||||
|
#include <XCEditor/Widgets/UIEditorVector4Field.h>
|
||||||
|
#include "EditorValidationTheme.h"
|
||||||
|
#include "Host/AutoScreenshot.h"
|
||||||
|
#include "Host/NativeRenderer.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
|
||||||
|
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Input::KeyCode;
|
||||||
|
using XCEngine::UI::UIDrawData;
|
||||||
|
using XCEngine::UI::UIDrawList;
|
||||||
|
using XCEngine::UI::UIInputEvent;
|
||||||
|
using XCEngine::UI::UIInputEventType;
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIPointerButton;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::Host::AutoScreenshotController;
|
||||||
|
using XCEngine::UI::Editor::Host::NativeRenderer;
|
||||||
|
using XCEngine::UI::Editor::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<std::int32_t>(KeyCode::Left);
|
||||||
|
case VK_RIGHT: return static_cast<std::int32_t>(KeyCode::Right);
|
||||||
|
case VK_UP: return static_cast<std::int32_t>(KeyCode::Up);
|
||||||
|
case VK_DOWN: return static_cast<std::int32_t>(KeyCode::Down);
|
||||||
|
case VK_HOME: return static_cast<std::int32_t>(KeyCode::Home);
|
||||||
|
case VK_END: return static_cast<std::int32_t>(KeyCode::End);
|
||||||
|
case VK_TAB: return static_cast<std::int32_t>(KeyCode::Tab);
|
||||||
|
case VK_RETURN: return static_cast<std::int32_t>(KeyCode::Enter);
|
||||||
|
case VK_ESCAPE: return static_cast<std::int32_t>(KeyCode::Escape);
|
||||||
|
default: return static_cast<std::int32_t>(KeyCode::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScenarioLayout BuildScenarioLayout(
|
||||||
|
float width,
|
||||||
|
float height,
|
||||||
|
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
|
||||||
|
const float margin = shellMetrics.margin;
|
||||||
|
constexpr float leftWidth = 470.0f;
|
||||||
|
const float gap = shellMetrics.gap;
|
||||||
|
|
||||||
|
ScenarioLayout layout = {};
|
||||||
|
layout.introRect = UIRect(margin, margin, leftWidth, 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<std::uint32_t>(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<int>(message.wParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||||
|
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
|
||||||
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||||
|
switch (message) {
|
||||||
|
case WM_SIZE:
|
||||||
|
if (app != nullptr && wParam != SIZE_MINIMIZED) {
|
||||||
|
app->m_renderer.Resize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case WM_MOUSEMOVE:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->m_mousePosition = UIPoint(static_cast<float>(GET_X_LPARAM(lParam)), static_cast<float>(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<float>(GET_X_LPARAM(lParam)), static_cast<float>(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<UINT>(wParam));
|
||||||
|
if (keyCode != static_cast<std::int32_t>(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<wchar_t>(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<UIInputEvent> events) {
|
||||||
|
RECT clientRect = {};
|
||||||
|
GetClientRect(m_hwnd, &clientRect);
|
||||||
|
const auto layout = BuildScenarioLayout(
|
||||||
|
static_cast<float>((std::max)(1L, clientRect.right - clientRect.left)),
|
||||||
|
static_cast<float>((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<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||||
|
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||||
|
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
|
||||||
|
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
|
||||||
|
const 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<unsigned int>(width), static_cast<unsigned int>(height), framePresented);
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND m_hwnd = nullptr;
|
||||||
|
ATOM m_windowClassAtom = 0;
|
||||||
|
NativeRenderer m_renderer = {};
|
||||||
|
AutoScreenshotController m_autoScreenshot = {};
|
||||||
|
std::filesystem::path m_captureRoot = {};
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -36,6 +36,8 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
|
|||||||
test_ui_editor_vector2_field_interaction.cpp
|
test_ui_editor_vector2_field_interaction.cpp
|
||||||
test_ui_editor_vector3_field.cpp
|
test_ui_editor_vector3_field.cpp
|
||||||
test_ui_editor_vector3_field_interaction.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.cpp
|
||||||
test_ui_editor_scroll_view_interaction.cpp
|
test_ui_editor_scroll_view_interaction.cpp
|
||||||
test_ui_editor_status_bar.cpp
|
test_ui_editor_status_bar.cpp
|
||||||
|
|||||||
@@ -94,6 +94,18 @@ UIEditorPropertyGridField MakeEnumField(
|
|||||||
return field;
|
return field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UIEditorPropertyGridField MakeVector4Field(
|
||||||
|
std::string id,
|
||||||
|
std::string label,
|
||||||
|
std::array<double, 4u> 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<UIEditorPropertyGridSection> BuildSections() {
|
std::vector<UIEditorPropertyGridSection> BuildSections() {
|
||||||
return {
|
return {
|
||||||
{
|
{
|
||||||
@@ -274,3 +286,54 @@ TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitTypedCommandsAndPopupO
|
|||||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Cutout"));
|
EXPECT_TRUE(ContainsTextCommand(drawData, "Cutout"));
|
||||||
EXPECT_EQ(ResolveUIEditorPropertyGridFieldValueText(sections[0].fields[1]), "2000");
|
EXPECT_EQ(ResolveUIEditorPropertyGridFieldValueText(sections[0].fields[1]), "2000");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorPropertyGridTest, Vector4FieldUsesHostedLayoutAndForegroundText) {
|
||||||
|
std::vector<UIEditorPropertyGridSection> 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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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_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_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_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", 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_hover", Style::UIStyleValue(Math::Color(0.22f, 0.22f, 0.22f, 1.0f)));
|
||||||
definition.SetToken("editor.color.field.checkbox_border", Style::UIStyleValue(Math::Color(0.33f, 0.33f, 0.33f, 1.0f)));
|
definition.SetToken("editor.color.field.checkbox_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.componentFocusedBorderColor.r, 0.64f);
|
||||||
EXPECT_FLOAT_EQ(vector3Palette.axisZColor.b, 0.82f);
|
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 enumMetrics = Editor::ResolveUIEditorEnumFieldMetrics(theme);
|
||||||
const auto enumPalette = Editor::ResolveUIEditorEnumFieldPalette(theme);
|
const auto enumPalette = Editor::ResolveUIEditorEnumFieldPalette(theme);
|
||||||
EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 9.0f);
|
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.componentEditingColor.r, 0.4f);
|
||||||
EXPECT_FLOAT_EQ(vector3Palette.componentFocusedBorderColor.r, 0.55f);
|
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 enumMetrics = Editor::BuildUIEditorHostedEnumFieldMetrics(propertyMetrics);
|
||||||
const auto enumPalette = Editor::BuildUIEditorHostedEnumFieldPalette(propertyPalette);
|
const auto enumPalette = Editor::BuildUIEditorHostedEnumFieldPalette(propertyPalette);
|
||||||
EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 5.0f);
|
EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 5.0f);
|
||||||
|
|||||||
61
tests/UI/Editor/unit/test_ui_editor_vector4_field.cpp
Normal file
61
tests/UI/Editor/unit/test_ui_editor_vector4_field.cpp
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEditor/Widgets/UIEditorVector4Field.h>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEditor/Core/UIEditorVector4FieldInteraction.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Input::KeyCode;
|
||||||
|
using XCEngine::UI::UIInputEvent;
|
||||||
|
using XCEngine::UI::UIInputEventType;
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIPointerButton;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::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<std::int32_t>(keyCode);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakeCharacter(char character) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::Character;
|
||||||
|
event.character = static_cast<std::uint32_t>(character);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user