ui: add vector4 editor field validation

This commit is contained in:
2026-04-08 03:23:15 +08:00
parent 2e961295bf
commit 7be3b2cc45
19 changed files with 2346 additions and 2 deletions

View File

@@ -35,6 +35,7 @@ add_library(XCUIEditorLib STATIC
src/Core/UIEditorTreeViewInteraction.cpp
src/Core/UIEditorVector2FieldInteraction.cpp
src/Core/UIEditorVector3FieldInteraction.cpp
src/Core/UIEditorVector4FieldInteraction.cpp
src/Core/UIEditorViewportInputBridge.cpp
src/Core/UIEditorViewportShell.cpp
src/Core/UIEditorWorkspaceCompose.cpp
@@ -61,6 +62,7 @@ add_library(XCUIEditorLib STATIC
src/Widgets/UIEditorTreeView.cpp
src/Widgets/UIEditorVector2Field.cpp
src/Widgets/UIEditorVector3Field.cpp
src/Widgets/UIEditorVector4Field.cpp
src/Widgets/UIEditorViewportSlot.cpp
)

View File

@@ -8,6 +8,7 @@
#include <XCEditor/Widgets/UIEditorTextField.h>
#include <XCEditor/Widgets/UIEditorVector2Field.h>
#include <XCEditor/Widgets/UIEditorVector3Field.h>
#include <XCEditor/Widgets/UIEditorVector4Field.h>
#include <XCEngine/UI/Style/Theme.h>
@@ -65,6 +66,14 @@ Widgets::UIEditorVector3FieldPalette ResolveUIEditorVector3FieldPalette(
const ::XCEngine::UI::Style::UITheme& theme,
const Widgets::UIEditorVector3FieldPalette& fallback = {});
Widgets::UIEditorVector4FieldMetrics ResolveUIEditorVector4FieldMetrics(
const ::XCEngine::UI::Style::UITheme& theme,
const Widgets::UIEditorVector4FieldMetrics& fallback = {});
Widgets::UIEditorVector4FieldPalette ResolveUIEditorVector4FieldPalette(
const ::XCEngine::UI::Style::UITheme& theme,
const Widgets::UIEditorVector4FieldPalette& fallback = {});
Widgets::UIEditorEnumFieldMetrics ResolveUIEditorEnumFieldMetrics(
const ::XCEngine::UI::Style::UITheme& theme,
const Widgets::UIEditorEnumFieldMetrics& fallback = {});
@@ -129,6 +138,14 @@ Widgets::UIEditorVector3FieldPalette BuildUIEditorHostedVector3FieldPalette(
const Widgets::UIEditorPropertyGridPalette& propertyGridPalette,
const Widgets::UIEditorVector3FieldPalette& fallback = {});
Widgets::UIEditorVector4FieldMetrics BuildUIEditorHostedVector4FieldMetrics(
const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics,
const Widgets::UIEditorVector4FieldMetrics& fallback = {});
Widgets::UIEditorVector4FieldPalette BuildUIEditorHostedVector4FieldPalette(
const Widgets::UIEditorPropertyGridPalette& propertyGridPalette,
const Widgets::UIEditorVector4FieldPalette& fallback = {});
Widgets::UIEditorEnumFieldMetrics BuildUIEditorHostedEnumFieldMetrics(
const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics,
const Widgets::UIEditorEnumFieldMetrics& fallback = {});

View File

@@ -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

View File

@@ -7,6 +7,7 @@
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
@@ -21,7 +22,10 @@ enum class UIEditorPropertyGridFieldKind : std::uint8_t {
Text = 0,
Bool,
Number,
Enum
Enum,
Vector2,
Vector3,
Vector4
};
enum class UIEditorPropertyGridHitTargetKind : std::uint8_t {
@@ -54,6 +58,42 @@ struct UIEditorPropertyGridEnumFieldValue {
std::size_t selectedIndex = 0u;
};
struct UIEditorPropertyGridVector2FieldValue {
std::array<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 {
std::string fieldId = {};
std::string label = {};
@@ -64,6 +104,9 @@ struct UIEditorPropertyGridField {
bool boolValue = false;
UIEditorPropertyGridNumberFieldValue numberValue = {};
UIEditorPropertyGridEnumFieldValue enumValue = {};
UIEditorPropertyGridVector2FieldValue vector2Value = {};
UIEditorPropertyGridVector3FieldValue vector3Value = {};
UIEditorPropertyGridVector4FieldValue vector4Value = {};
};
struct UIEditorPropertyGridSection {

View 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

View File

@@ -636,6 +636,145 @@ Widgets::UIEditorVector3FieldPalette ResolveUIEditorVector3FieldPalette(
return palette;
}
Widgets::UIEditorVector4FieldMetrics ResolveUIEditorVector4FieldMetrics(
const UITheme& theme,
const Widgets::UIEditorVector4FieldMetrics& fallback) {
Widgets::UIEditorVector4FieldMetrics metrics = fallback;
metrics.rowHeight = ResolveUIEditorThemeFloat(theme, "editor.size.field.row", metrics.rowHeight);
metrics.horizontalPadding =
ResolveUIEditorThemeFloat(theme, "editor.space.field.padding_x", metrics.horizontalPadding);
metrics.labelControlGap =
ResolveUIEditorThemeFloat(theme, "editor.space.field.label_gap", metrics.labelControlGap);
metrics.controlColumnStart =
ResolveUIEditorThemeFloat(theme, "editor.layout.field.control_column", metrics.controlColumnStart);
metrics.controlTrailingInset = ResolveThemeFloatAliases(
theme,
{ "editor.space.field.control_trailing_inset", "editor.space.field.padding_x" },
metrics.controlTrailingInset);
metrics.controlInsetY =
ResolveUIEditorThemeFloat(theme, "editor.space.field.control_inset_y", metrics.controlInsetY);
metrics.componentGap = ResolveThemeFloatAliases(
theme,
{ "editor.space.field.vector_component_gap", "editor.space.field.control_gap" },
metrics.componentGap);
metrics.componentMinWidth = ResolveThemeFloatAliases(
theme,
{ "editor.size.field.vector_component_min_width", "editor.size.field.control_min_width" },
metrics.componentMinWidth);
metrics.componentPrefixWidth = ResolveThemeFloatAliases(
theme,
{ "editor.size.field.vector_prefix_width", "editor.size.field.inline_prefix_width" },
metrics.componentPrefixWidth);
metrics.componentLabelGap = ResolveThemeFloatAliases(
theme,
{ "editor.space.field.vector_prefix_gap", "editor.space.field.inline_prefix_gap" },
metrics.componentLabelGap);
metrics.labelTextInsetY =
ResolveUIEditorThemeFloat(theme, "editor.space.field.label_inset_y", metrics.labelTextInsetY);
metrics.labelFontSize = ResolveThemeFloatAliases(
theme,
{ "editor.typography.field.label", "editor.font.field.label" },
metrics.labelFontSize);
metrics.valueTextInsetX =
ResolveUIEditorThemeFloat(theme, "editor.space.field.value_inset_x", metrics.valueTextInsetX);
metrics.valueTextInsetY =
ResolveUIEditorThemeFloat(theme, "editor.space.field.value_inset_y", metrics.valueTextInsetY);
metrics.valueFontSize = ResolveThemeFloatAliases(
theme,
{ "editor.typography.field.value", "editor.font.field.value" },
metrics.valueFontSize);
metrics.prefixTextInsetX = ResolveThemeFloatAliases(
theme,
{ "editor.space.field.vector_prefix_inset_x", "editor.space.field.inline_prefix_inset_x" },
metrics.prefixTextInsetX);
metrics.prefixTextInsetY = ResolveThemeFloatAliases(
theme,
{ "editor.space.field.vector_prefix_inset_y", "editor.space.field.inline_prefix_inset_y" },
metrics.prefixTextInsetY);
metrics.prefixFontSize = ResolveThemeFloatAliases(
theme,
{ "editor.typography.field.vector_prefix", "editor.font.field.vector_prefix", "editor.font.field.glyph" },
metrics.prefixFontSize);
metrics.cornerRounding =
ResolveUIEditorThemeFloat(theme, "editor.radius.field.row", metrics.cornerRounding);
metrics.componentRounding = ResolveThemeFloatAliases(
theme,
{ "editor.radius.field.vector_component", "editor.radius.field.control" },
metrics.componentRounding);
metrics.borderThickness =
ResolveUIEditorThemeFloat(theme, "editor.border.field", metrics.borderThickness);
metrics.focusedBorderThickness =
ResolveUIEditorThemeFloat(theme, "editor.border.field.focus", metrics.focusedBorderThickness);
return metrics;
}
Widgets::UIEditorVector4FieldPalette ResolveUIEditorVector4FieldPalette(
const UITheme& theme,
const Widgets::UIEditorVector4FieldPalette& fallback) {
Widgets::UIEditorVector4FieldPalette palette = fallback;
palette.surfaceColor = ResolveUIEditorThemeColor(theme, "editor.color.field.row", palette.surfaceColor);
palette.borderColor = ResolveUIEditorThemeColor(theme, "editor.color.field.border", palette.borderColor);
palette.focusedBorderColor =
ResolveUIEditorThemeColor(theme, "editor.color.field.border_focus", palette.focusedBorderColor);
palette.rowHoverColor =
ResolveUIEditorThemeColor(theme, "editor.color.field.row_hover", palette.rowHoverColor);
palette.rowActiveColor =
ResolveUIEditorThemeColor(theme, "editor.color.field.row_active", palette.rowActiveColor);
palette.componentColor =
ResolveUIEditorThemeColor(theme, "editor.color.field.control", palette.componentColor);
palette.componentHoverColor =
ResolveUIEditorThemeColor(theme, "editor.color.field.control_hover", palette.componentHoverColor);
palette.componentEditingColor = ResolveUIEditorThemeColor(
theme,
"editor.color.field.control_editing",
palette.componentEditingColor);
palette.readOnlyColor = ResolveUIEditorThemeColor(
theme,
"editor.color.field.control_readonly",
palette.readOnlyColor);
palette.componentBorderColor = ResolveUIEditorThemeColor(
theme,
"editor.color.field.control_border",
palette.componentBorderColor);
palette.componentFocusedBorderColor = ResolveThemeColorAliases(
theme,
{ "editor.color.field.control_border_focus", "editor.color.field.border_focus" },
palette.componentFocusedBorderColor);
palette.prefixColor = ResolveThemeColorAliases(
theme,
{ "editor.color.field.vector_prefix", "editor.color.field.control_hover" },
palette.prefixColor);
palette.prefixBorderColor = ResolveThemeColorAliases(
theme,
{ "editor.color.field.vector_prefix_border", "editor.color.field.control_border" },
palette.prefixBorderColor);
palette.labelColor =
ResolveUIEditorThemeColor(theme, "editor.color.field.label", palette.labelColor);
palette.valueColor =
ResolveUIEditorThemeColor(theme, "editor.color.field.value", palette.valueColor);
palette.readOnlyValueColor = ResolveUIEditorThemeColor(
theme,
"editor.color.field.value_readonly",
palette.readOnlyValueColor);
palette.axisXColor = ResolveThemeColorAliases(
theme,
{ "editor.color.field.vector_axis_x", "editor.color.field.value" },
palette.axisXColor);
palette.axisYColor = ResolveThemeColorAliases(
theme,
{ "editor.color.field.vector_axis_y", "editor.color.field.value" },
palette.axisYColor);
palette.axisZColor = ResolveThemeColorAliases(
theme,
{ "editor.color.field.vector_axis_z", "editor.color.field.value" },
palette.axisZColor);
palette.axisWColor = ResolveThemeColorAliases(
theme,
{ "editor.color.field.vector_axis_w", "editor.color.field.value" },
palette.axisWColor);
return palette;
}
Widgets::UIEditorEnumFieldMetrics ResolveUIEditorEnumFieldMetrics(
const UITheme& theme,
const Widgets::UIEditorEnumFieldMetrics& fallback) {
@@ -1386,6 +1525,51 @@ Widgets::UIEditorVector3FieldPalette BuildUIEditorHostedVector3FieldPalette(
return hosted;
}
Widgets::UIEditorVector4FieldMetrics BuildUIEditorHostedVector4FieldMetrics(
const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics,
const Widgets::UIEditorVector4FieldMetrics& fallback) {
Widgets::UIEditorVector4FieldMetrics hosted = fallback;
hosted.rowHeight = propertyGridMetrics.fieldRowHeight;
hosted.horizontalPadding = propertyGridMetrics.horizontalPadding;
hosted.labelControlGap = propertyGridMetrics.labelControlGap;
hosted.controlColumnStart = propertyGridMetrics.controlColumnStart;
hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX;
hosted.controlInsetY = propertyGridMetrics.valueBoxInsetY;
hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY;
hosted.labelFontSize = propertyGridMetrics.labelFontSize;
hosted.valueTextInsetX = propertyGridMetrics.valueBoxInsetX;
hosted.valueTextInsetY = propertyGridMetrics.valueTextInsetY;
hosted.valueFontSize = propertyGridMetrics.valueFontSize;
hosted.cornerRounding = propertyGridMetrics.cornerRounding;
hosted.componentRounding = propertyGridMetrics.valueBoxRounding;
hosted.borderThickness = propertyGridMetrics.borderThickness;
hosted.focusedBorderThickness = propertyGridMetrics.focusedBorderThickness;
return hosted;
}
Widgets::UIEditorVector4FieldPalette BuildUIEditorHostedVector4FieldPalette(
const Widgets::UIEditorPropertyGridPalette& propertyGridPalette,
const Widgets::UIEditorVector4FieldPalette& fallback) {
Widgets::UIEditorVector4FieldPalette hosted = fallback;
hosted.surfaceColor = kTransparent;
hosted.borderColor = kTransparent;
hosted.focusedBorderColor = kTransparent;
hosted.rowHoverColor = kTransparent;
hosted.rowActiveColor = kTransparent;
hosted.componentColor = propertyGridPalette.valueBoxColor;
hosted.componentHoverColor = propertyGridPalette.valueBoxHoverColor;
hosted.componentEditingColor = propertyGridPalette.valueBoxEditingColor;
hosted.readOnlyColor = propertyGridPalette.valueBoxReadOnlyColor;
hosted.componentBorderColor = propertyGridPalette.valueBoxBorderColor;
hosted.componentFocusedBorderColor = propertyGridPalette.valueBoxEditingBorderColor;
hosted.prefixColor = propertyGridPalette.valueBoxHoverColor;
hosted.prefixBorderColor = propertyGridPalette.valueBoxBorderColor;
hosted.labelColor = propertyGridPalette.labelTextColor;
hosted.valueColor = propertyGridPalette.valueTextColor;
hosted.readOnlyValueColor = propertyGridPalette.readOnlyValueTextColor;
return hosted;
}
Widgets::UIEditorEnumFieldMetrics BuildUIEditorHostedEnumFieldMetrics(
const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics,
const Widgets::UIEditorEnumFieldMetrics& fallback) {

View 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

View File

@@ -7,6 +7,9 @@
#include <XCEditor/Widgets/UIEditorNumberField.h>
#include <XCEditor/Widgets/UIEditorTextField.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>
@@ -94,6 +97,48 @@ UIEditorTextFieldSpec BuildTextFieldSpec(const UIEditorPropertyGridField& field)
return spec;
}
UIEditorVector2FieldSpec BuildVector2FieldSpec(const UIEditorPropertyGridField& field) {
UIEditorVector2FieldSpec spec = {};
spec.fieldId = field.fieldId;
spec.label = field.label;
spec.values = field.vector2Value.values;
spec.componentLabels = field.vector2Value.componentLabels;
spec.step = field.vector2Value.step;
spec.minValue = field.vector2Value.minValue;
spec.maxValue = field.vector2Value.maxValue;
spec.integerMode = field.vector2Value.integerMode;
spec.readOnly = field.readOnly;
return spec;
}
UIEditorVector3FieldSpec BuildVector3FieldSpec(const UIEditorPropertyGridField& field) {
UIEditorVector3FieldSpec spec = {};
spec.fieldId = field.fieldId;
spec.label = field.label;
spec.values = field.vector3Value.values;
spec.componentLabels = field.vector3Value.componentLabels;
spec.step = field.vector3Value.step;
spec.minValue = field.vector3Value.minValue;
spec.maxValue = field.vector3Value.maxValue;
spec.integerMode = field.vector3Value.integerMode;
spec.readOnly = field.readOnly;
return spec;
}
UIEditorVector4FieldSpec BuildVector4FieldSpec(const UIEditorPropertyGridField& field) {
UIEditorVector4FieldSpec spec = {};
spec.fieldId = field.fieldId;
spec.label = field.label;
spec.values = field.vector4Value.values;
spec.componentLabels = field.vector4Value.componentLabels;
spec.step = field.vector4Value.step;
spec.minValue = field.vector4Value.minValue;
spec.maxValue = field.vector4Value.maxValue;
spec.integerMode = field.vector4Value.integerMode;
spec.readOnly = field.readOnly;
return spec;
}
struct UIEditorPropertyGridFieldRects {
UIRect labelRect = {};
UIRect valueRect = {};
@@ -128,6 +173,30 @@ UIEditorPropertyGridFieldRects ResolveFieldRects(
return { fieldLayout.labelRect, fieldLayout.valueRect };
}
case UIEditorPropertyGridFieldKind::Vector2: {
const UIEditorVector2FieldLayout fieldLayout = BuildUIEditorVector2FieldLayout(
rowRect,
BuildVector2FieldSpec(field),
::XCEngine::UI::Editor::BuildUIEditorHostedVector2FieldMetrics(metrics));
return { fieldLayout.labelRect, fieldLayout.controlRect };
}
case UIEditorPropertyGridFieldKind::Vector3: {
const UIEditorVector3FieldLayout fieldLayout = BuildUIEditorVector3FieldLayout(
rowRect,
BuildVector3FieldSpec(field),
::XCEngine::UI::Editor::BuildUIEditorHostedVector3FieldMetrics(metrics));
return { fieldLayout.labelRect, fieldLayout.controlRect };
}
case UIEditorPropertyGridFieldKind::Vector4: {
const UIEditorVector4FieldLayout fieldLayout = BuildUIEditorVector4FieldLayout(
rowRect,
BuildVector4FieldSpec(field),
::XCEngine::UI::Editor::BuildUIEditorHostedVector4FieldMetrics(metrics));
return { fieldLayout.labelRect, fieldLayout.controlRect };
}
case UIEditorPropertyGridFieldKind::Text:
default: {
const UIEditorTextFieldLayout fieldLayout = BuildUIEditorTextFieldLayout(
@@ -199,6 +268,36 @@ UIEditorTextFieldHitTargetKind ResolveTextHoveredTarget(
: UIEditorTextFieldHitTargetKind::Row;
}
UIEditorVector2FieldHitTargetKind ResolveVector2HoveredTarget(
const UIEditorPropertyGridState& state,
const UIEditorPropertyGridField& field) {
if (state.hoveredFieldId != field.fieldId) {
return UIEditorVector2FieldHitTargetKind::None;
}
return UIEditorVector2FieldHitTargetKind::Row;
}
UIEditorVector3FieldHitTargetKind ResolveVector3HoveredTarget(
const UIEditorPropertyGridState& state,
const UIEditorPropertyGridField& field) {
if (state.hoveredFieldId != field.fieldId) {
return UIEditorVector3FieldHitTargetKind::None;
}
return UIEditorVector3FieldHitTargetKind::Row;
}
UIEditorVector4FieldHitTargetKind ResolveVector4HoveredTarget(
const UIEditorPropertyGridState& state,
const UIEditorPropertyGridField& field) {
if (state.hoveredFieldId != field.fieldId) {
return UIEditorVector4FieldHitTargetKind::None;
}
return UIEditorVector4FieldHitTargetKind::Row;
}
std::vector<UIEditorMenuPopupItem> BuildEnumPopupItems(
const UIEditorPropertyGridField& field) {
std::vector<UIEditorMenuPopupItem> items = {};
@@ -280,6 +379,27 @@ bool BuildEnumPopupRuntime(
return true;
}
std::string FormatVector2ValueText(const UIEditorPropertyGridField& field) {
const UIEditorVector2FieldSpec spec = BuildVector2FieldSpec(field);
return FormatUIEditorVector2FieldComponentValue(spec, 0u) + ", " +
FormatUIEditorVector2FieldComponentValue(spec, 1u);
}
std::string FormatVector3ValueText(const UIEditorPropertyGridField& field) {
const UIEditorVector3FieldSpec spec = BuildVector3FieldSpec(field);
return FormatUIEditorVector3FieldComponentValue(spec, 0u) + ", " +
FormatUIEditorVector3FieldComponentValue(spec, 1u) + ", " +
FormatUIEditorVector3FieldComponentValue(spec, 2u);
}
std::string FormatVector4ValueText(const UIEditorPropertyGridField& field) {
const UIEditorVector4FieldSpec spec = BuildVector4FieldSpec(field);
return FormatUIEditorVector4FieldComponentValue(spec, 0u) + ", " +
FormatUIEditorVector4FieldComponentValue(spec, 1u) + ", " +
FormatUIEditorVector4FieldComponentValue(spec, 2u) + ", " +
FormatUIEditorVector4FieldComponentValue(spec, 3u);
}
} // namespace
bool IsUIEditorPropertyGridPointInside(
@@ -330,6 +450,15 @@ std::string ResolveUIEditorPropertyGridFieldValueText(
case UIEditorPropertyGridFieldKind::Enum:
return ResolveUIEditorEnumFieldValueText(BuildEnumFieldSpec(field));
case UIEditorPropertyGridFieldKind::Vector2:
return FormatVector2ValueText(field);
case UIEditorPropertyGridFieldKind::Vector3:
return FormatVector3ValueText(field);
case UIEditorPropertyGridFieldKind::Vector4:
return FormatVector4ValueText(field);
case UIEditorPropertyGridFieldKind::Text:
default:
return field.valueText;
@@ -571,6 +700,18 @@ void AppendUIEditorPropertyGridForeground(
::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(metrics);
const UIEditorTextFieldPalette textPalette =
::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette(palette);
const UIEditorVector2FieldMetrics vector2Metrics =
::XCEngine::UI::Editor::BuildUIEditorHostedVector2FieldMetrics(metrics);
const UIEditorVector2FieldPalette vector2Palette =
::XCEngine::UI::Editor::BuildUIEditorHostedVector2FieldPalette(palette);
const UIEditorVector3FieldMetrics vector3Metrics =
::XCEngine::UI::Editor::BuildUIEditorHostedVector3FieldMetrics(metrics);
const UIEditorVector3FieldPalette vector3Palette =
::XCEngine::UI::Editor::BuildUIEditorHostedVector3FieldPalette(palette);
const UIEditorVector4FieldMetrics vector4Metrics =
::XCEngine::UI::Editor::BuildUIEditorHostedVector4FieldMetrics(metrics);
const UIEditorVector4FieldPalette vector4Palette =
::XCEngine::UI::Editor::BuildUIEditorHostedVector4FieldPalette(palette);
const UIEditorEnumFieldMetrics enumMetrics =
::XCEngine::UI::Editor::BuildUIEditorHostedEnumFieldMetrics(metrics);
const UIEditorEnumFieldPalette enumPalette =
@@ -640,6 +781,60 @@ void AppendUIEditorPropertyGridForeground(
break;
}
case UIEditorPropertyGridFieldKind::Vector2: {
UIEditorVector2FieldState fieldState = {};
fieldState.hoveredTarget = ResolveVector2HoveredTarget(state, field);
fieldState.activeTarget =
state.pressedFieldId == field.fieldId
? UIEditorVector2FieldHitTargetKind::Row
: UIEditorVector2FieldHitTargetKind::None;
fieldState.focused = state.focused;
AppendUIEditorVector2Field(
drawList,
layout.fieldRowRects[visibleFieldIndex],
BuildVector2FieldSpec(field),
fieldState,
vector2Palette,
vector2Metrics);
break;
}
case UIEditorPropertyGridFieldKind::Vector3: {
UIEditorVector3FieldState fieldState = {};
fieldState.hoveredTarget = ResolveVector3HoveredTarget(state, field);
fieldState.activeTarget =
state.pressedFieldId == field.fieldId
? UIEditorVector3FieldHitTargetKind::Row
: UIEditorVector3FieldHitTargetKind::None;
fieldState.focused = state.focused;
AppendUIEditorVector3Field(
drawList,
layout.fieldRowRects[visibleFieldIndex],
BuildVector3FieldSpec(field),
fieldState,
vector3Palette,
vector3Metrics);
break;
}
case UIEditorPropertyGridFieldKind::Vector4: {
UIEditorVector4FieldState fieldState = {};
fieldState.hoveredTarget = ResolveVector4HoveredTarget(state, field);
fieldState.activeTarget =
state.pressedFieldId == field.fieldId
? UIEditorVector4FieldHitTargetKind::Row
: UIEditorVector4FieldHitTargetKind::None;
fieldState.focused = state.focused;
AppendUIEditorVector4Field(
drawList,
layout.fieldRowRects[visibleFieldIndex],
BuildVector4FieldSpec(field),
fieldState,
vector4Palette,
vector4Metrics);
break;
}
case UIEditorPropertyGridFieldKind::Text:
default: {
const std::string& displayedValue =

View 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