diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index dfc82e6f..7a2a139d 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -30,7 +30,11 @@ add_library(XCUIEditorLib STATIC src/Core/UIEditorShellCompose.cpp src/Core/UIEditorShellInteraction.cpp src/Core/UIEditorShortcutManager.cpp + src/Core/UIEditorTextFieldInteraction.cpp + src/Core/UIEditorTheme.cpp src/Core/UIEditorTreeViewInteraction.cpp + src/Core/UIEditorVector2FieldInteraction.cpp + src/Core/UIEditorVector3FieldInteraction.cpp src/Core/UIEditorViewportInputBridge.cpp src/Core/UIEditorViewportShell.cpp src/Core/UIEditorWorkspaceCompose.cpp @@ -40,6 +44,7 @@ add_library(XCUIEditorLib STATIC src/Core/UIEditorWorkspaceModel.cpp src/Core/UIEditorWorkspaceSession.cpp src/Widgets/UIEditorCollectionPrimitives.cpp + src/Widgets/UIEditorFieldRowLayout.cpp src/Widgets/UIEditorBoolField.cpp src/Widgets/UIEditorDockHost.cpp src/Widgets/UIEditorEnumField.cpp @@ -52,7 +57,10 @@ add_library(XCUIEditorLib STATIC src/Widgets/UIEditorScrollView.cpp src/Widgets/UIEditorStatusBar.cpp src/Widgets/UIEditorTabStrip.cpp + src/Widgets/UIEditorTextField.cpp src/Widgets/UIEditorTreeView.cpp + src/Widgets/UIEditorVector2Field.cpp + src/Widgets/UIEditorVector3Field.cpp src/Widgets/UIEditorViewportSlot.cpp ) diff --git a/new_editor/include/XCEditor/Core/UIEditorEnumFieldInteraction.h b/new_editor/include/XCEditor/Core/UIEditorEnumFieldInteraction.h index d1789158..d2eff906 100644 --- a/new_editor/include/XCEditor/Core/UIEditorEnumFieldInteraction.h +++ b/new_editor/include/XCEditor/Core/UIEditorEnumFieldInteraction.h @@ -1,9 +1,12 @@ #pragma once +#include #include #include +#include +#include #include namespace XCEngine::UI::Editor { @@ -11,19 +14,29 @@ namespace XCEngine::UI::Editor { struct UIEditorEnumFieldInteractionState { Widgets::UIEditorEnumFieldState fieldState = {}; ::XCEngine::UI::UIPoint pointerPosition = {}; + std::size_t highlightedIndex = Widgets::UIEditorMenuPopupInvalidIndex; + std::size_t pressedPopupIndex = Widgets::UIEditorMenuPopupInvalidIndex; bool hasPointerPosition = false; + bool popupOpen = false; }; struct UIEditorEnumFieldInteractionResult { bool consumed = false; bool selectionChanged = false; bool focusedChanged = false; + bool popupOpened = false; + bool popupClosed = false; std::size_t selectedIndex = 0u; Widgets::UIEditorEnumFieldHitTarget hitTarget = {}; + std::size_t popupItemIndex = Widgets::UIEditorMenuPopupInvalidIndex; }; struct UIEditorEnumFieldInteractionFrame { Widgets::UIEditorEnumFieldLayout layout = {}; + Widgets::UIEditorMenuPopupLayout popupLayout = {}; + Widgets::UIEditorMenuPopupState popupState = {}; + std::vector popupItems = {}; + bool popupOpen = false; UIEditorEnumFieldInteractionResult result = {}; }; @@ -33,6 +46,8 @@ UIEditorEnumFieldInteractionFrame UpdateUIEditorEnumFieldInteraction( const ::XCEngine::UI::UIRect& bounds, const Widgets::UIEditorEnumFieldSpec& spec, const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, - const Widgets::UIEditorEnumFieldMetrics& metrics = {}); + const Widgets::UIEditorEnumFieldMetrics& metrics = {}, + const Widgets::UIEditorMenuPopupMetrics& popupMetrics = {}, + const ::XCEngine::UI::UIRect& viewportRect = {}); } // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Core/UIEditorPropertyGridInteraction.h b/new_editor/include/XCEditor/Core/UIEditorPropertyGridInteraction.h index 63db5876..ae0855d6 100644 --- a/new_editor/include/XCEditor/Core/UIEditorPropertyGridInteraction.h +++ b/new_editor/include/XCEditor/Core/UIEditorPropertyGridInteraction.h @@ -19,6 +19,7 @@ struct UIEditorPropertyGridInteractionState { ::XCEngine::UI::Widgets::UIKeyboardNavigationModel keyboardNavigation = {}; ::XCEngine::UI::Text::UITextInputState textInputState = {}; ::XCEngine::UI::UIPoint pointerPosition = {}; + std::size_t pressedPopupIndex = Widgets::UIEditorPropertyGridInvalidIndex; bool hasPointerPosition = false; }; @@ -30,7 +31,11 @@ struct UIEditorPropertyGridInteractionResult { bool editStarted = false; bool editValueChanged = false; bool editCommitted = false; + bool editCommitRejected = false; bool editCanceled = false; + bool popupOpened = false; + bool popupClosed = false; + bool fieldValueChanged = false; bool secondaryClicked = false; Widgets::UIEditorPropertyGridHitTarget hitTarget = {}; std::string toggledSectionId = {}; @@ -38,6 +43,8 @@ struct UIEditorPropertyGridInteractionResult { std::string activeFieldId = {}; std::string committedFieldId = {}; std::string committedValue = {}; + std::string changedFieldId = {}; + std::string changedValue = {}; }; struct UIEditorPropertyGridInteractionFrame { @@ -51,8 +58,9 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel, ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const ::XCEngine::UI::UIRect& bounds, - const std::vector& sections, + std::vector& sections, const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, - const Widgets::UIEditorPropertyGridMetrics& metrics = {}); + const Widgets::UIEditorPropertyGridMetrics& metrics = {}, + const Widgets::UIEditorMenuPopupMetrics& popupMetrics = {}); } // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Core/UIEditorTextFieldInteraction.h b/new_editor/include/XCEditor/Core/UIEditorTextFieldInteraction.h new file mode 100644 index 00000000..11fbbbc6 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorTextFieldInteraction.h @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor { + +struct UIEditorTextFieldInteractionState { + Widgets::UIEditorTextFieldState textFieldState = {}; + ::XCEngine::UI::Text::UITextInputState textInputState = {}; + ::XCEngine::UI::Widgets::UIPropertyEditModel editModel = {}; + ::XCEngine::UI::UIPoint pointerPosition = {}; + bool hasPointerPosition = false; +}; + +struct UIEditorTextFieldInteractionResult { + bool consumed = false; + bool focusChanged = false; + bool valueChanged = false; + bool editStarted = false; + bool editCommitted = false; + bool editCanceled = false; + Widgets::UIEditorTextFieldHitTarget hitTarget = {}; + std::string valueBefore = {}; + std::string valueAfter = {}; + std::string committedText = {}; +}; + +struct UIEditorTextFieldInteractionFrame { + Widgets::UIEditorTextFieldLayout layout = {}; + UIEditorTextFieldInteractionResult result = {}; +}; + +UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( + UIEditorTextFieldInteractionState& state, + Widgets::UIEditorTextFieldSpec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorTextFieldMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Core/UIEditorTheme.h b/new_editor/include/XCEditor/Core/UIEditorTheme.h new file mode 100644 index 00000000..2e92c9c5 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorTheme.h @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace XCEngine::UI::Editor { + +float ResolveUIEditorThemeFloat( + const ::XCEngine::UI::Style::UITheme& theme, + std::string_view tokenName, + float fallbackValue); + +::XCEngine::UI::UIColor ResolveUIEditorThemeColor( + const ::XCEngine::UI::Style::UITheme& theme, + std::string_view tokenName, + const ::XCEngine::UI::UIColor& fallbackValue); + +Widgets::UIEditorBoolFieldMetrics ResolveUIEditorBoolFieldMetrics( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorBoolFieldMetrics& fallback = {}); + +Widgets::UIEditorBoolFieldPalette ResolveUIEditorBoolFieldPalette( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorBoolFieldPalette& fallback = {}); + +Widgets::UIEditorNumberFieldMetrics ResolveUIEditorNumberFieldMetrics( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorNumberFieldMetrics& fallback = {}); + +Widgets::UIEditorNumberFieldPalette ResolveUIEditorNumberFieldPalette( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorNumberFieldPalette& fallback = {}); + +Widgets::UIEditorTextFieldMetrics ResolveUIEditorTextFieldMetrics( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorTextFieldMetrics& fallback = {}); + +Widgets::UIEditorTextFieldPalette ResolveUIEditorTextFieldPalette( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorTextFieldPalette& fallback = {}); + +Widgets::UIEditorVector2FieldMetrics ResolveUIEditorVector2FieldMetrics( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorVector2FieldMetrics& fallback = {}); + +Widgets::UIEditorVector2FieldPalette ResolveUIEditorVector2FieldPalette( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorVector2FieldPalette& fallback = {}); + +Widgets::UIEditorVector3FieldMetrics ResolveUIEditorVector3FieldMetrics( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorVector3FieldMetrics& fallback = {}); + +Widgets::UIEditorVector3FieldPalette ResolveUIEditorVector3FieldPalette( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorVector3FieldPalette& fallback = {}); + +Widgets::UIEditorEnumFieldMetrics ResolveUIEditorEnumFieldMetrics( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorEnumFieldMetrics& fallback = {}); + +Widgets::UIEditorEnumFieldPalette ResolveUIEditorEnumFieldPalette( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorEnumFieldPalette& fallback = {}); + +Widgets::UIEditorMenuPopupMetrics ResolveUIEditorMenuPopupMetrics( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorMenuPopupMetrics& fallback = {}); + +Widgets::UIEditorMenuPopupPalette ResolveUIEditorMenuPopupPalette( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorMenuPopupPalette& fallback = {}); + +Widgets::UIEditorPropertyGridMetrics ResolveUIEditorPropertyGridMetrics( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorPropertyGridMetrics& fallback = {}); + +Widgets::UIEditorPropertyGridPalette ResolveUIEditorPropertyGridPalette( + const ::XCEngine::UI::Style::UITheme& theme, + const Widgets::UIEditorPropertyGridPalette& fallback = {}); + +Widgets::UIEditorBoolFieldMetrics BuildUIEditorHostedBoolFieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorBoolFieldMetrics& fallback = {}); + +Widgets::UIEditorBoolFieldPalette BuildUIEditorHostedBoolFieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorBoolFieldPalette& fallback = {}); + +Widgets::UIEditorNumberFieldMetrics BuildUIEditorHostedNumberFieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorNumberFieldMetrics& fallback = {}); + +Widgets::UIEditorNumberFieldPalette BuildUIEditorHostedNumberFieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorNumberFieldPalette& fallback = {}); + +Widgets::UIEditorTextFieldMetrics BuildUIEditorHostedTextFieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorTextFieldMetrics& fallback = {}); + +Widgets::UIEditorTextFieldPalette BuildUIEditorHostedTextFieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorTextFieldPalette& fallback = {}); + +Widgets::UIEditorVector2FieldMetrics BuildUIEditorHostedVector2FieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorVector2FieldMetrics& fallback = {}); + +Widgets::UIEditorVector2FieldPalette BuildUIEditorHostedVector2FieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorVector2FieldPalette& fallback = {}); + +Widgets::UIEditorVector3FieldMetrics BuildUIEditorHostedVector3FieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorVector3FieldMetrics& fallback = {}); + +Widgets::UIEditorVector3FieldPalette BuildUIEditorHostedVector3FieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorVector3FieldPalette& fallback = {}); + +Widgets::UIEditorEnumFieldMetrics BuildUIEditorHostedEnumFieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorEnumFieldMetrics& fallback = {}); + +Widgets::UIEditorEnumFieldPalette BuildUIEditorHostedEnumFieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorEnumFieldPalette& fallback = {}); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Core/UIEditorVector2FieldInteraction.h b/new_editor/include/XCEditor/Core/UIEditorVector2FieldInteraction.h new file mode 100644 index 00000000..4bab2a12 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorVector2FieldInteraction.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor { + +struct UIEditorVector2FieldInteractionState { + Widgets::UIEditorVector2FieldState vector2FieldState = {}; + ::XCEngine::UI::Text::UITextInputState textInputState = {}; + ::XCEngine::UI::Widgets::UIPropertyEditModel editModel = {}; + ::XCEngine::UI::UIPoint pointerPosition = {}; + bool hasPointerPosition = false; +}; + +struct UIEditorVector2FieldInteractionResult { + 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::UIEditorVector2FieldHitTarget hitTarget = {}; + std::size_t selectedComponentIndex = Widgets::UIEditorVector2FieldInvalidComponentIndex; + std::size_t changedComponentIndex = Widgets::UIEditorVector2FieldInvalidComponentIndex; + std::array valuesBefore = { 0.0, 0.0 }; + std::array valuesAfter = { 0.0, 0.0 }; + double stepDelta = 0.0; + std::string committedText = {}; +}; + +struct UIEditorVector2FieldInteractionFrame { + Widgets::UIEditorVector2FieldLayout layout = {}; + UIEditorVector2FieldInteractionResult result = {}; +}; + +UIEditorVector2FieldInteractionFrame UpdateUIEditorVector2FieldInteraction( + UIEditorVector2FieldInteractionState& state, + Widgets::UIEditorVector2FieldSpec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorVector2FieldMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Core/UIEditorVector3FieldInteraction.h b/new_editor/include/XCEditor/Core/UIEditorVector3FieldInteraction.h new file mode 100644 index 00000000..2d75e295 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorVector3FieldInteraction.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include +#include + +#include +#include + +namespace XCEngine::UI::Editor { + +struct UIEditorVector3FieldInteractionState { + Widgets::UIEditorVector3FieldState vector3FieldState = {}; + ::XCEngine::UI::Text::UITextInputState textInputState = {}; + ::XCEngine::UI::Widgets::UIPropertyEditModel editModel = {}; + ::XCEngine::UI::UIPoint pointerPosition = {}; + bool hasPointerPosition = false; +}; + +struct UIEditorVector3FieldInteractionResult { + 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::UIEditorVector3FieldHitTarget hitTarget = {}; + std::size_t selectedComponentIndex = Widgets::UIEditorVector3FieldInvalidComponentIndex; + std::size_t changedComponentIndex = Widgets::UIEditorVector3FieldInvalidComponentIndex; + std::array valuesBefore = { 0.0, 0.0, 0.0 }; + std::array valuesAfter = { 0.0, 0.0, 0.0 }; + double stepDelta = 0.0; + std::string committedText = {}; +}; + +struct UIEditorVector3FieldInteractionFrame { + Widgets::UIEditorVector3FieldLayout layout = {}; + UIEditorVector3FieldInteractionResult result = {}; +}; + +UIEditorVector3FieldInteractionFrame UpdateUIEditorVector3FieldInteraction( + UIEditorVector3FieldInteractionState& state, + Widgets::UIEditorVector3FieldSpec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents, + const Widgets::UIEditorVector3FieldMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/include/XCEditor/Widgets/UIEditorBoolField.h b/new_editor/include/XCEditor/Widgets/UIEditorBoolField.h index b26b8a07..7643832a 100644 --- a/new_editor/include/XCEditor/Widgets/UIEditorBoolField.h +++ b/new_editor/include/XCEditor/Widgets/UIEditorBoolField.h @@ -10,7 +10,7 @@ namespace XCEngine::UI::Editor::Widgets { enum class UIEditorBoolFieldHitTargetKind : std::uint8_t { None = 0, Row, - Toggle + Checkbox }; struct UIEditorBoolFieldSpec { @@ -27,51 +27,54 @@ struct UIEditorBoolFieldState { }; struct UIEditorBoolFieldMetrics { - float rowHeight = 32.0f; + float rowHeight = 22.0f; float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; - float toggleWidth = 42.0f; - float toggleHeight = 20.0f; - float toggleKnobInset = 3.0f; - float textInsetY = 8.0f; - float cornerRounding = 6.0f; + float controlTrailingInset = 8.0f; + float checkboxSize = 18.0f; + float labelTextInsetY = 0.0f; + float labelFontSize = 11.0f; + float checkboxGlyphInsetX = 2.0f; + float checkboxGlyphInsetY = -1.0f; + float checkboxGlyphFontSize = 10.0f; + float cornerRounding = 0.0f; + float checkboxRounding = 2.0f; float borderThickness = 1.0f; - float focusedBorderThickness = 2.0f; + float focusedBorderThickness = 1.0f; }; struct UIEditorBoolFieldPalette { ::XCEngine::UI::UIColor surfaceColor = - ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor borderColor = - ::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor focusedBorderColor = - ::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f); + ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); ::XCEngine::UI::UIColor rowHoverColor = - ::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor rowActiveColor = - ::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f); - ::XCEngine::UI::UIColor toggleOffColor = - ::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f); - ::XCEngine::UI::UIColor toggleOnColor = - ::XCEngine::UI::UIColor(0.78f, 0.78f, 0.78f, 1.0f); - ::XCEngine::UI::UIColor toggleReadOnlyColor = + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); + ::XCEngine::UI::UIColor checkboxColor = ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f); - ::XCEngine::UI::UIColor toggleBorderColor = - ::XCEngine::UI::UIColor(0.36f, 0.36f, 0.36f, 1.0f); - ::XCEngine::UI::UIColor knobColor = - ::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f); + ::XCEngine::UI::UIColor checkboxHoverColor = + ::XCEngine::UI::UIColor(0.21f, 0.21f, 0.21f, 1.0f); + ::XCEngine::UI::UIColor checkboxReadOnlyColor = + ::XCEngine::UI::UIColor(0.17f, 0.17f, 0.17f, 1.0f); + ::XCEngine::UI::UIColor checkboxBorderColor = + ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); + ::XCEngine::UI::UIColor checkboxMarkColor = + ::XCEngine::UI::UIColor(0.72f, 0.72f, 0.72f, 1.0f); ::XCEngine::UI::UIColor labelColor = - ::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f); - ::XCEngine::UI::UIColor valueColor = - ::XCEngine::UI::UIColor(0.70f, 0.70f, 0.70f, 1.0f); + ::XCEngine::UI::UIColor(0.88f, 0.88f, 0.88f, 1.0f); }; struct UIEditorBoolFieldLayout { ::XCEngine::UI::UIRect bounds = {}; ::XCEngine::UI::UIRect labelRect = {}; - ::XCEngine::UI::UIRect toggleRect = {}; - ::XCEngine::UI::UIRect knobRect = {}; + ::XCEngine::UI::UIRect controlRect = {}; + ::XCEngine::UI::UIRect checkboxRect = {}; + ::XCEngine::UI::UIRect checkmarkRect = {}; }; struct UIEditorBoolFieldHitTarget { diff --git a/new_editor/include/XCEditor/Widgets/UIEditorEnumField.h b/new_editor/include/XCEditor/Widgets/UIEditorEnumField.h index 2dd15b17..ae79a2a6 100644 --- a/new_editor/include/XCEditor/Widgets/UIEditorEnumField.h +++ b/new_editor/include/XCEditor/Widgets/UIEditorEnumField.h @@ -12,9 +12,8 @@ namespace XCEngine::UI::Editor::Widgets { enum class UIEditorEnumFieldHitTargetKind : std::uint8_t { None = 0, Row, - PreviousButton, - NextButton, - ValueBox + ValueBox, + DropdownArrow }; struct UIEditorEnumFieldSpec { @@ -29,52 +28,57 @@ struct UIEditorEnumFieldState { UIEditorEnumFieldHitTargetKind hoveredTarget = UIEditorEnumFieldHitTargetKind::None; bool focused = false; bool active = false; + bool popupOpen = false; }; struct UIEditorEnumFieldMetrics { - float rowHeight = 32.0f; + float rowHeight = 22.0f; float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; - float buttonWidth = 24.0f; - float controlGap = 4.0f; + float controlTrailingInset = 8.0f; float valueBoxMinWidth = 96.0f; - float valueBoxHeight = 24.0f; - float labelTextInsetY = 8.0f; - float valueTextInsetX = 8.0f; - float valueTextInsetY = 6.0f; - float buttonTextInsetX = 8.0f; - float buttonTextInsetY = 6.0f; - float cornerRounding = 6.0f; + float controlInsetY = 1.0f; + float labelTextInsetY = 0.0f; + float labelFontSize = 11.0f; + float valueTextInsetX = 3.0f; + float valueTextInsetY = 0.0f; + float valueFontSize = 12.0f; + float dropdownArrowWidth = 20.0f; + float dropdownArrowInsetX = 0.0f; + float dropdownArrowInsetY = -1.0f; + float dropdownArrowFontSize = 10.0f; + float cornerRounding = 0.0f; + float valueBoxRounding = 2.0f; float borderThickness = 1.0f; - float focusedBorderThickness = 2.0f; + float focusedBorderThickness = 1.0f; }; struct UIEditorEnumFieldPalette { ::XCEngine::UI::UIColor surfaceColor = - ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor borderColor = - ::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor focusedBorderColor = - ::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f); + ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); ::XCEngine::UI::UIColor rowHoverColor = - ::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor rowActiveColor = - ::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor valueBoxColor = ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f); - ::XCEngine::UI::UIColor buttonColor = - ::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f); - ::XCEngine::UI::UIColor buttonHoverColor = - ::XCEngine::UI::UIColor(0.30f, 0.30f, 0.30f, 1.0f); + ::XCEngine::UI::UIColor valueBoxHoverColor = + ::XCEngine::UI::UIColor(0.21f, 0.21f, 0.21f, 1.0f); ::XCEngine::UI::UIColor readOnlyColor = ::XCEngine::UI::UIColor(0.17f, 0.17f, 0.17f, 1.0f); ::XCEngine::UI::UIColor controlBorderColor = - ::XCEngine::UI::UIColor(0.36f, 0.36f, 0.36f, 1.0f); + ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); ::XCEngine::UI::UIColor labelColor = - ::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f); + ::XCEngine::UI::UIColor(0.88f, 0.88f, 0.88f, 1.0f); ::XCEngine::UI::UIColor valueColor = - ::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f); + ::XCEngine::UI::UIColor(0.88f, 0.88f, 0.88f, 1.0f); + ::XCEngine::UI::UIColor arrowColor = + ::XCEngine::UI::UIColor(0.88f, 0.88f, 0.88f, 1.0f); }; struct UIEditorEnumFieldLayout { @@ -82,8 +86,7 @@ struct UIEditorEnumFieldLayout { ::XCEngine::UI::UIRect labelRect = {}; ::XCEngine::UI::UIRect controlRect = {}; ::XCEngine::UI::UIRect valueRect = {}; - ::XCEngine::UI::UIRect previousRect = {}; - ::XCEngine::UI::UIRect nextRect = {}; + ::XCEngine::UI::UIRect arrowRect = {}; }; struct UIEditorEnumFieldHitTarget { diff --git a/new_editor/include/XCEditor/Widgets/UIEditorFieldRowLayout.h b/new_editor/include/XCEditor/Widgets/UIEditorFieldRowLayout.h new file mode 100644 index 00000000..877beccf --- /dev/null +++ b/new_editor/include/XCEditor/Widgets/UIEditorFieldRowLayout.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace XCEngine::UI::Editor::Widgets { + +struct UIEditorFieldRowLayoutMetrics { + float rowHeight = 22.0f; + float horizontalPadding = 12.0f; + float labelControlGap = 20.0f; + float controlColumnStart = 236.0f; + float controlTrailingInset = 8.0f; + float controlInsetY = 1.0f; +}; + +struct UIEditorFieldRowLayout { + ::XCEngine::UI::UIRect bounds = {}; + ::XCEngine::UI::UIRect labelRect = {}; + ::XCEngine::UI::UIRect controlRect = {}; +}; + +UIEditorFieldRowLayout BuildUIEditorFieldRowLayout( + const ::XCEngine::UI::UIRect& bounds, + float minimumControlWidth, + const UIEditorFieldRowLayoutMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/include/XCEditor/Widgets/UIEditorMenuPopup.h b/new_editor/include/XCEditor/Widgets/UIEditorMenuPopup.h index 2e4fa96b..a931c585 100644 --- a/new_editor/include/XCEditor/Widgets/UIEditorMenuPopup.h +++ b/new_editor/include/XCEditor/Widgets/UIEditorMenuPopup.h @@ -45,8 +45,10 @@ struct UIEditorMenuPopupMetrics { float popupCornerRounding = 8.0f; float labelInsetX = 14.0f; float labelInsetY = -1.0f; + float labelFontSize = 13.0f; float shortcutInsetRight = 24.0f; float estimatedGlyphWidth = 7.0f; + float glyphFontSize = 12.0f; float separatorThickness = 1.0f; float borderThickness = 1.0f; }; diff --git a/new_editor/include/XCEditor/Widgets/UIEditorNumberField.h b/new_editor/include/XCEditor/Widgets/UIEditorNumberField.h index 4d237c55..129acddf 100644 --- a/new_editor/include/XCEditor/Widgets/UIEditorNumberField.h +++ b/new_editor/include/XCEditor/Widgets/UIEditorNumberField.h @@ -5,14 +5,13 @@ #include #include #include +#include namespace XCEngine::UI::Editor::Widgets { enum class UIEditorNumberFieldHitTargetKind : std::uint8_t { None = 0, Row, - DecrementButton, - IncrementButton, ValueBox }; @@ -36,60 +35,53 @@ struct UIEditorNumberFieldState { }; struct UIEditorNumberFieldMetrics { - float rowHeight = 32.0f; + float rowHeight = 22.0f; float horizontalPadding = 12.0f; float labelControlGap = 20.0f; float controlColumnStart = 236.0f; - float buttonWidth = 22.0f; - float buttonGap = 4.0f; - float valueBoxMinWidth = 72.0f; - float controlInsetY = 4.0f; - float labelTextInsetY = 8.0f; - float valueTextInsetX = 8.0f; - float valueTextInsetY = 8.0f; - float buttonTextInsetY = 8.0f; - float cornerRounding = 6.0f; - float valueBoxRounding = 5.0f; - float buttonRounding = 4.0f; + float controlTrailingInset = 8.0f; + float valueBoxMinWidth = 96.0f; + float controlInsetY = 1.0f; + float labelTextInsetY = 0.0f; + float labelFontSize = 11.0f; + float valueTextInsetX = 5.0f; + float valueTextInsetY = 0.0f; + float valueFontSize = 12.0f; + float cornerRounding = 0.0f; + float valueBoxRounding = 2.0f; float borderThickness = 1.0f; - float focusedBorderThickness = 2.0f; + float focusedBorderThickness = 1.0f; }; struct UIEditorNumberFieldPalette { ::XCEngine::UI::UIColor surfaceColor = - ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor borderColor = - ::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor focusedBorderColor = - ::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor rowHoverColor = - ::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor rowActiveColor = - ::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.0f); + ::XCEngine::UI::UIColor(0.0f, 0.0f, 0.0f, 0.0f); ::XCEngine::UI::UIColor valueBoxColor = ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f); ::XCEngine::UI::UIColor valueBoxHoverColor = - ::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 1.0f); + ::XCEngine::UI::UIColor(0.21f, 0.21f, 0.21f, 1.0f); ::XCEngine::UI::UIColor valueBoxEditingColor = ::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f); - ::XCEngine::UI::UIColor buttonColor = - ::XCEngine::UI::UIColor(0.24f, 0.24f, 0.24f, 1.0f); - ::XCEngine::UI::UIColor buttonHoverColor = - ::XCEngine::UI::UIColor(0.30f, 0.30f, 0.30f, 1.0f); - ::XCEngine::UI::UIColor buttonActiveColor = - ::XCEngine::UI::UIColor(0.36f, 0.36f, 0.36f, 1.0f); ::XCEngine::UI::UIColor readOnlyColor = ::XCEngine::UI::UIColor(0.17f, 0.17f, 0.17f, 1.0f); ::XCEngine::UI::UIColor controlBorderColor = - ::XCEngine::UI::UIColor(0.36f, 0.36f, 0.36f, 1.0f); + ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); + ::XCEngine::UI::UIColor controlFocusedBorderColor = + ::XCEngine::UI::UIColor(0.46f, 0.46f, 0.46f, 1.0f); ::XCEngine::UI::UIColor labelColor = - ::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f); + ::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 stepTextColor = - ::XCEngine::UI::UIColor(0.90f, 0.90f, 0.90f, 1.0f); }; struct UIEditorNumberFieldLayout { @@ -97,8 +89,6 @@ struct UIEditorNumberFieldLayout { ::XCEngine::UI::UIRect labelRect = {}; ::XCEngine::UI::UIRect controlRect = {}; ::XCEngine::UI::UIRect valueRect = {}; - ::XCEngine::UI::UIRect decrementRect = {}; - ::XCEngine::UI::UIRect incrementRect = {}; }; struct UIEditorNumberFieldHitTarget { @@ -109,6 +99,15 @@ bool IsUIEditorNumberFieldPointInside( const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIPoint& point); +double NormalizeUIEditorNumberFieldValue( + const UIEditorNumberFieldSpec& spec, + double value); + +bool TryParseUIEditorNumberFieldValue( + const UIEditorNumberFieldSpec& spec, + std::string_view text, + double& outValue); + std::string FormatUIEditorNumberFieldValue(const UIEditorNumberFieldSpec& spec); UIEditorNumberFieldLayout BuildUIEditorNumberFieldLayout( @@ -132,6 +131,7 @@ void AppendUIEditorNumberFieldForeground( ::XCEngine::UI::UIDrawList& drawList, const UIEditorNumberFieldLayout& layout, const UIEditorNumberFieldSpec& spec, + const UIEditorNumberFieldState& state, const UIEditorNumberFieldPalette& palette = {}, const UIEditorNumberFieldMetrics& metrics = {}); diff --git a/new_editor/include/XCEditor/Widgets/UIEditorPropertyGrid.h b/new_editor/include/XCEditor/Widgets/UIEditorPropertyGrid.h index 65a414ab..b69fcd03 100644 --- a/new_editor/include/XCEditor/Widgets/UIEditorPropertyGrid.h +++ b/new_editor/include/XCEditor/Widgets/UIEditorPropertyGrid.h @@ -5,6 +5,8 @@ #include #include +#include + #include #include #include @@ -15,6 +17,13 @@ namespace XCEngine::UI::Editor::Widgets { inline constexpr std::size_t UIEditorPropertyGridInvalidIndex = static_cast(-1); +enum class UIEditorPropertyGridFieldKind : std::uint8_t { + Text = 0, + Bool, + Number, + Enum +}; + enum class UIEditorPropertyGridHitTargetKind : std::uint8_t { None = 0, SectionHeader, @@ -32,12 +41,29 @@ struct UIEditorPropertyGridFieldLocation { } }; +struct UIEditorPropertyGridNumberFieldValue { + double value = 0.0; + double step = 1.0; + double minValue = 0.0; + double maxValue = 100.0; + bool integerMode = true; +}; + +struct UIEditorPropertyGridEnumFieldValue { + std::vector options = {}; + std::size_t selectedIndex = 0u; +}; + struct UIEditorPropertyGridField { std::string fieldId = {}; std::string label = {}; std::string valueText = {}; bool readOnly = false; float desiredHeight = 0.0f; + UIEditorPropertyGridFieldKind kind = UIEditorPropertyGridFieldKind::Text; + bool boolValue = false; + UIEditorPropertyGridNumberFieldValue numberValue = {}; + UIEditorPropertyGridEnumFieldValue enumValue = {}; }; struct UIEditorPropertyGridSection { @@ -50,7 +76,11 @@ struct UIEditorPropertyGridSection { struct UIEditorPropertyGridState { std::string hoveredSectionId = {}; std::string hoveredFieldId = {}; + UIEditorPropertyGridHitTargetKind hoveredHitTarget = UIEditorPropertyGridHitTargetKind::None; bool focused = false; + std::string pressedFieldId = {}; + std::string popupFieldId = {}; + std::size_t popupHighlightedIndex = UIEditorPropertyGridInvalidIndex; }; struct UIEditorPropertyGridMetrics { @@ -65,10 +95,17 @@ struct UIEditorPropertyGridMetrics { float disclosureExtent = 12.0f; float disclosureLabelGap = 8.0f; float sectionTextInsetY = 8.0f; + float sectionFontSize = 12.0f; + float disclosureGlyphInsetX = 2.0f; + float disclosureGlyphInsetY = -1.0f; + float disclosureGlyphFontSize = 12.0f; float labelTextInsetY = 8.0f; + float labelFontSize = 12.0f; float valueTextInsetY = 8.0f; + float valueFontSize = 12.0f; float valueBoxInsetY = 4.0f; float valueBoxInsetX = 8.0f; + float tagFontSize = 11.0f; float cornerRounding = 6.0f; float valueBoxRounding = 5.0f; float borderThickness = 1.0f; @@ -153,6 +190,9 @@ UIEditorPropertyGridFieldLocation FindUIEditorPropertyGridFieldLocation( const std::vector& sections, std::string_view fieldId); +std::string ResolveUIEditorPropertyGridFieldValueText( + const UIEditorPropertyGridField& field); + std::size_t FindUIEditorPropertyGridVisibleFieldIndex( const UIEditorPropertyGridLayout& layout, std::string_view fieldId, @@ -182,9 +222,12 @@ void AppendUIEditorPropertyGridForeground( ::XCEngine::UI::UIDrawList& drawList, const UIEditorPropertyGridLayout& layout, const std::vector& sections, + const UIEditorPropertyGridState& state, const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const UIEditorPropertyGridPalette& palette = {}, - const UIEditorPropertyGridMetrics& metrics = {}); + const UIEditorPropertyGridMetrics& metrics = {}, + const UIEditorMenuPopupPalette& popupPalette = {}, + const UIEditorMenuPopupMetrics& popupMetrics = {}); void AppendUIEditorPropertyGrid( ::XCEngine::UI::UIDrawList& drawList, @@ -195,6 +238,8 @@ void AppendUIEditorPropertyGrid( const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const UIEditorPropertyGridState& state, const UIEditorPropertyGridPalette& palette = {}, - const UIEditorPropertyGridMetrics& metrics = {}); + const UIEditorPropertyGridMetrics& metrics = {}, + const UIEditorMenuPopupPalette& popupPalette = {}, + const UIEditorMenuPopupMetrics& popupMetrics = {}); } // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/include/XCEditor/Widgets/UIEditorTextField.h b/new_editor/include/XCEditor/Widgets/UIEditorTextField.h new file mode 100644 index 00000000..345b960a --- /dev/null +++ b/new_editor/include/XCEditor/Widgets/UIEditorTextField.h @@ -0,0 +1,130 @@ +#pragma once + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor::Widgets { + +enum class UIEditorTextFieldHitTargetKind : std::uint8_t { + None = 0, + Row, + ValueBox +}; + +struct UIEditorTextFieldSpec { + std::string fieldId = {}; + std::string label = {}; + std::string value = {}; + bool readOnly = false; +}; + +struct UIEditorTextFieldState { + UIEditorTextFieldHitTargetKind hoveredTarget = UIEditorTextFieldHitTargetKind::None; + UIEditorTextFieldHitTargetKind activeTarget = UIEditorTextFieldHitTargetKind::None; + bool focused = false; + bool editing = false; + std::string displayText = {}; +}; + +struct UIEditorTextFieldMetrics { + float rowHeight = 22.0f; + float horizontalPadding = 12.0f; + float labelControlGap = 20.0f; + float controlColumnStart = 236.0f; + float controlTrailingInset = 8.0f; + float valueBoxMinWidth = 96.0f; + float controlInsetY = 1.0f; + float labelTextInsetY = 0.0f; + float labelFontSize = 11.0f; + float valueTextInsetX = 5.0f; + float valueTextInsetY = 0.0f; + float valueFontSize = 12.0f; + float cornerRounding = 0.0f; + float valueBoxRounding = 2.0f; + float borderThickness = 1.0f; + float focusedBorderThickness = 1.0f; +}; + +struct UIEditorTextFieldPalette { + ::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 valueBoxColor = + ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f); + ::XCEngine::UI::UIColor valueBoxHoverColor = + ::XCEngine::UI::UIColor(0.21f, 0.21f, 0.21f, 1.0f); + ::XCEngine::UI::UIColor valueBoxEditingColor = + ::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 controlBorderColor = + ::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f); + ::XCEngine::UI::UIColor controlFocusedBorderColor = + ::XCEngine::UI::UIColor(0.46f, 0.46f, 0.46f, 1.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); +}; + +struct UIEditorTextFieldLayout { + ::XCEngine::UI::UIRect bounds = {}; + ::XCEngine::UI::UIRect labelRect = {}; + ::XCEngine::UI::UIRect controlRect = {}; + ::XCEngine::UI::UIRect valueRect = {}; +}; + +struct UIEditorTextFieldHitTarget { + UIEditorTextFieldHitTargetKind kind = UIEditorTextFieldHitTargetKind::None; +}; + +bool IsUIEditorTextFieldPointInside( + const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIPoint& point); + +UIEditorTextFieldLayout BuildUIEditorTextFieldLayout( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorTextFieldSpec& spec, + const UIEditorTextFieldMetrics& metrics = {}); + +UIEditorTextFieldHitTarget HitTestUIEditorTextField( + const UIEditorTextFieldLayout& layout, + const ::XCEngine::UI::UIPoint& point); + +void AppendUIEditorTextFieldBackground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorTextFieldLayout& layout, + const UIEditorTextFieldSpec& spec, + const UIEditorTextFieldState& state, + const UIEditorTextFieldPalette& palette = {}, + const UIEditorTextFieldMetrics& metrics = {}); + +void AppendUIEditorTextFieldForeground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorTextFieldLayout& layout, + const UIEditorTextFieldSpec& spec, + const UIEditorTextFieldState& state, + const UIEditorTextFieldPalette& palette = {}, + const UIEditorTextFieldMetrics& metrics = {}); + +void AppendUIEditorTextField( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorTextFieldSpec& spec, + const UIEditorTextFieldState& state, + const UIEditorTextFieldPalette& palette = {}, + const UIEditorTextFieldMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/include/XCEditor/Widgets/UIEditorTextLayout.h b/new_editor/include/XCEditor/Widgets/UIEditorTextLayout.h new file mode 100644 index 00000000..b074d141 --- /dev/null +++ b/new_editor/include/XCEditor/Widgets/UIEditorTextLayout.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include +#include + +namespace XCEngine::UI::Editor::Widgets { + +inline float MeasureUIEditorTextLayoutHeight(float fontSize) { + return (std::max)(0.0f, fontSize * 1.35f); +} + +inline float ResolveUIEditorTextTop( + const ::XCEngine::UI::UIRect& rect, + float fontSize, + float offsetY = 0.0f) { + const float textHeight = MeasureUIEditorTextLayoutHeight(fontSize); + const float centeredTop = + rect.y + (std::max)(0.0f, std::floor((rect.height - textHeight) * 0.5f)); + return centeredTop - 1.0f + offsetY; +} + +inline ::XCEngine::UI::UIRect ResolveUIEditorTextClipRect( + const ::XCEngine::UI::UIRect& rect, + float fontSize) { + const float extraPadding = (std::max)(2.0f, std::ceil(fontSize * 0.35f)); + return ::XCEngine::UI::UIRect( + rect.x, + rect.y - 1.0f, + rect.width, + rect.height + extraPadding + 1.0f); +} + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/include/XCEditor/Widgets/UIEditorVector2Field.h b/new_editor/include/XCEditor/Widgets/UIEditorVector2Field.h new file mode 100644 index 00000000..dfdd4e83 --- /dev/null +++ b/new_editor/include/XCEditor/Widgets/UIEditorVector2Field.h @@ -0,0 +1,171 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Widgets { + +inline constexpr std::size_t UIEditorVector2FieldInvalidComponentIndex = static_cast(-1); + +enum class UIEditorVector2FieldHitTargetKind : std::uint8_t { + None = 0, + Row, + Component +}; + +struct UIEditorVector2FieldSpec { + std::string fieldId = {}; + std::string label = {}; + std::array values = { 0.0, 0.0 }; + std::array componentLabels = { std::string("X"), std::string("Y") }; + double step = 0.1; + double minValue = -1000000.0; + double maxValue = 1000000.0; + bool integerMode = false; + bool readOnly = false; +}; + +struct UIEditorVector2FieldState { + UIEditorVector2FieldHitTargetKind hoveredTarget = UIEditorVector2FieldHitTargetKind::None; + UIEditorVector2FieldHitTargetKind activeTarget = UIEditorVector2FieldHitTargetKind::None; + std::size_t hoveredComponentIndex = UIEditorVector2FieldInvalidComponentIndex; + std::size_t activeComponentIndex = UIEditorVector2FieldInvalidComponentIndex; + std::size_t selectedComponentIndex = UIEditorVector2FieldInvalidComponentIndex; + bool focused = false; + bool editing = false; + std::array displayTexts = { std::string(), std::string() }; +}; + +struct UIEditorVector2FieldMetrics { + 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 UIEditorVector2FieldPalette { + ::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); +}; + +struct UIEditorVector2FieldLayout { + ::XCEngine::UI::UIRect bounds = {}; + ::XCEngine::UI::UIRect labelRect = {}; + ::XCEngine::UI::UIRect controlRect = {}; + std::array<::XCEngine::UI::UIRect, 2u> componentRects = {}; + std::array<::XCEngine::UI::UIRect, 2u> componentPrefixRects = {}; + std::array<::XCEngine::UI::UIRect, 2u> componentValueRects = {}; +}; + +struct UIEditorVector2FieldHitTarget { + UIEditorVector2FieldHitTargetKind kind = UIEditorVector2FieldHitTargetKind::None; + std::size_t componentIndex = UIEditorVector2FieldInvalidComponentIndex; +}; + +bool IsUIEditorVector2FieldPointInside( + const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIPoint& point); + +double NormalizeUIEditorVector2FieldComponentValue( + const UIEditorVector2FieldSpec& spec, + double value); + +bool TryParseUIEditorVector2FieldComponentValue( + const UIEditorVector2FieldSpec& spec, + std::string_view text, + double& outValue); + +std::string FormatUIEditorVector2FieldComponentValue( + const UIEditorVector2FieldSpec& spec, + std::size_t componentIndex); + +UIEditorVector2FieldLayout BuildUIEditorVector2FieldLayout( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorVector2FieldSpec& spec, + const UIEditorVector2FieldMetrics& metrics = {}); + +UIEditorVector2FieldHitTarget HitTestUIEditorVector2Field( + const UIEditorVector2FieldLayout& layout, + const ::XCEngine::UI::UIPoint& point); + +void AppendUIEditorVector2FieldBackground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorVector2FieldLayout& layout, + const UIEditorVector2FieldSpec& spec, + const UIEditorVector2FieldState& state, + const UIEditorVector2FieldPalette& palette = {}, + const UIEditorVector2FieldMetrics& metrics = {}); + +void AppendUIEditorVector2FieldForeground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorVector2FieldLayout& layout, + const UIEditorVector2FieldSpec& spec, + const UIEditorVector2FieldState& state, + const UIEditorVector2FieldPalette& palette = {}, + const UIEditorVector2FieldMetrics& metrics = {}); + +void AppendUIEditorVector2Field( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorVector2FieldSpec& spec, + const UIEditorVector2FieldState& state, + const UIEditorVector2FieldPalette& palette = {}, + const UIEditorVector2FieldMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/include/XCEditor/Widgets/UIEditorVector3Field.h b/new_editor/include/XCEditor/Widgets/UIEditorVector3Field.h new file mode 100644 index 00000000..a28eea3d --- /dev/null +++ b/new_editor/include/XCEditor/Widgets/UIEditorVector3Field.h @@ -0,0 +1,173 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Widgets { + +inline constexpr std::size_t UIEditorVector3FieldInvalidComponentIndex = static_cast(-1); + +enum class UIEditorVector3FieldHitTargetKind : std::uint8_t { + None = 0, + Row, + Component +}; + +struct UIEditorVector3FieldSpec { + std::string fieldId = {}; + std::string label = {}; + std::array values = { 0.0, 0.0, 0.0 }; + std::array componentLabels = { std::string("X"), std::string("Y"), std::string("Z") }; + double step = 0.1; + double minValue = -1000000.0; + double maxValue = 1000000.0; + bool integerMode = false; + bool readOnly = false; +}; + +struct UIEditorVector3FieldState { + UIEditorVector3FieldHitTargetKind hoveredTarget = UIEditorVector3FieldHitTargetKind::None; + UIEditorVector3FieldHitTargetKind activeTarget = UIEditorVector3FieldHitTargetKind::None; + std::size_t hoveredComponentIndex = UIEditorVector3FieldInvalidComponentIndex; + std::size_t activeComponentIndex = UIEditorVector3FieldInvalidComponentIndex; + std::size_t selectedComponentIndex = UIEditorVector3FieldInvalidComponentIndex; + bool focused = false; + bool editing = false; + std::array displayTexts = { std::string(), std::string(), std::string() }; +}; + +struct UIEditorVector3FieldMetrics { + 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 UIEditorVector3FieldPalette { + ::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); +}; + +struct UIEditorVector3FieldLayout { + ::XCEngine::UI::UIRect bounds = {}; + ::XCEngine::UI::UIRect labelRect = {}; + ::XCEngine::UI::UIRect controlRect = {}; + std::array<::XCEngine::UI::UIRect, 3u> componentRects = {}; + std::array<::XCEngine::UI::UIRect, 3u> componentPrefixRects = {}; + std::array<::XCEngine::UI::UIRect, 3u> componentValueRects = {}; +}; + +struct UIEditorVector3FieldHitTarget { + UIEditorVector3FieldHitTargetKind kind = UIEditorVector3FieldHitTargetKind::None; + std::size_t componentIndex = UIEditorVector3FieldInvalidComponentIndex; +}; + +bool IsUIEditorVector3FieldPointInside( + const ::XCEngine::UI::UIRect& rect, + const ::XCEngine::UI::UIPoint& point); + +double NormalizeUIEditorVector3FieldComponentValue( + const UIEditorVector3FieldSpec& spec, + double value); + +bool TryParseUIEditorVector3FieldComponentValue( + const UIEditorVector3FieldSpec& spec, + std::string_view text, + double& outValue); + +std::string FormatUIEditorVector3FieldComponentValue( + const UIEditorVector3FieldSpec& spec, + std::size_t componentIndex); + +UIEditorVector3FieldLayout BuildUIEditorVector3FieldLayout( + const ::XCEngine::UI::UIRect& bounds, + const UIEditorVector3FieldSpec& spec, + const UIEditorVector3FieldMetrics& metrics = {}); + +UIEditorVector3FieldHitTarget HitTestUIEditorVector3Field( + const UIEditorVector3FieldLayout& layout, + const ::XCEngine::UI::UIPoint& point); + +void AppendUIEditorVector3FieldBackground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorVector3FieldLayout& layout, + const UIEditorVector3FieldSpec& spec, + const UIEditorVector3FieldState& state, + const UIEditorVector3FieldPalette& palette = {}, + const UIEditorVector3FieldMetrics& metrics = {}); + +void AppendUIEditorVector3FieldForeground( + ::XCEngine::UI::UIDrawList& drawList, + const UIEditorVector3FieldLayout& layout, + const UIEditorVector3FieldSpec& spec, + const UIEditorVector3FieldState& state, + const UIEditorVector3FieldPalette& palette = {}, + const UIEditorVector3FieldMetrics& metrics = {}); + +void AppendUIEditorVector3Field( + ::XCEngine::UI::UIDrawList& drawList, + const ::XCEngine::UI::UIRect& bounds, + const UIEditorVector3FieldSpec& spec, + const UIEditorVector3FieldState& state, + const UIEditorVector3FieldPalette& palette = {}, + const UIEditorVector3FieldMetrics& metrics = {}); + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Core/UIEditorBoolFieldInteraction.cpp b/new_editor/src/Core/UIEditorBoolFieldInteraction.cpp index 3988fc1a..2f288704 100644 --- a/new_editor/src/Core/UIEditorBoolFieldInteraction.cpp +++ b/new_editor/src/Core/UIEditorBoolFieldInteraction.cpp @@ -137,6 +137,16 @@ UIEditorBoolFieldInteractionFrame UpdateUIEditorBoolFieldInteraction( } } + layout = BuildUIEditorBoolFieldLayout(bounds, resolvedSpec, metrics); + if (state.hasPointerPosition) { + state.fieldState.hoveredTarget = HitTestUIEditorBoolField(layout, state.pointerPosition).kind; + if (interactionResult.hitTarget.kind == UIEditorBoolFieldHitTargetKind::None) { + interactionResult.hitTarget = HitTestUIEditorBoolField(layout, state.pointerPosition); + } + } else { + state.fieldState.hoveredTarget = UIEditorBoolFieldHitTargetKind::None; + } + return { layout, interactionResult }; } diff --git a/new_editor/src/Core/UIEditorEnumFieldInteraction.cpp b/new_editor/src/Core/UIEditorEnumFieldInteraction.cpp index d8773a54..9c87b695 100644 --- a/new_editor/src/Core/UIEditorEnumFieldInteraction.cpp +++ b/new_editor/src/Core/UIEditorEnumFieldInteraction.cpp @@ -1,6 +1,10 @@ #include #include +#include + +#include +#include namespace XCEngine::UI::Editor { @@ -10,9 +14,28 @@ using ::XCEngine::Input::KeyCode; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPointerButton; +using ::XCEngine::UI::UISize; +using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect; +using ::XCEngine::UI::Widgets::UIPopupPlacement; using Widgets::BuildUIEditorEnumFieldLayout; +using Widgets::BuildUIEditorMenuPopupLayout; using Widgets::HitTestUIEditorEnumField; +using Widgets::HitTestUIEditorMenuPopup; +using Widgets::MeasureUIEditorMenuPopupHeight; +using Widgets::ResolveUIEditorMenuPopupDesiredWidth; +using Widgets::UIEditorEnumFieldHitTarget; using Widgets::UIEditorEnumFieldHitTargetKind; +using Widgets::UIEditorEnumFieldLayout; +using Widgets::UIEditorEnumFieldMetrics; +using Widgets::UIEditorEnumFieldSpec; +using Widgets::UIEditorMenuPopupHitTarget; +using Widgets::UIEditorMenuPopupHitTargetKind; +using Widgets::UIEditorMenuPopupInvalidIndex; +using Widgets::UIEditorMenuPopupItem; +using Widgets::UIEditorMenuPopupLayout; +using Widgets::UIEditorMenuPopupMetrics; +using Widgets::UIEditorMenuPopupState; +using ::XCEngine::UI::Editor::UIEditorMenuItemKind; bool ShouldUsePointerPosition(const UIInputEvent& event) { switch (event.type) { @@ -26,23 +49,199 @@ bool ShouldUsePointerPosition(const UIInputEvent& event) { } } -bool MoveSelection(std::size_t& selectedIndex, const Widgets::UIEditorEnumFieldSpec& spec, int direction) { +std::size_t ClampSelectedIndex(std::size_t selectedIndex, const UIEditorEnumFieldSpec& spec) { if (spec.options.empty()) { - return false; + return 0u; } - std::size_t nextIndex = selectedIndex; - if (direction < 0) { - nextIndex = selectedIndex == 0u ? 0u : selectedIndex - 1u; + return (std::min)(selectedIndex, spec.options.size() - 1u); +} + +::XCEngine::UI::UIRect ResolveViewportRect( + const ::XCEngine::UI::UIRect& bounds, + const ::XCEngine::UI::UIRect& viewportRect) { + if (viewportRect.width > 0.0f && viewportRect.height > 0.0f) { + return viewportRect; + } + + return ::XCEngine::UI::UIRect(bounds.x - 4096.0f, bounds.y - 4096.0f, 8192.0f, 8192.0f); +} + +std::vector BuildPopupItems( + const UIEditorEnumFieldSpec& spec, + std::size_t selectedIndex) { + std::vector items = {}; + items.reserve(spec.options.size()); + + const std::size_t clampedIndex = ClampSelectedIndex(selectedIndex, spec); + for (std::size_t index = 0u; index < spec.options.size(); ++index) { + UIEditorMenuPopupItem item = {}; + item.itemId = spec.fieldId + "." + std::to_string(index); + item.kind = UIEditorMenuItemKind::Command; + item.label = spec.options[index]; + item.enabled = !spec.readOnly; + item.checked = index == clampedIndex; + items.push_back(std::move(item)); + } + + return items; +} + +UIEditorMenuPopupLayout BuildPopupLayout( + const UIEditorEnumFieldLayout& fieldLayout, + const std::vector& popupItems, + const UIEditorMenuPopupMetrics& popupMetrics, + const ::XCEngine::UI::UIRect& viewportRect) { + if (popupItems.empty()) { + return {}; + } + + const float popupWidth = (std::max)( + fieldLayout.valueRect.width, + ResolveUIEditorMenuPopupDesiredWidth(popupItems, popupMetrics)); + const float popupHeight = MeasureUIEditorMenuPopupHeight(popupItems, popupMetrics); + const auto placement = ResolvePopupPlacementRect( + fieldLayout.valueRect, + UISize(popupWidth, popupHeight), + viewportRect, + UIPopupPlacement::BottomStart); + return BuildUIEditorMenuPopupLayout(placement.rect, popupItems, popupMetrics); +} + +void SyncControlHover( + UIEditorEnumFieldInteractionState& state, + const UIEditorEnumFieldLayout& layout) { + if (!state.hasPointerPosition) { + state.fieldState.hoveredTarget = UIEditorEnumFieldHitTargetKind::None; + return; + } + + state.fieldState.hoveredTarget = HitTestUIEditorEnumField(layout, state.pointerPosition).kind; +} + +UIEditorMenuPopupHitTarget ResolvePopupHit( + const UIEditorEnumFieldInteractionState& state, + const UIEditorMenuPopupLayout& popupLayout, + const std::vector& popupItems) { + if (!state.popupOpen || !state.hasPointerPosition) { + return {}; + } + + return HitTestUIEditorMenuPopup(popupLayout, popupItems, state.pointerPosition); +} + +void SyncPopupHover( + UIEditorEnumFieldInteractionState& state, + const UIEditorMenuPopupLayout& popupLayout, + const std::vector& popupItems, + std::size_t selectedIndex) { + if (!state.popupOpen) { + state.highlightedIndex = UIEditorMenuPopupInvalidIndex; + return; + } + + if (popupItems.empty()) { + state.popupOpen = false; + state.highlightedIndex = UIEditorMenuPopupInvalidIndex; + return; + } + + const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, popupLayout, popupItems); + if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item) { + state.highlightedIndex = popupHit.index; + return; + } + + if (state.highlightedIndex == UIEditorMenuPopupInvalidIndex || + state.highlightedIndex >= popupItems.size()) { + state.highlightedIndex = (std::min)(selectedIndex, popupItems.size() - 1u); + } +} + +void OpenPopup( + UIEditorEnumFieldInteractionState& state, + const UIEditorEnumFieldSpec& spec, + std::size_t selectedIndex, + UIEditorEnumFieldInteractionResult& result) { + if (spec.readOnly || spec.options.empty()) { + return; + } + + if (!state.popupOpen) { + state.popupOpen = true; + state.highlightedIndex = ClampSelectedIndex(selectedIndex, spec); + result.popupOpened = true; + result.consumed = true; + } +} + +void ClosePopup( + UIEditorEnumFieldInteractionState& state, + UIEditorEnumFieldInteractionResult& result) { + if (!state.popupOpen) { + return; + } + + state.popupOpen = false; + state.pressedPopupIndex = UIEditorMenuPopupInvalidIndex; + state.highlightedIndex = UIEditorMenuPopupInvalidIndex; + result.popupClosed = true; + result.consumed = true; +} + +void MovePopupHighlight( + UIEditorEnumFieldInteractionState& state, + const UIEditorEnumFieldSpec& spec, + int delta) { + if (spec.options.empty()) { + state.highlightedIndex = UIEditorMenuPopupInvalidIndex; + return; + } + + if (state.highlightedIndex == UIEditorMenuPopupInvalidIndex || + state.highlightedIndex >= spec.options.size()) { + state.highlightedIndex = ClampSelectedIndex(0u, spec); + } + + const std::size_t currentIndex = state.highlightedIndex; + if (delta < 0) { + state.highlightedIndex = currentIndex == 0u ? 0u : currentIndex - 1u; } else { - nextIndex = (selectedIndex + 1u >= spec.options.size()) ? spec.options.size() - 1u : selectedIndex + 1u; + state.highlightedIndex = + currentIndex + 1u >= spec.options.size() ? spec.options.size() - 1u : currentIndex + 1u; + } +} + +void JumpPopupHighlightToEdge( + UIEditorEnumFieldInteractionState& state, + const UIEditorEnumFieldSpec& spec, + bool toEnd) { + if (spec.options.empty()) { + state.highlightedIndex = UIEditorMenuPopupInvalidIndex; + return; } - if (nextIndex == selectedIndex) { + state.highlightedIndex = toEnd ? spec.options.size() - 1u : 0u; +} + +bool TrySelectHighlighted( + UIEditorEnumFieldInteractionState& state, + std::size_t& selectedIndex, + const UIEditorEnumFieldSpec& spec, + UIEditorEnumFieldInteractionResult& result) { + if (!state.popupOpen || + spec.readOnly || + spec.options.empty() || + state.highlightedIndex == UIEditorMenuPopupInvalidIndex || + state.highlightedIndex >= spec.options.size()) { return false; } - selectedIndex = nextIndex; + selectedIndex = state.highlightedIndex; + result.selectionChanged = true; + result.selectedIndex = selectedIndex; + result.popupItemIndex = state.highlightedIndex; + ClosePopup(state, result); return true; } @@ -52,22 +251,25 @@ UIEditorEnumFieldInteractionFrame UpdateUIEditorEnumFieldInteraction( UIEditorEnumFieldInteractionState& state, std::size_t& selectedIndex, const ::XCEngine::UI::UIRect& bounds, - const Widgets::UIEditorEnumFieldSpec& spec, + const UIEditorEnumFieldSpec& spec, const std::vector& inputEvents, - const Widgets::UIEditorEnumFieldMetrics& metrics) { - Widgets::UIEditorEnumFieldSpec resolvedSpec = spec; - if (!resolvedSpec.options.empty() && selectedIndex >= resolvedSpec.options.size()) { - selectedIndex = resolvedSpec.options.size() - 1u; - } + const UIEditorEnumFieldMetrics& metrics, + const UIEditorMenuPopupMetrics& popupMetrics, + const ::XCEngine::UI::UIRect& viewportRect) { + UIEditorEnumFieldSpec resolvedSpec = spec; + selectedIndex = ClampSelectedIndex(selectedIndex, resolvedSpec); resolvedSpec.selectedIndex = selectedIndex; - Widgets::UIEditorEnumFieldLayout layout = - BuildUIEditorEnumFieldLayout(bounds, resolvedSpec, metrics); - if (state.hasPointerPosition) { - state.fieldState.hoveredTarget = HitTestUIEditorEnumField(layout, state.pointerPosition).kind; - } else { - state.fieldState.hoveredTarget = UIEditorEnumFieldHitTargetKind::None; - } + UIEditorEnumFieldLayout layout = BuildUIEditorEnumFieldLayout(bounds, resolvedSpec, metrics); + const ::XCEngine::UI::UIRect resolvedViewport = ResolveViewportRect(bounds, viewportRect); + std::vector popupItems = + state.popupOpen ? BuildPopupItems(resolvedSpec, selectedIndex) : std::vector {}; + UIEditorMenuPopupLayout popupLayout = + state.popupOpen ? BuildPopupLayout(layout, popupItems, popupMetrics, resolvedViewport) : UIEditorMenuPopupLayout {}; + + SyncControlHover(state, layout); + SyncPopupHover(state, popupLayout, popupItems, selectedIndex); + state.fieldState.popupOpen = state.popupOpen; UIEditorEnumFieldInteractionResult interactionResult = {}; for (const UIInputEvent& event : inputEvents) { @@ -81,14 +283,17 @@ UIEditorEnumFieldInteractionFrame UpdateUIEditorEnumFieldInteraction( UIEditorEnumFieldInteractionResult eventResult = {}; switch (event.type) { case UIInputEventType::FocusGained: + eventResult.focusedChanged = !state.fieldState.focused; state.fieldState.focused = true; break; case UIInputEventType::FocusLost: + eventResult.focusedChanged = state.fieldState.focused; state.fieldState.focused = false; state.fieldState.active = false; state.hasPointerPosition = false; state.fieldState.hoveredTarget = UIEditorEnumFieldHitTargetKind::None; + ClosePopup(state, eventResult); break; case UIInputEventType::PointerMove: @@ -96,71 +301,134 @@ UIEditorEnumFieldInteractionFrame UpdateUIEditorEnumFieldInteraction( case UIInputEventType::PointerLeave: break; - case UIInputEventType::PointerButtonDown: - eventResult.hitTarget = state.hasPointerPosition - ? HitTestUIEditorEnumField(layout, state.pointerPosition) - : Widgets::UIEditorEnumFieldHitTarget {}; - if (event.pointerButton == UIPointerButton::Left && - eventResult.hitTarget.kind != UIEditorEnumFieldHitTargetKind::None) { + case UIInputEventType::PointerButtonDown: { + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + const UIEditorEnumFieldHitTarget fieldHit = + state.hasPointerPosition ? HitTestUIEditorEnumField(layout, state.pointerPosition) : UIEditorEnumFieldHitTarget {}; + const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, popupLayout, popupItems); + eventResult.hitTarget = fieldHit; + + if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item) { + state.fieldState.focused = true; + state.pressedPopupIndex = popupHit.index; + state.highlightedIndex = popupHit.index; + eventResult.popupItemIndex = popupHit.index; + eventResult.consumed = true; + } else if (popupHit.kind == UIEditorMenuPopupHitTargetKind::PopupSurface) { + state.fieldState.focused = true; + state.pressedPopupIndex = UIEditorMenuPopupInvalidIndex; + eventResult.consumed = true; + } else if ( + fieldHit.kind == UIEditorEnumFieldHitTargetKind::ValueBox || + fieldHit.kind == UIEditorEnumFieldHitTargetKind::DropdownArrow) { + eventResult.focusedChanged = !state.fieldState.focused; state.fieldState.focused = true; state.fieldState.active = true; eventResult.consumed = true; - } else if (event.pointerButton == UIPointerButton::Left) { - state.fieldState.focused = false; + } else { + if (state.popupOpen) { + ClosePopup(state, eventResult); + } + if (state.fieldState.focused) { + eventResult.focusedChanged = true; + state.fieldState.focused = false; + } + state.fieldState.active = false; + state.pressedPopupIndex = UIEditorMenuPopupInvalidIndex; } break; + } - case UIInputEventType::PointerButtonUp: - eventResult.hitTarget = state.hasPointerPosition - ? HitTestUIEditorEnumField(layout, state.pointerPosition) - : Widgets::UIEditorEnumFieldHitTarget {}; - if (event.pointerButton == UIPointerButton::Left && - state.fieldState.active && - !resolvedSpec.readOnly) { - bool changed = false; - if (eventResult.hitTarget.kind == UIEditorEnumFieldHitTargetKind::PreviousButton) { - changed = MoveSelection(selectedIndex, resolvedSpec, -1); - } else if (eventResult.hitTarget.kind == UIEditorEnumFieldHitTargetKind::NextButton) { - changed = MoveSelection(selectedIndex, resolvedSpec, 1); - } - if (changed) { + case UIInputEventType::PointerButtonUp: { + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + const UIEditorEnumFieldHitTarget fieldHit = + state.hasPointerPosition ? HitTestUIEditorEnumField(layout, state.pointerPosition) : UIEditorEnumFieldHitTarget {}; + const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, popupLayout, popupItems); + eventResult.hitTarget = fieldHit; + + if (state.pressedPopupIndex != UIEditorMenuPopupInvalidIndex) { + if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item && + popupHit.index == state.pressedPopupIndex && + popupHit.index < resolvedSpec.options.size()) { + state.highlightedIndex = popupHit.index; + selectedIndex = popupHit.index; eventResult.selectionChanged = true; eventResult.selectedIndex = selectedIndex; - eventResult.consumed = true; + eventResult.popupItemIndex = popupHit.index; + ClosePopup(state, eventResult); + } + state.pressedPopupIndex = UIEditorMenuPopupInvalidIndex; + eventResult.consumed = true; + } else if (state.fieldState.active) { + state.fieldState.active = false; + if (fieldHit.kind == UIEditorEnumFieldHitTargetKind::ValueBox || + fieldHit.kind == UIEditorEnumFieldHitTargetKind::DropdownArrow) { + if (state.popupOpen) { + ClosePopup(state, eventResult); + } else { + OpenPopup(state, resolvedSpec, selectedIndex, eventResult); + } } } - state.fieldState.active = false; break; + } case UIInputEventType::KeyDown: - if (state.fieldState.focused && !resolvedSpec.readOnly) { - bool changed = false; + if (!state.fieldState.focused) { + break; + } + + if (state.popupOpen) { switch (static_cast(event.keyCode)) { - case KeyCode::Left: - changed = MoveSelection(selectedIndex, resolvedSpec, -1); + case KeyCode::Up: + MovePopupHighlight(state, resolvedSpec, -1); + eventResult.consumed = true; break; - case KeyCode::Right: - case KeyCode::Enter: - changed = MoveSelection(selectedIndex, resolvedSpec, 1); + + case KeyCode::Down: + MovePopupHighlight(state, resolvedSpec, 1); + eventResult.consumed = true; break; + case KeyCode::Home: - changed = !resolvedSpec.options.empty() && selectedIndex != 0u; - selectedIndex = 0u; + JumpPopupHighlightToEdge(state, resolvedSpec, false); + eventResult.consumed = true; break; + case KeyCode::End: - if (!resolvedSpec.options.empty()) { - const std::size_t lastIndex = resolvedSpec.options.size() - 1u; - changed = selectedIndex != lastIndex; - selectedIndex = lastIndex; - } + JumpPopupHighlightToEdge(state, resolvedSpec, true); + eventResult.consumed = true; break; + + case KeyCode::Enter: + case KeyCode::Space: + TrySelectHighlighted(state, selectedIndex, resolvedSpec, eventResult); + break; + + case KeyCode::Escape: + ClosePopup(state, eventResult); + break; + default: break; } - if (changed) { - eventResult.selectionChanged = true; - eventResult.selectedIndex = selectedIndex; - eventResult.consumed = true; + } else { + switch (static_cast(event.keyCode)) { + case KeyCode::Enter: + case KeyCode::Space: + case KeyCode::Down: + case KeyCode::Up: + OpenPopup(state, resolvedSpec, selectedIndex, eventResult); + break; + + default: + break; } } break; @@ -169,22 +437,50 @@ UIEditorEnumFieldInteractionFrame UpdateUIEditorEnumFieldInteraction( break; } + selectedIndex = ClampSelectedIndex(selectedIndex, resolvedSpec); resolvedSpec.selectedIndex = selectedIndex; layout = BuildUIEditorEnumFieldLayout(bounds, resolvedSpec, metrics); - if (state.hasPointerPosition) { - state.fieldState.hoveredTarget = HitTestUIEditorEnumField(layout, state.pointerPosition).kind; - } else { - state.fieldState.hoveredTarget = UIEditorEnumFieldHitTargetKind::None; - } + popupItems = state.popupOpen ? BuildPopupItems(resolvedSpec, selectedIndex) : std::vector {}; + popupLayout = state.popupOpen + ? BuildPopupLayout(layout, popupItems, popupMetrics, resolvedViewport) + : UIEditorMenuPopupLayout {}; + SyncControlHover(state, layout); + SyncPopupHover(state, popupLayout, popupItems, selectedIndex); + state.fieldState.popupOpen = state.popupOpen; - if (eventResult.consumed || - eventResult.selectionChanged || - eventResult.hitTarget.kind != UIEditorEnumFieldHitTargetKind::None) { - interactionResult = eventResult; + if (eventResult.selectionChanged || + eventResult.popupOpened || + eventResult.popupClosed || + eventResult.focusedChanged || + eventResult.consumed || + eventResult.hitTarget.kind != UIEditorEnumFieldHitTargetKind::None || + eventResult.popupItemIndex != UIEditorMenuPopupInvalidIndex) { + interactionResult = std::move(eventResult); } } - return { layout, interactionResult }; + resolvedSpec.selectedIndex = selectedIndex; + layout = BuildUIEditorEnumFieldLayout(bounds, resolvedSpec, metrics); + popupItems = state.popupOpen ? BuildPopupItems(resolvedSpec, selectedIndex) : std::vector {}; + popupLayout = state.popupOpen + ? BuildPopupLayout(layout, popupItems, popupMetrics, resolvedViewport) + : UIEditorMenuPopupLayout {}; + SyncControlHover(state, layout); + SyncPopupHover(state, popupLayout, popupItems, selectedIndex); + state.fieldState.popupOpen = state.popupOpen; + + UIEditorMenuPopupState popupState = {}; + popupState.focused = state.fieldState.focused || state.popupOpen; + popupState.hoveredIndex = state.popupOpen ? state.highlightedIndex : UIEditorMenuPopupInvalidIndex; + + return { + std::move(layout), + std::move(popupLayout), + std::move(popupState), + std::move(popupItems), + state.popupOpen, + std::move(interactionResult) + }; } } // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Core/UIEditorNumberFieldInteraction.cpp b/new_editor/src/Core/UIEditorNumberFieldInteraction.cpp index 26a92bc8..253ad1b2 100644 --- a/new_editor/src/Core/UIEditorNumberFieldInteraction.cpp +++ b/new_editor/src/Core/UIEditorNumberFieldInteraction.cpp @@ -53,38 +53,6 @@ bool IsPermittedCharacter(const UIEditorNumberFieldSpec& spec, std::uint32_t cha return !spec.integerMode && character == static_cast('.'); } -double NormalizeValue(const UIEditorNumberFieldSpec& spec, double value) { - const double minValue = (std::min)(spec.minValue, spec.maxValue); - const double maxValue = (std::max)(spec.minValue, spec.maxValue); - value = (std::clamp)(value, minValue, maxValue); - if (spec.integerMode) { - value = static_cast(std::llround(value)); - } - return value; -} - -bool TryParseValue( - const UIEditorNumberFieldSpec& spec, - const std::string& text, - double& outValue) { - if (text.empty()) { - return false; - } - - try { - std::size_t consumed = 0u; - const double parsed = std::stod(text, &consumed); - if (consumed != text.size()) { - return false; - } - - outValue = NormalizeValue(spec, parsed); - return true; - } catch (...) { - return false; - } -} - void SyncDisplayText( UIEditorNumberFieldInteractionState& state, const UIEditorNumberFieldSpec& spec) { @@ -141,7 +109,7 @@ bool CommitEdit( } double parsedValue = spec.value; - if (!TryParseValue(spec, state.textInputState.value, parsedValue)) { + if (!TryParseUIEditorNumberFieldValue(spec, state.textInputState.value, parsedValue)) { result.consumed = true; result.editCommitRejected = true; return false; @@ -196,7 +164,7 @@ bool ApplyStep( const double step = spec.step == 0.0 ? 1.0 : spec.step; const double before = spec.value; - spec.value = NormalizeValue(spec, spec.value + step * direction); + spec.value = NormalizeUIEditorNumberFieldValue(spec, spec.value + step * direction); result.consumed = true; result.stepApplied = true; @@ -228,7 +196,9 @@ bool ApplyKeyboardStep( } const double before = spec.value; - spec.value = NormalizeValue(spec, (std::min)(spec.minValue, spec.maxValue)); + spec.value = NormalizeUIEditorNumberFieldValue( + spec, + (std::min)(spec.minValue, spec.maxValue)); result.consumed = true; result.stepApplied = true; result.valueBefore = before; @@ -244,7 +214,9 @@ bool ApplyKeyboardStep( } const double before = spec.value; - spec.value = NormalizeValue(spec, (std::max)(spec.minValue, spec.maxValue)); + spec.value = NormalizeUIEditorNumberFieldValue( + spec, + (std::max)(spec.minValue, spec.maxValue)); result.consumed = true; result.stepApplied = true; result.valueBefore = before; @@ -356,15 +328,7 @@ UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction( const UIEditorNumberFieldHitTargetKind activeTarget = state.numberFieldState.activeTarget; state.numberFieldState.activeTarget = UIEditorNumberFieldHitTargetKind::None; - if (activeTarget == UIEditorNumberFieldHitTargetKind::DecrementButton && - hitTarget.kind == UIEditorNumberFieldHitTargetKind::DecrementButton) { - ApplyStep(state, spec, -1.0, eventResult); - } else if ( - activeTarget == UIEditorNumberFieldHitTargetKind::IncrementButton && - hitTarget.kind == UIEditorNumberFieldHitTargetKind::IncrementButton) { - ApplyStep(state, spec, 1.0, eventResult); - } else if ( - activeTarget == UIEditorNumberFieldHitTargetKind::ValueBox && + if (activeTarget == UIEditorNumberFieldHitTargetKind::ValueBox && hitTarget.kind == UIEditorNumberFieldHitTargetKind::ValueBox) { if (!state.numberFieldState.editing) { eventResult.editStarted = BeginEdit(state, spec, false); diff --git a/new_editor/src/Core/UIEditorPropertyGridInteraction.cpp b/new_editor/src/Core/UIEditorPropertyGridInteraction.cpp index 3cba1575..713db339 100644 --- a/new_editor/src/Core/UIEditorPropertyGridInteraction.cpp +++ b/new_editor/src/Core/UIEditorPropertyGridInteraction.cpp @@ -1,7 +1,12 @@ #include -#include +#include +#include +#include +#include + +#include #include namespace XCEngine::UI::Editor { @@ -14,13 +19,28 @@ using ::XCEngine::UI::Text::InsertCharacter; using ::XCEngine::UI::UIInputEvent; using ::XCEngine::UI::UIInputEventType; using ::XCEngine::UI::UIPointerButton; +using ::XCEngine::UI::UISize; +using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect; +using ::XCEngine::UI::Widgets::UIPopupPlacement; using Widgets::BuildUIEditorPropertyGridLayout; +using Widgets::FindUIEditorPropertyGridFieldLocation; using Widgets::FindUIEditorPropertyGridVisibleFieldIndex; using Widgets::HitTestUIEditorPropertyGrid; using Widgets::IsUIEditorPropertyGridPointInside; +using Widgets::ResolveUIEditorPropertyGridFieldValueText; +using Widgets::UIEditorPropertyGridField; +using Widgets::UIEditorPropertyGridFieldKind; using Widgets::UIEditorPropertyGridHitTarget; using Widgets::UIEditorPropertyGridHitTargetKind; using Widgets::UIEditorPropertyGridInvalidIndex; +using Widgets::UIEditorPropertyGridLayout; +using Widgets::UIEditorPropertyGridSection; +using Widgets::UIEditorMenuPopupHitTarget; +using Widgets::UIEditorMenuPopupHitTargetKind; +using Widgets::UIEditorMenuPopupInvalidIndex; +using Widgets::UIEditorMenuPopupItem; +using Widgets::UIEditorMenuPopupLayout; +using ::XCEngine::UI::Editor::UIEditorMenuItemKind; bool ShouldUsePointerPosition(const UIInputEvent& event) { switch (event.type) { @@ -58,19 +78,190 @@ bool ApplyKeyboardNavigation( void ClearHoverState(UIEditorPropertyGridInteractionState& state) { state.propertyGridState.hoveredSectionId.clear(); state.propertyGridState.hoveredFieldId.clear(); + state.propertyGridState.hoveredHitTarget = UIEditorPropertyGridHitTargetKind::None; +} + +void ClearPopupState(UIEditorPropertyGridInteractionState& state) { + state.propertyGridState.popupFieldId.clear(); + state.propertyGridState.popupHighlightedIndex = UIEditorPropertyGridInvalidIndex; + state.pressedPopupIndex = UIEditorPropertyGridInvalidIndex; +} + +bool IsInlineEditable(const UIEditorPropertyGridField& field) { + return field.kind == UIEditorPropertyGridFieldKind::Text || + field.kind == UIEditorPropertyGridFieldKind::Number; +} + +bool IsNumberEditCharacter(const UIEditorPropertyGridField& field, std::uint32_t character) { + if (field.kind != UIEditorPropertyGridFieldKind::Number) { + return true; + } + + if (character >= static_cast('0') && + character <= static_cast('9')) { + return true; + } + + if (character == static_cast('-') || + character == static_cast('+')) { + return true; + } + + return !field.numberValue.integerMode && character == static_cast('.'); +} + +UIEditorPropertyGridField* FindMutableField( + std::vector& sections, + std::string_view fieldId) { + const auto location = FindUIEditorPropertyGridFieldLocation(sections, fieldId); + if (!location.IsValid() || + location.sectionIndex >= sections.size() || + location.fieldIndex >= sections[location.sectionIndex].fields.size()) { + return nullptr; + } + + return §ions[location.sectionIndex].fields[location.fieldIndex]; +} + +const UIEditorPropertyGridField* FindField( + const std::vector& sections, + std::string_view fieldId) { + const auto location = FindUIEditorPropertyGridFieldLocation(sections, fieldId); + if (!location.IsValid() || + location.sectionIndex >= sections.size() || + location.fieldIndex >= sections[location.sectionIndex].fields.size()) { + return nullptr; + } + + return §ions[location.sectionIndex].fields[location.fieldIndex]; +} + +void SetChangedValueResult( + const UIEditorPropertyGridField& field, + UIEditorPropertyGridInteractionResult& result) { + result.fieldValueChanged = true; + result.changedFieldId = field.fieldId; + result.changedValue = ResolveUIEditorPropertyGridFieldValueText(field); +} + +std::vector BuildPopupItems( + const UIEditorPropertyGridField& field) { + std::vector items = {}; + if (field.kind != UIEditorPropertyGridFieldKind::Enum) { + return items; + } + + items.reserve(field.enumValue.options.size()); + const std::size_t selectedIndex = + field.enumValue.options.empty() + ? 0u + : (std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u); + for (std::size_t index = 0u; index < field.enumValue.options.size(); ++index) { + UIEditorMenuPopupItem item = {}; + item.itemId = field.fieldId + "." + std::to_string(index); + item.kind = UIEditorMenuItemKind::Command; + item.label = field.enumValue.options[index]; + item.enabled = !field.readOnly; + item.checked = index == selectedIndex; + items.push_back(std::move(item)); + } + + return items; +} + +::XCEngine::UI::UIRect ResolvePopupViewportRect( + const ::XCEngine::UI::UIRect& bounds) { + return ::XCEngine::UI::UIRect(bounds.x - 4096.0f, bounds.y - 4096.0f, 8192.0f, 8192.0f); +} + +bool BuildPopupLayout( + const UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + const Widgets::UIEditorMenuPopupMetrics& popupMetrics, + UIEditorMenuPopupLayout& popupLayout, + std::vector& popupItems) { + if (state.propertyGridState.popupFieldId.empty()) { + return false; + } + + const std::size_t visibleFieldIndex = + FindUIEditorPropertyGridVisibleFieldIndex( + layout, + state.propertyGridState.popupFieldId, + sections); + if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || + visibleFieldIndex >= layout.visibleFieldIndices.size()) { + return false; + } + + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + return false; + } + + const UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + popupItems = BuildPopupItems(field); + if (popupItems.empty()) { + return false; + } + + const float popupWidth = (std::max)( + layout.fieldValueRects[visibleFieldIndex].width, + Widgets::ResolveUIEditorMenuPopupDesiredWidth(popupItems, popupMetrics)); + const float popupHeight = Widgets::MeasureUIEditorMenuPopupHeight(popupItems, popupMetrics); + const auto placement = ResolvePopupPlacementRect( + layout.fieldValueRects[visibleFieldIndex], + UISize(popupWidth, popupHeight), + ResolvePopupViewportRect(layout.bounds), + UIPopupPlacement::BottomStart); + popupLayout = Widgets::BuildUIEditorMenuPopupLayout(placement.rect, popupItems, popupMetrics); + return true; +} + +UIEditorMenuPopupHitTarget ResolvePopupHit( + const UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { + if (!state.hasPointerPosition) { + return {}; + } + + UIEditorMenuPopupLayout popupLayout = {}; + std::vector popupItems = {}; + if (!BuildPopupLayout(state, layout, sections, popupMetrics, popupLayout, popupItems)) { + return {}; + } + + return Widgets::HitTestUIEditorMenuPopup(popupLayout, popupItems, state.pointerPosition); } void SyncHoverTarget( UIEditorPropertyGridInteractionState& state, - const Widgets::UIEditorPropertyGridLayout& layout, - const std::vector& sections) { + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { ClearHoverState(state); if (!state.hasPointerPosition) { return; } + const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, layout, sections, popupMetrics); + if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item) { + state.propertyGridState.popupHighlightedIndex = popupHit.index; + return; + } + if (popupHit.kind != UIEditorMenuPopupHitTargetKind::None) { + return; + } + const UIEditorPropertyGridHitTarget hitTarget = HitTestUIEditorPropertyGrid(layout, state.pointerPosition); + state.propertyGridState.hoveredHitTarget = hitTarget.kind; if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::SectionHeader && hitTarget.sectionIndex < sections.size()) { state.propertyGridState.hoveredSectionId = sections[hitTarget.sectionIndex].sectionId; @@ -89,8 +280,8 @@ void SyncHoverTarget( void SyncKeyboardNavigation( UIEditorPropertyGridInteractionState& state, const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - const Widgets::UIEditorPropertyGridLayout& layout, - const std::vector& sections) { + const UIEditorPropertyGridLayout& layout, + const std::vector& sections) { state.keyboardNavigation.SetItemCount(layout.visibleFieldIndices.size()); state.keyboardNavigation.ClampToItemCount(); @@ -113,16 +304,47 @@ void SyncKeyboardNavigation( } } -bool BeginFieldEdit( +bool SelectVisibleField( UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, - const Widgets::UIEditorPropertyGridField& field, + ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + std::size_t visibleFieldIndex, UIEditorPropertyGridInteractionResult& result) { - if (field.readOnly) { + if (visibleFieldIndex >= layout.visibleFieldIndices.size() || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size()) { return false; } - const bool changed = propertyEditModel.BeginEdit(field.fieldId, field.valueText); + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + return false; + } + + const UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + result.selectionChanged = selectionModel.SetSelection(field.fieldId); + result.selectedFieldId = field.fieldId; + result.consumed = true; + state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); + return true; +} + +bool BeginFieldEdit( + UIEditorPropertyGridInteractionState& state, + ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const UIEditorPropertyGridField& field, + UIEditorPropertyGridInteractionResult& result) { + if (field.readOnly || !IsInlineEditable(field)) { + return false; + } + + const std::string initialValue = + field.kind == UIEditorPropertyGridFieldKind::Text + ? field.valueText + : ResolveUIEditorPropertyGridFieldValueText(field); + const bool changed = propertyEditModel.BeginEdit(field.fieldId, initialValue); if (!changed && (!propertyEditModel.HasActiveEdit() || propertyEditModel.GetActiveFieldId() != field.fieldId)) { @@ -140,15 +362,54 @@ bool BeginFieldEdit( bool CommitActiveEdit( UIEditorPropertyGridInteractionState& state, ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + std::vector& sections, UIEditorPropertyGridInteractionResult& result) { if (!propertyEditModel.HasActiveEdit()) { return false; } - if (!propertyEditModel.CommitEdit(&result.committedFieldId, &result.committedValue)) { + UIEditorPropertyGridField* field = + FindMutableField(sections, propertyEditModel.GetActiveFieldId()); + if (field == nullptr) { + propertyEditModel.CancelEdit(); + state.textInputState = {}; return false; } + result.activeFieldId = field->fieldId; + if (field->kind == UIEditorPropertyGridFieldKind::Number) { + Widgets::UIEditorNumberFieldSpec spec = {}; + spec.fieldId = field->fieldId; + spec.label = field->label; + spec.value = field->numberValue.value; + spec.step = field->numberValue.step; + spec.minValue = field->numberValue.minValue; + spec.maxValue = field->numberValue.maxValue; + spec.integerMode = field->numberValue.integerMode; + spec.readOnly = field->readOnly; + + double parsedValue = field->numberValue.value; + if (!Widgets::TryParseUIEditorNumberFieldValue( + spec, + propertyEditModel.GetStagedValue(), + parsedValue)) { + result.editCommitRejected = true; + result.consumed = true; + return false; + } + + field->numberValue.value = parsedValue; + result.committedFieldId = field->fieldId; + result.committedValue = ResolveUIEditorPropertyGridFieldValueText(*field); + SetChangedValueResult(*field, result); + } else { + field->valueText = propertyEditModel.GetStagedValue(); + result.committedFieldId = field->fieldId; + result.committedValue = field->valueText; + SetChangedValueResult(*field, result); + } + + propertyEditModel.CommitEdit(); state.textInputState = {}; result.editCommitted = true; result.consumed = true; @@ -174,30 +435,112 @@ bool CancelActiveEdit( return true; } -bool SelectVisibleField( +void ClosePopup( UIEditorPropertyGridInteractionState& state, - ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - const Widgets::UIEditorPropertyGridLayout& layout, - const std::vector& sections, - std::size_t visibleFieldIndex, UIEditorPropertyGridInteractionResult& result) { - if (visibleFieldIndex >= layout.visibleFieldIndices.size() || - visibleFieldIndex >= layout.visibleFieldSectionIndices.size()) { - return false; + if (state.propertyGridState.popupFieldId.empty()) { + return; } - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - if (sectionIndex >= sections.size() || - fieldIndex >= sections[sectionIndex].fields.size()) { - return false; - } - - const Widgets::UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; - result.selectionChanged = selectionModel.SetSelection(field.fieldId); - result.selectedFieldId = field.fieldId; + ClearPopupState(state); + result.popupClosed = true; + result.consumed = true; +} + +void OpenPopup( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridField& field, + UIEditorPropertyGridInteractionResult& result) { + if (field.kind != UIEditorPropertyGridFieldKind::Enum || + field.readOnly || + field.enumValue.options.empty()) { + return; + } + + if (state.propertyGridState.popupFieldId == field.fieldId) { + return; + } + + state.propertyGridState.popupFieldId = field.fieldId; + state.propertyGridState.popupHighlightedIndex = + (std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u); + result.popupOpened = true; + result.consumed = true; +} + +void MovePopupHighlight( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridField& field, + int delta) { + if (field.kind != UIEditorPropertyGridFieldKind::Enum || + field.enumValue.options.empty()) { + state.propertyGridState.popupHighlightedIndex = UIEditorPropertyGridInvalidIndex; + return; + } + + if (state.propertyGridState.popupHighlightedIndex == UIEditorPropertyGridInvalidIndex || + state.propertyGridState.popupHighlightedIndex >= field.enumValue.options.size()) { + state.propertyGridState.popupHighlightedIndex = + (std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u); + } + + const std::size_t currentIndex = state.propertyGridState.popupHighlightedIndex; + if (delta < 0) { + state.propertyGridState.popupHighlightedIndex = currentIndex == 0u ? 0u : currentIndex - 1u; + } else { + state.propertyGridState.popupHighlightedIndex = + currentIndex + 1u >= field.enumValue.options.size() + ? field.enumValue.options.size() - 1u + : currentIndex + 1u; + } +} + +void JumpPopupHighlightToEdge( + UIEditorPropertyGridInteractionState& state, + const UIEditorPropertyGridField& field, + bool toEnd) { + if (field.kind != UIEditorPropertyGridFieldKind::Enum || + field.enumValue.options.empty()) { + state.propertyGridState.popupHighlightedIndex = UIEditorPropertyGridInvalidIndex; + return; + } + + state.propertyGridState.popupHighlightedIndex = toEnd + ? field.enumValue.options.size() - 1u + : 0u; +} + +bool CommitPopupSelection( + UIEditorPropertyGridInteractionState& state, + std::vector& sections, + UIEditorPropertyGridInteractionResult& result) { + UIEditorPropertyGridField* field = + FindMutableField(sections, state.propertyGridState.popupFieldId); + if (field == nullptr || + field->kind != UIEditorPropertyGridFieldKind::Enum || + field->readOnly || + field->enumValue.options.empty() || + state.propertyGridState.popupHighlightedIndex == UIEditorPropertyGridInvalidIndex || + state.propertyGridState.popupHighlightedIndex >= field->enumValue.options.size()) { + return false; + } + + field->enumValue.selectedIndex = state.propertyGridState.popupHighlightedIndex; + SetChangedValueResult(*field, result); + ClosePopup(state, result); + return true; +} + +bool ToggleBoolField( + UIEditorPropertyGridField& field, + UIEditorPropertyGridInteractionResult& result) { + if (field.kind != UIEditorPropertyGridFieldKind::Bool || field.readOnly) { + return false; + } + + field.boolValue = !field.boolValue; + SetChangedValueResult(field, result); result.consumed = true; - state.keyboardNavigation.SetCurrentIndex(visibleFieldIndex); return true; } @@ -209,13 +552,14 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel, ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const ::XCEngine::UI::UIRect& bounds, - const std::vector& sections, + std::vector& sections, const std::vector& inputEvents, - const Widgets::UIEditorPropertyGridMetrics& metrics) { - Widgets::UIEditorPropertyGridLayout layout = + const Widgets::UIEditorPropertyGridMetrics& metrics, + const Widgets::UIEditorMenuPopupMetrics& popupMetrics) { + UIEditorPropertyGridLayout layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); SyncKeyboardNavigation(state, selectionModel, layout, sections); - SyncHoverTarget(state, layout, sections); + SyncHoverTarget(state, layout, sections, popupMetrics); UIEditorPropertyGridInteractionResult interactionResult = {}; for (const UIInputEvent& event : inputEvents) { @@ -227,14 +571,23 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( } UIEditorPropertyGridInteractionResult eventResult = {}; + const UIEditorMenuPopupHitTarget popupHit = ResolvePopupHit(state, layout, sections, popupMetrics); + const UIEditorPropertyGridHitTarget hitTarget = + state.hasPointerPosition + ? HitTestUIEditorPropertyGrid(layout, state.pointerPosition) + : UIEditorPropertyGridHitTarget {}; + eventResult.hitTarget = hitTarget; + switch (event.type) { case UIInputEventType::FocusGained: state.propertyGridState.focused = true; break; case UIInputEventType::FocusLost: - CommitActiveEdit(state, propertyEditModel, eventResult); + CommitActiveEdit(state, propertyEditModel, sections, eventResult); + ClosePopup(state, eventResult); state.propertyGridState.focused = false; + state.propertyGridState.pressedFieldId.clear(); state.hasPointerPosition = false; ClearHoverState(state); break; @@ -245,74 +598,91 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( break; case UIInputEventType::PointerButtonDown: { - const UIEditorPropertyGridHitTarget hitTarget = - state.hasPointerPosition - ? HitTestUIEditorPropertyGrid(layout, state.pointerPosition) - : UIEditorPropertyGridHitTarget {}; - eventResult.hitTarget = hitTarget; - const bool insideGrid = - state.hasPointerPosition && - IsUIEditorPropertyGridPointInside(layout.bounds, state.pointerPosition); - if ((event.pointerButton == UIPointerButton::Left || - event.pointerButton == UIPointerButton::Right) && - hitTarget.kind != UIEditorPropertyGridHitTargetKind::None) { - state.propertyGridState.focused = true; - eventResult.consumed = true; - } else if (event.pointerButton == UIPointerButton::Left && insideGrid) { - state.propertyGridState.focused = true; - eventResult.consumed = true; - } else if (event.pointerButton == UIPointerButton::Left) { - state.propertyGridState.focused = false; + if (event.pointerButton == UIPointerButton::Left) { + const bool insideGrid = + state.hasPointerPosition && + IsUIEditorPropertyGridPointInside(layout.bounds, state.pointerPosition); + if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item) { + state.propertyGridState.focused = true; + state.pressedPopupIndex = popupHit.index; + eventResult.consumed = true; + } else if (popupHit.kind == UIEditorMenuPopupHitTargetKind::PopupSurface) { + state.propertyGridState.focused = true; + eventResult.consumed = true; + } else if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::FieldRow || + hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) { + state.propertyGridState.focused = true; + state.propertyGridState.pressedFieldId = + sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex].fieldId; + eventResult.consumed = true; + } else if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::SectionHeader || insideGrid) { + state.propertyGridState.focused = true; + state.propertyGridState.pressedFieldId.clear(); + eventResult.consumed = true; + } else { + state.propertyGridState.focused = false; + state.propertyGridState.pressedFieldId.clear(); + } } break; } case UIInputEventType::PointerButtonUp: { - const UIEditorPropertyGridHitTarget hitTarget = - state.hasPointerPosition - ? HitTestUIEditorPropertyGrid(layout, state.pointerPosition) - : UIEditorPropertyGridHitTarget {}; - eventResult.hitTarget = hitTarget; - - const bool insideGrid = - state.hasPointerPosition && - IsUIEditorPropertyGridPointInside(layout.bounds, state.pointerPosition); - if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::None) { - if (event.pointerButton == UIPointerButton::Left) { - CommitActiveEdit(state, propertyEditModel, eventResult); - if (insideGrid) { - state.propertyGridState.focused = true; - eventResult.consumed = true; - } else { - state.propertyGridState.focused = false; - } - } - break; - } - if (event.pointerButton == UIPointerButton::Left) { - const bool clickingActiveValueBox = - hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox && - hitTarget.sectionIndex < sections.size() && - hitTarget.fieldIndex < sections[hitTarget.sectionIndex].fields.size() && - propertyEditModel.HasActiveEdit() && - propertyEditModel.GetActiveFieldId() == - sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex].fieldId; - if (!clickingActiveValueBox) { - CommitActiveEdit(state, propertyEditModel, eventResult); + const bool insideGrid = + state.hasPointerPosition && + IsUIEditorPropertyGridPointInside(layout.bounds, state.pointerPosition); + + if (state.pressedPopupIndex != UIEditorPropertyGridInvalidIndex) { + if (popupHit.kind == UIEditorMenuPopupHitTargetKind::Item && + popupHit.index == state.pressedPopupIndex) { + eventResult.fieldValueChanged = + CommitPopupSelection(state, sections, eventResult); + } else if (popupHit.kind == UIEditorMenuPopupHitTargetKind::None) { + ClosePopup(state, eventResult); + } + state.pressedPopupIndex = UIEditorPropertyGridInvalidIndex; + state.propertyGridState.pressedFieldId.clear(); + eventResult.consumed = true; + break; + } + + if (popupHit.kind == UIEditorMenuPopupHitTargetKind::PopupSurface) { + state.propertyGridState.pressedFieldId.clear(); + eventResult.consumed = true; + break; + } + + if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::None) { + CommitActiveEdit(state, propertyEditModel, sections, eventResult); + ClosePopup(state, eventResult); + state.propertyGridState.focused = insideGrid; + state.propertyGridState.pressedFieldId.clear(); + if (insideGrid) { + eventResult.consumed = true; + } + break; } if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::SectionHeader && hitTarget.sectionIndex < sections.size()) { + CommitActiveEdit(state, propertyEditModel, sections, eventResult); + ClosePopup(state, eventResult); const std::string& sectionId = sections[hitTarget.sectionIndex].sectionId; eventResult.sectionToggled = expansionModel.ToggleExpanded(sectionId); eventResult.toggledSectionId = sectionId; eventResult.consumed = true; state.propertyGridState.focused = true; - } else if ((hitTarget.kind == UIEditorPropertyGridHitTargetKind::FieldRow || - hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) && + state.propertyGridState.pressedFieldId.clear(); + break; + } + + if ((hitTarget.kind == UIEditorPropertyGridHitTargetKind::FieldRow || + hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) && hitTarget.sectionIndex < sections.size() && hitTarget.fieldIndex < sections[hitTarget.sectionIndex].fields.size()) { + UIEditorPropertyGridField& field = + sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex]; state.propertyGridState.focused = true; SelectVisibleField( state, @@ -322,18 +692,39 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( hitTarget.visibleFieldIndex, eventResult); - const Widgets::UIEditorPropertyGridField& field = - sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex]; if (hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) { - BeginFieldEdit(state, propertyEditModel, field, eventResult); + if (field.kind == UIEditorPropertyGridFieldKind::Bool) { + CommitActiveEdit(state, propertyEditModel, sections, eventResult); + ClosePopup(state, eventResult); + ToggleBoolField(field, eventResult); + } else if (field.kind == UIEditorPropertyGridFieldKind::Enum) { + CommitActiveEdit(state, propertyEditModel, sections, eventResult); + if (state.propertyGridState.popupFieldId == field.fieldId) { + ClosePopup(state, eventResult); + } else { + ClearPopupState(state); + OpenPopup(state, field, eventResult); + } + } else { + ClosePopup(state, eventResult); + if (!(propertyEditModel.HasActiveEdit() && + propertyEditModel.GetActiveFieldId() == field.fieldId)) { + CommitActiveEdit(state, propertyEditModel, sections, eventResult); + BeginFieldEdit(state, propertyEditModel, field, eventResult); + } + } + } else if (state.propertyGridState.popupFieldId != field.fieldId) { + ClosePopup(state, eventResult); } } + + state.propertyGridState.pressedFieldId.clear(); } else if (event.pointerButton == UIPointerButton::Right && (hitTarget.kind == UIEditorPropertyGridHitTargetKind::FieldRow || hitTarget.kind == UIEditorPropertyGridHitTargetKind::ValueBox) && hitTarget.sectionIndex < sections.size() && hitTarget.fieldIndex < sections[hitTarget.sectionIndex].fields.size()) { - const Widgets::UIEditorPropertyGridField& field = + const UIEditorPropertyGridField& field = sections[hitTarget.sectionIndex].fields[hitTarget.fieldIndex]; eventResult.selectionChanged = selectionModel.SetSelection(field.fieldId); eventResult.selectedFieldId = field.fieldId; @@ -352,6 +743,42 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( break; } + if (!state.propertyGridState.popupFieldId.empty()) { + const UIEditorPropertyGridField* popupField = + FindField(sections, state.propertyGridState.popupFieldId); + if (popupField != nullptr) { + switch (static_cast(event.keyCode)) { + case KeyCode::Up: + MovePopupHighlight(state, *popupField, -1); + eventResult.consumed = true; + break; + case KeyCode::Down: + MovePopupHighlight(state, *popupField, 1); + eventResult.consumed = true; + break; + case KeyCode::Home: + JumpPopupHighlightToEdge(state, *popupField, false); + eventResult.consumed = true; + break; + case KeyCode::End: + JumpPopupHighlightToEdge(state, *popupField, true); + eventResult.consumed = true; + break; + case KeyCode::Enter: + case KeyCode::Space: + CommitPopupSelection(state, sections, eventResult); + eventResult.consumed = true; + break; + case KeyCode::Escape: + ClosePopup(state, eventResult); + break; + default: + break; + } + } + break; + } + if (propertyEditModel.HasActiveEdit()) { if (static_cast(event.keyCode) == KeyCode::Escape) { CancelActiveEdit(state, propertyEditModel, eventResult); @@ -371,29 +798,24 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( eventResult.editValueChanged = true; } if (editResult.submitRequested) { - CommitActiveEdit(state, propertyEditModel, eventResult); + CommitActiveEdit(state, propertyEditModel, sections, eventResult); } } break; } - if (static_cast(event.keyCode) == KeyCode::Enter && + if ((static_cast(event.keyCode) == KeyCode::Enter || + static_cast(event.keyCode) == KeyCode::Space) && selectionModel.HasSelection()) { - const std::size_t visibleFieldIndex = - FindUIEditorPropertyGridVisibleFieldIndex( - layout, - selectionModel.GetSelectedId(), - sections); - if (visibleFieldIndex != UIEditorPropertyGridInvalidIndex) { - const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; - const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; - if (sectionIndex < sections.size() && - fieldIndex < sections[sectionIndex].fields.size()) { - BeginFieldEdit( - state, - propertyEditModel, - sections[sectionIndex].fields[fieldIndex], - eventResult); + UIEditorPropertyGridField* field = + FindMutableField(sections, selectionModel.GetSelectedId()); + if (field != nullptr) { + if (field->kind == UIEditorPropertyGridFieldKind::Bool) { + ToggleBoolField(*field, eventResult); + } else if (field->kind == UIEditorPropertyGridFieldKind::Enum) { + OpenPopup(state, *field, eventResult); + } else if (static_cast(event.keyCode) == KeyCode::Enter) { + BeginFieldEdit(state, propertyEditModel, *field, eventResult); } } break; @@ -423,6 +845,12 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( break; } + if (const UIEditorPropertyGridField* field = + FindField(sections, propertyEditModel.GetActiveFieldId()); + field == nullptr || !IsNumberEditCharacter(*field, event.character)) { + break; + } + if (InsertCharacter(state.textInputState, event.character)) { propertyEditModel.UpdateStagedValue(state.textInputState.value); eventResult.consumed = true; @@ -437,7 +865,7 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); SyncKeyboardNavigation(state, selectionModel, layout, sections); - SyncHoverTarget(state, layout, sections); + SyncHoverTarget(state, layout, sections, popupMetrics); if (eventResult.hitTarget.kind == UIEditorPropertyGridHitTargetKind::None && state.hasPointerPosition) { eventResult.hitTarget = HitTestUIEditorPropertyGrid(layout, state.pointerPosition); @@ -450,20 +878,25 @@ UIEditorPropertyGridInteractionFrame UpdateUIEditorPropertyGridInteraction( eventResult.editStarted || eventResult.editValueChanged || eventResult.editCommitted || + eventResult.editCommitRejected || eventResult.editCanceled || + eventResult.popupOpened || + eventResult.popupClosed || + eventResult.fieldValueChanged || eventResult.secondaryClicked || eventResult.hitTarget.kind != UIEditorPropertyGridHitTargetKind::None || !eventResult.toggledSectionId.empty() || !eventResult.selectedFieldId.empty() || !eventResult.activeFieldId.empty() || - !eventResult.committedFieldId.empty()) { + !eventResult.committedFieldId.empty() || + !eventResult.changedFieldId.empty()) { interactionResult = std::move(eventResult); } } layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); SyncKeyboardNavigation(state, selectionModel, layout, sections); - SyncHoverTarget(state, layout, sections); + SyncHoverTarget(state, layout, sections, popupMetrics); if (interactionResult.hitTarget.kind == UIEditorPropertyGridHitTargetKind::None && state.hasPointerPosition) { interactionResult.hitTarget = HitTestUIEditorPropertyGrid(layout, state.pointerPosition); diff --git a/new_editor/src/Core/UIEditorTextFieldInteraction.cpp b/new_editor/src/Core/UIEditorTextFieldInteraction.cpp new file mode 100644 index 00000000..79726805 --- /dev/null +++ b/new_editor/src/Core/UIEditorTextFieldInteraction.cpp @@ -0,0 +1,316 @@ +#include + +#include + +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPointerButton; +using ::XCEngine::UI::Text::HandleKeyDown; +using ::XCEngine::UI::Text::InsertCharacter; +using ::XCEngine::UI::Editor::Widgets::BuildUIEditorTextFieldLayout; +using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField; +using ::XCEngine::UI::Editor::Widgets::IsUIEditorTextFieldPointInside; +using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTarget; +using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTargetKind; +using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldLayout; +using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics; +using ::XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec; + +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; + } +} + +void SyncDisplayText( + UIEditorTextFieldInteractionState& state, + const UIEditorTextFieldSpec& spec) { + if (!state.textFieldState.editing) { + state.textFieldState.displayText = spec.value; + } +} + +void SyncHoverTarget( + UIEditorTextFieldInteractionState& state, + const UIEditorTextFieldLayout& layout) { + if (!state.hasPointerPosition) { + state.textFieldState.hoveredTarget = UIEditorTextFieldHitTargetKind::None; + return; + } + + state.textFieldState.hoveredTarget = + HitTestUIEditorTextField(layout, state.pointerPosition).kind; +} + +bool BeginEdit( + UIEditorTextFieldInteractionState& state, + const UIEditorTextFieldSpec& spec, + bool clearText) { + if (spec.readOnly) { + return false; + } + + const bool changed = state.editModel.BeginEdit(spec.fieldId, spec.value); + if (!changed && + state.editModel.HasActiveEdit() && + state.editModel.GetActiveFieldId() != spec.fieldId) { + return false; + } + if (!changed && state.textFieldState.editing) { + return false; + } + + state.textFieldState.editing = true; + state.textInputState.value = clearText ? std::string() : spec.value; + state.textInputState.caret = state.textInputState.value.size(); + state.editModel.UpdateStagedValue(state.textInputState.value); + state.textFieldState.displayText = state.textInputState.value; + return true; +} + +bool CommitEdit( + UIEditorTextFieldInteractionState& state, + UIEditorTextFieldSpec& spec, + UIEditorTextFieldInteractionResult& result) { + if (!state.textFieldState.editing || !state.editModel.HasActiveEdit()) { + return false; + } + + result.valueBefore = spec.value; + spec.value = state.textInputState.value; + result.valueAfter = spec.value; + result.valueChanged = result.valueBefore != result.valueAfter; + result.editCommitted = true; + result.consumed = true; + result.committedText = spec.value; + + state.editModel.CommitEdit(); + state.textInputState = {}; + state.textFieldState.editing = false; + state.textFieldState.displayText = spec.value; + return true; +} + +bool CancelEdit( + UIEditorTextFieldInteractionState& state, + const UIEditorTextFieldSpec& spec, + UIEditorTextFieldInteractionResult& result) { + if (!state.textFieldState.editing || !state.editModel.HasActiveEdit()) { + return false; + } + + state.editModel.CancelEdit(); + state.textInputState = {}; + state.textFieldState.editing = false; + state.textFieldState.displayText = spec.value; + result.consumed = true; + result.editCanceled = true; + result.valueBefore = spec.value; + result.valueAfter = spec.value; + return true; +} + +} // namespace + +UIEditorTextFieldInteractionFrame UpdateUIEditorTextFieldInteraction( + UIEditorTextFieldInteractionState& state, + UIEditorTextFieldSpec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector& inputEvents, + const UIEditorTextFieldMetrics& metrics) { + UIEditorTextFieldLayout layout = BuildUIEditorTextFieldLayout(bounds, spec, metrics); + SyncDisplayText(state, spec); + SyncHoverTarget(state, layout); + + UIEditorTextFieldInteractionResult interactionResult = {}; + for (const UIInputEvent& event : inputEvents) { + if (ShouldUsePointerPosition(event)) { + state.pointerPosition = event.position; + state.hasPointerPosition = true; + } else if (event.type == UIInputEventType::PointerLeave) { + state.hasPointerPosition = false; + } + + UIEditorTextFieldInteractionResult eventResult = {}; + switch (event.type) { + case UIInputEventType::FocusGained: + eventResult.focusChanged = !state.textFieldState.focused; + state.textFieldState.focused = true; + break; + + case UIInputEventType::FocusLost: + eventResult.focusChanged = state.textFieldState.focused; + state.textFieldState.focused = false; + state.textFieldState.activeTarget = UIEditorTextFieldHitTargetKind::None; + state.hasPointerPosition = false; + if (state.textFieldState.editing) { + CommitEdit(state, spec, eventResult); + } + break; + + case UIInputEventType::PointerMove: + case UIInputEventType::PointerEnter: + case UIInputEventType::PointerLeave: + break; + + case UIInputEventType::PointerButtonDown: { + const UIEditorTextFieldHitTarget hitTarget = + state.hasPointerPosition + ? HitTestUIEditorTextField(layout, state.pointerPosition) + : UIEditorTextFieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + const bool insideField = + state.hasPointerPosition && + IsUIEditorTextFieldPointInside(layout.bounds, state.pointerPosition); + if (insideField) { + eventResult.focusChanged = !state.textFieldState.focused; + state.textFieldState.focused = true; + state.textFieldState.activeTarget = + hitTarget.kind == UIEditorTextFieldHitTargetKind::None + ? UIEditorTextFieldHitTargetKind::Row + : hitTarget.kind; + eventResult.consumed = true; + } else { + if (state.textFieldState.editing) { + CommitEdit(state, spec, eventResult); + eventResult.focusChanged = state.textFieldState.focused; + state.textFieldState.focused = false; + } else if (state.textFieldState.focused) { + eventResult.focusChanged = true; + state.textFieldState.focused = false; + } + state.textFieldState.activeTarget = UIEditorTextFieldHitTargetKind::None; + } + break; + } + + case UIInputEventType::PointerButtonUp: { + const UIEditorTextFieldHitTarget hitTarget = + state.hasPointerPosition + ? HitTestUIEditorTextField(layout, state.pointerPosition) + : UIEditorTextFieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton == UIPointerButton::Left) { + const UIEditorTextFieldHitTargetKind activeTarget = state.textFieldState.activeTarget; + state.textFieldState.activeTarget = UIEditorTextFieldHitTargetKind::None; + + if (activeTarget == UIEditorTextFieldHitTargetKind::ValueBox && + hitTarget.kind == UIEditorTextFieldHitTargetKind::ValueBox) { + if (!state.textFieldState.editing) { + eventResult.editStarted = BeginEdit(state, spec, false); + } + eventResult.consumed = true; + } else if (hitTarget.kind == UIEditorTextFieldHitTargetKind::Row) { + eventResult.consumed = true; + } + } + break; + } + + case UIInputEventType::KeyDown: + if (!state.textFieldState.focused) { + break; + } + + if (state.textFieldState.editing) { + if (event.keyCode == static_cast(KeyCode::Escape)) { + CancelEdit(state, spec, eventResult); + break; + } + + const auto textResult = + HandleKeyDown(state.textInputState, event.keyCode, event.modifiers); + if (textResult.handled) { + state.editModel.UpdateStagedValue(state.textInputState.value); + state.textFieldState.displayText = state.textInputState.value; + eventResult.consumed = true; + eventResult.valueBefore = spec.value; + eventResult.valueAfter = spec.value; + if (textResult.submitRequested) { + CommitEdit(state, spec, eventResult); + } + } + } else if (event.keyCode == static_cast(KeyCode::Enter)) { + eventResult.editStarted = BeginEdit(state, spec, false); + eventResult.consumed = eventResult.editStarted; + } + break; + + case UIInputEventType::Character: + if (!state.textFieldState.focused || + spec.readOnly || + event.modifiers.control || + event.modifiers.alt || + event.modifiers.super) { + break; + } + + if (!state.textFieldState.editing) { + eventResult.editStarted = BeginEdit(state, spec, true); + } + + if (InsertCharacter(state.textInputState, event.character)) { + state.editModel.UpdateStagedValue(state.textInputState.value); + state.textFieldState.displayText = state.textInputState.value; + eventResult.consumed = true; + } + break; + + default: + break; + } + + layout = BuildUIEditorTextFieldLayout(bounds, spec, metrics); + SyncDisplayText(state, spec); + SyncHoverTarget(state, layout); + if (eventResult.hitTarget.kind == UIEditorTextFieldHitTargetKind::None && + state.hasPointerPosition) { + eventResult.hitTarget = HitTestUIEditorTextField(layout, state.pointerPosition); + } + + if (eventResult.consumed || + eventResult.focusChanged || + eventResult.valueChanged || + eventResult.editStarted || + eventResult.editCommitted || + eventResult.editCanceled || + eventResult.hitTarget.kind != UIEditorTextFieldHitTargetKind::None) { + interactionResult = std::move(eventResult); + } + } + + layout = BuildUIEditorTextFieldLayout(bounds, spec, metrics); + SyncDisplayText(state, spec); + SyncHoverTarget(state, layout); + if (interactionResult.hitTarget.kind == UIEditorTextFieldHitTargetKind::None && + state.hasPointerPosition) { + interactionResult.hitTarget = HitTestUIEditorTextField(layout, state.pointerPosition); + } + + return { + std::move(layout), + std::move(interactionResult) + }; +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Core/UIEditorTheme.cpp b/new_editor/src/Core/UIEditorTheme.cpp new file mode 100644 index 00000000..0ba98877 --- /dev/null +++ b/new_editor/src/Core/UIEditorTheme.cpp @@ -0,0 +1,1431 @@ +#include + +#include + +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using ::XCEngine::Math::Color; +using ::XCEngine::UI::UIColor; +using ::XCEngine::UI::Style::UICornerRadius; +using ::XCEngine::UI::Style::UIStyleValueType; +using ::XCEngine::UI::Style::UITheme; +using ::XCEngine::UI::Style::UITokenResolveResult; +using ::XCEngine::UI::Style::UITokenResolveStatus; + +constexpr UIColor kTransparent(0.0f, 0.0f, 0.0f, 0.0f); + +UIColor ToUIColor(const Color& color) { + return UIColor(color.r, color.g, color.b, color.a); +} + +bool TryResolveThemeFloat( + const UITheme& theme, + std::string_view tokenName, + float& outValue) { + UITokenResolveResult result = + theme.ResolveToken(std::string(tokenName), UIStyleValueType::Float); + if (result.status == UITokenResolveStatus::Resolved) { + if (const float* value = result.value.TryGetFloat(); value != nullptr) { + outValue = *value; + return true; + } + } + + result = theme.ResolveToken(std::string(tokenName), UIStyleValueType::CornerRadius); + if (result.status == UITokenResolveStatus::Resolved) { + if (const UICornerRadius* value = result.value.TryGetCornerRadius(); + value != nullptr && + value->IsUniform()) { + outValue = value->topLeft; + return true; + } + } + + return false; +} + +bool TryResolveThemeColor( + const UITheme& theme, + std::string_view tokenName, + UIColor& outValue) { + const UITokenResolveResult result = + theme.ResolveToken(std::string(tokenName), UIStyleValueType::Color); + if (result.status != UITokenResolveStatus::Resolved) { + return false; + } + + const Color* value = result.value.TryGetColor(); + if (value == nullptr) { + return false; + } + + outValue = ToUIColor(*value); + return true; +} + +float ResolveThemeFloatAliases( + const UITheme& theme, + std::initializer_list tokenNames, + float fallbackValue) { + float resolvedValue = fallbackValue; + for (const std::string_view tokenName : tokenNames) { + if (TryResolveThemeFloat(theme, tokenName, resolvedValue)) { + return resolvedValue; + } + } + + return fallbackValue; +} + +UIColor ResolveThemeColorAliases( + const UITheme& theme, + std::initializer_list tokenNames, + const UIColor& fallbackValue) { + UIColor resolvedValue = fallbackValue; + for (const std::string_view tokenName : tokenNames) { + if (TryResolveThemeColor(theme, tokenName, resolvedValue)) { + return resolvedValue; + } + } + + return fallbackValue; +} + +} // namespace + +float ResolveUIEditorThemeFloat( + const UITheme& theme, + std::string_view tokenName, + float fallbackValue) { + return ResolveThemeFloatAliases(theme, { tokenName }, fallbackValue); +} + +UIColor ResolveUIEditorThemeColor( + const UITheme& theme, + std::string_view tokenName, + const UIColor& fallbackValue) { + return ResolveThemeColorAliases(theme, { tokenName }, fallbackValue); +} + +Widgets::UIEditorBoolFieldMetrics ResolveUIEditorBoolFieldMetrics( + const UITheme& theme, + const Widgets::UIEditorBoolFieldMetrics& fallback) { + Widgets::UIEditorBoolFieldMetrics 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.checkboxSize = + ResolveUIEditorThemeFloat(theme, "editor.size.field.checkbox", metrics.checkboxSize); + metrics.labelTextInsetY = + ResolveUIEditorThemeFloat(theme, "editor.space.field.label_inset_y", metrics.labelTextInsetY); + metrics.checkboxGlyphInsetX = ResolveThemeFloatAliases( + theme, + { "editor.space.field.checkbox_mark_inset_x", "editor.space.field.checkbox_glyph_inset_x" }, + metrics.checkboxGlyphInsetX); + metrics.checkboxGlyphInsetY = ResolveThemeFloatAliases( + theme, + { "editor.space.field.checkbox_mark_inset_y", "editor.space.field.checkbox_glyph_inset_y" }, + metrics.checkboxGlyphInsetY); + metrics.cornerRounding = + ResolveUIEditorThemeFloat(theme, "editor.radius.field.row", metrics.cornerRounding); + metrics.checkboxRounding = ResolveThemeFloatAliases( + theme, + { "editor.radius.field.checkbox", "editor.radius.field.control" }, + metrics.checkboxRounding); + metrics.borderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.field", metrics.borderThickness); + metrics.focusedBorderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.field.focus", metrics.focusedBorderThickness); + metrics.labelFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.label", "editor.font.field.label" }, + metrics.labelFontSize); + metrics.checkboxGlyphFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.checkbox_mark", "editor.font.field.glyph" }, + metrics.checkboxGlyphFontSize); + return metrics; +} + +Widgets::UIEditorBoolFieldPalette ResolveUIEditorBoolFieldPalette( + const UITheme& theme, + const Widgets::UIEditorBoolFieldPalette& fallback) { + Widgets::UIEditorBoolFieldPalette 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.checkboxColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.checkbox", palette.checkboxColor); + palette.checkboxHoverColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.checkbox_hover", palette.checkboxHoverColor); + palette.checkboxReadOnlyColor = ResolveThemeColorAliases( + theme, + { "editor.color.field.checkbox_readonly", "editor.color.field.control_readonly" }, + palette.checkboxReadOnlyColor); + palette.checkboxBorderColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.checkbox_border", + palette.checkboxBorderColor); + palette.checkboxMarkColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.checkbox_mark", + palette.checkboxMarkColor); + palette.labelColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.label", palette.labelColor); + return palette; +} + +Widgets::UIEditorNumberFieldMetrics ResolveUIEditorNumberFieldMetrics( + const UITheme& theme, + const Widgets::UIEditorNumberFieldMetrics& fallback) { + Widgets::UIEditorNumberFieldMetrics 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.valueBoxMinWidth = ResolveThemeFloatAliases( + theme, + { "editor.size.field.number_min_width", "editor.size.field.control_min_width" }, + metrics.valueBoxMinWidth); + metrics.controlInsetY = + ResolveUIEditorThemeFloat(theme, "editor.space.field.control_inset_y", metrics.controlInsetY); + metrics.labelTextInsetY = + ResolveUIEditorThemeFloat(theme, "editor.space.field.label_inset_y", metrics.labelTextInsetY); + 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.cornerRounding = + ResolveUIEditorThemeFloat(theme, "editor.radius.field.row", metrics.cornerRounding); + metrics.valueBoxRounding = + ResolveUIEditorThemeFloat(theme, "editor.radius.field.control", metrics.valueBoxRounding); + metrics.borderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.field", metrics.borderThickness); + metrics.focusedBorderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.field.focus", metrics.focusedBorderThickness); + metrics.labelFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.label", "editor.font.field.label" }, + metrics.labelFontSize); + metrics.valueFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.value", "editor.font.field.value" }, + metrics.valueFontSize); + return metrics; +} + +Widgets::UIEditorNumberFieldPalette ResolveUIEditorNumberFieldPalette( + const UITheme& theme, + const Widgets::UIEditorNumberFieldPalette& fallback) { + Widgets::UIEditorNumberFieldPalette 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.valueBoxColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.control", palette.valueBoxColor); + palette.valueBoxHoverColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.control_hover", palette.valueBoxHoverColor); + palette.valueBoxEditingColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_editing", + palette.valueBoxEditingColor); + palette.readOnlyColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_readonly", + palette.readOnlyColor); + palette.controlBorderColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_border", + palette.controlBorderColor); + palette.controlFocusedBorderColor = ResolveThemeColorAliases( + theme, + { "editor.color.field.control_border_focus", "editor.color.field.border_focus" }, + palette.controlFocusedBorderColor); + 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); + return palette; +} + +Widgets::UIEditorTextFieldMetrics ResolveUIEditorTextFieldMetrics( + const UITheme& theme, + const Widgets::UIEditorTextFieldMetrics& fallback) { + Widgets::UIEditorTextFieldMetrics 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.valueBoxMinWidth = ResolveThemeFloatAliases( + theme, + { "editor.size.field.text_min_width", "editor.size.field.control_min_width" }, + metrics.valueBoxMinWidth); + metrics.controlInsetY = + ResolveUIEditorThemeFloat(theme, "editor.space.field.control_inset_y", metrics.controlInsetY); + metrics.labelTextInsetY = + ResolveUIEditorThemeFloat(theme, "editor.space.field.label_inset_y", metrics.labelTextInsetY); + 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.cornerRounding = + ResolveUIEditorThemeFloat(theme, "editor.radius.field.row", metrics.cornerRounding); + metrics.valueBoxRounding = + ResolveUIEditorThemeFloat(theme, "editor.radius.field.control", metrics.valueBoxRounding); + metrics.borderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.field", metrics.borderThickness); + metrics.focusedBorderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.field.focus", metrics.focusedBorderThickness); + metrics.labelFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.label", "editor.font.field.label" }, + metrics.labelFontSize); + metrics.valueFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.value", "editor.font.field.value" }, + metrics.valueFontSize); + return metrics; +} + +Widgets::UIEditorTextFieldPalette ResolveUIEditorTextFieldPalette( + const UITheme& theme, + const Widgets::UIEditorTextFieldPalette& fallback) { + Widgets::UIEditorTextFieldPalette 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.valueBoxColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.control", palette.valueBoxColor); + palette.valueBoxHoverColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.control_hover", palette.valueBoxHoverColor); + palette.valueBoxEditingColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_editing", + palette.valueBoxEditingColor); + palette.readOnlyColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_readonly", + palette.readOnlyColor); + palette.controlBorderColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_border", + palette.controlBorderColor); + palette.controlFocusedBorderColor = ResolveThemeColorAliases( + theme, + { "editor.color.field.control_border_focus", "editor.color.field.border_focus" }, + palette.controlFocusedBorderColor); + 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); + return palette; +} + +Widgets::UIEditorVector2FieldMetrics ResolveUIEditorVector2FieldMetrics( + const UITheme& theme, + const Widgets::UIEditorVector2FieldMetrics& fallback) { + Widgets::UIEditorVector2FieldMetrics 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::UIEditorVector2FieldPalette ResolveUIEditorVector2FieldPalette( + const UITheme& theme, + const Widgets::UIEditorVector2FieldPalette& fallback) { + Widgets::UIEditorVector2FieldPalette 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); + return palette; +} + +Widgets::UIEditorVector3FieldMetrics ResolveUIEditorVector3FieldMetrics( + const UITheme& theme, + const Widgets::UIEditorVector3FieldMetrics& fallback) { + Widgets::UIEditorVector3FieldMetrics 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::UIEditorVector3FieldPalette ResolveUIEditorVector3FieldPalette( + const UITheme& theme, + const Widgets::UIEditorVector3FieldPalette& fallback) { + Widgets::UIEditorVector3FieldPalette 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); + return palette; +} + +Widgets::UIEditorEnumFieldMetrics ResolveUIEditorEnumFieldMetrics( + const UITheme& theme, + const Widgets::UIEditorEnumFieldMetrics& fallback) { + Widgets::UIEditorEnumFieldMetrics 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.valueBoxMinWidth = ResolveThemeFloatAliases( + theme, + { "editor.size.field.enum_min_width", "editor.size.field.control_min_width" }, + metrics.valueBoxMinWidth); + metrics.controlInsetY = + ResolveUIEditorThemeFloat(theme, "editor.space.field.control_inset_y", metrics.controlInsetY); + metrics.labelTextInsetY = + ResolveUIEditorThemeFloat(theme, "editor.space.field.label_inset_y", metrics.labelTextInsetY); + 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.dropdownArrowWidth = ResolveUIEditorThemeFloat( + theme, + "editor.size.field.dropdown_arrow_width", + metrics.dropdownArrowWidth); + metrics.dropdownArrowInsetX = ResolveUIEditorThemeFloat( + theme, + "editor.space.field.dropdown_arrow_inset_x", + metrics.dropdownArrowInsetX); + metrics.dropdownArrowInsetY = ResolveUIEditorThemeFloat( + theme, + "editor.space.field.dropdown_arrow_inset_y", + metrics.dropdownArrowInsetY); + metrics.cornerRounding = + ResolveUIEditorThemeFloat(theme, "editor.radius.field.row", metrics.cornerRounding); + metrics.valueBoxRounding = + ResolveUIEditorThemeFloat(theme, "editor.radius.field.control", metrics.valueBoxRounding); + metrics.borderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.field", metrics.borderThickness); + metrics.focusedBorderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.field.focus", metrics.focusedBorderThickness); + metrics.labelFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.label", "editor.font.field.label" }, + metrics.labelFontSize); + metrics.valueFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.value", "editor.font.field.value" }, + metrics.valueFontSize); + metrics.dropdownArrowFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.field.dropdown_arrow", "editor.font.field.glyph" }, + metrics.dropdownArrowFontSize); + return metrics; +} + +Widgets::UIEditorEnumFieldPalette ResolveUIEditorEnumFieldPalette( + const UITheme& theme, + const Widgets::UIEditorEnumFieldPalette& fallback) { + Widgets::UIEditorEnumFieldPalette 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.valueBoxColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.control", palette.valueBoxColor); + palette.valueBoxHoverColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.control_hover", palette.valueBoxHoverColor); + palette.readOnlyColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_readonly", + palette.readOnlyColor); + palette.controlBorderColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.control_border", + palette.controlBorderColor); + palette.labelColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.label", palette.labelColor); + palette.valueColor = + ResolveUIEditorThemeColor(theme, "editor.color.field.value", palette.valueColor); + palette.arrowColor = ResolveUIEditorThemeColor( + theme, + "editor.color.field.dropdown_arrow", + palette.arrowColor); + return palette; +} + +Widgets::UIEditorMenuPopupMetrics ResolveUIEditorMenuPopupMetrics( + const UITheme& theme, + const Widgets::UIEditorMenuPopupMetrics& fallback) { + Widgets::UIEditorMenuPopupMetrics metrics = fallback; + metrics.contentPaddingX = ResolveThemeFloatAliases( + theme, + { + "editor.space.popup.padding_x", + "editor.space.menu_popup.padding_x", + "editor.space.menu.padding_x" + }, + metrics.contentPaddingX); + metrics.contentPaddingY = ResolveThemeFloatAliases( + theme, + { + "editor.space.popup.padding_y", + "editor.space.menu_popup.padding_y", + "editor.space.menu.padding_y" + }, + metrics.contentPaddingY); + metrics.itemHeight = ResolveThemeFloatAliases( + theme, + { + "editor.size.popup.item", + "editor.size.menu_popup.item", + "editor.size.menu.item" + }, + metrics.itemHeight); + metrics.separatorHeight = ResolveThemeFloatAliases( + theme, + { + "editor.size.popup.separator", + "editor.size.menu_popup.separator", + "editor.size.menu.separator" + }, + metrics.separatorHeight); + metrics.checkColumnWidth = ResolveThemeFloatAliases( + theme, + { + "editor.size.popup.check_column", + "editor.size.menu_popup.check_column", + "editor.size.menu.check_column" + }, + metrics.checkColumnWidth); + metrics.shortcutGap = ResolveThemeFloatAliases( + theme, + { + "editor.space.popup.shortcut_gap", + "editor.space.menu_popup.shortcut_gap", + "editor.space.menu.shortcut_gap" + }, + metrics.shortcutGap); + metrics.submenuIndicatorWidth = ResolveThemeFloatAliases( + theme, + { + "editor.size.popup.submenu_indicator", + "editor.size.menu_popup.submenu_indicator", + "editor.size.menu.submenu_indicator" + }, + metrics.submenuIndicatorWidth); + metrics.rowCornerRounding = ResolveThemeFloatAliases( + theme, + { + "editor.radius.popup.row", + "editor.radius.menu_popup.row", + "editor.radius.menu.row" + }, + metrics.rowCornerRounding); + metrics.popupCornerRounding = ResolveThemeFloatAliases( + theme, + { + "editor.radius.popup.surface", + "editor.radius.popup.panel", + "editor.radius.menu_popup.surface", + "editor.radius.menu.surface" + }, + metrics.popupCornerRounding); + metrics.labelInsetX = ResolveThemeFloatAliases( + theme, + { + "editor.space.popup.label_inset_x", + "editor.space.menu_popup.label_inset_x", + "editor.space.menu.label_inset_x" + }, + metrics.labelInsetX); + metrics.labelInsetY = ResolveThemeFloatAliases( + theme, + { + "editor.space.popup.label_inset_y", + "editor.space.menu_popup.label_inset_y", + "editor.space.menu.label_inset_y" + }, + metrics.labelInsetY); + metrics.labelFontSize = ResolveThemeFloatAliases( + theme, + { + "editor.typography.popup.label", + "editor.font.popup.label", + "editor.typography.menu_popup.label", + "editor.font.menu_popup.label", + "editor.typography.menu.label", + "editor.font.menu.label" + }, + metrics.labelFontSize); + metrics.shortcutInsetRight = ResolveThemeFloatAliases( + theme, + { + "editor.space.popup.shortcut_inset_right", + "editor.space.menu_popup.shortcut_inset_right", + "editor.space.menu.shortcut_inset_right" + }, + metrics.shortcutInsetRight); + metrics.estimatedGlyphWidth = ResolveThemeFloatAliases( + theme, + { + "editor.size.popup.estimated_glyph", + "editor.size.popup.estimated_glyph_width", + "editor.size.popup.glyph_width", + "editor.size.menu_popup.estimated_glyph", + "editor.size.menu_popup.estimated_glyph_width", + "editor.size.menu_popup.glyph_width", + "editor.size.menu.estimated_glyph", + "editor.size.menu.estimated_glyph_width", + "editor.size.menu.glyph_width" + }, + metrics.estimatedGlyphWidth); + metrics.glyphFontSize = ResolveThemeFloatAliases( + theme, + { + "editor.typography.popup.glyph", + "editor.font.popup.glyph", + "editor.typography.menu_popup.glyph", + "editor.font.menu_popup.glyph", + "editor.typography.menu.glyph", + "editor.font.menu.glyph" + }, + metrics.glyphFontSize); + metrics.separatorThickness = ResolveThemeFloatAliases( + theme, + { + "editor.border.popup.separator", + "editor.border.menu_popup.separator", + "editor.border.menu.separator" + }, + metrics.separatorThickness); + metrics.borderThickness = ResolveThemeFloatAliases( + theme, + { + "editor.border.popup", + "editor.border.popup.surface", + "editor.border.menu_popup", + "editor.border.menu_popup.surface", + "editor.border.menu.surface" + }, + metrics.borderThickness); + return metrics; +} + +Widgets::UIEditorMenuPopupPalette ResolveUIEditorMenuPopupPalette( + const UITheme& theme, + const Widgets::UIEditorMenuPopupPalette& fallback) { + Widgets::UIEditorMenuPopupPalette palette = fallback; + palette.popupColor = ResolveThemeColorAliases( + theme, + { + "editor.color.popup.surface", + "editor.color.menu_popup.surface", + "editor.color.menu.surface" + }, + palette.popupColor); + palette.borderColor = ResolveThemeColorAliases( + theme, + { + "editor.color.popup.border", + "editor.color.menu_popup.border", + "editor.color.menu.border" + }, + palette.borderColor); + palette.itemHoverColor = ResolveThemeColorAliases( + theme, + { + "editor.color.popup.item_hover", + "editor.color.menu_popup.item_hover", + "editor.color.menu.item_hover" + }, + palette.itemHoverColor); + palette.itemOpenColor = ResolveThemeColorAliases( + theme, + { + "editor.color.popup.item_open", + "editor.color.menu_popup.item_open", + "editor.color.menu.item_open" + }, + palette.itemOpenColor); + palette.separatorColor = ResolveThemeColorAliases( + theme, + { + "editor.color.popup.separator", + "editor.color.menu_popup.separator", + "editor.color.menu.separator" + }, + palette.separatorColor); + palette.textPrimary = ResolveThemeColorAliases( + theme, + { + "editor.color.popup.text", + "editor.color.popup.label", + "editor.color.menu_popup.text", + "editor.color.menu_popup.label", + "editor.color.menu.text", + "editor.color.menu.label" + }, + palette.textPrimary); + palette.textMuted = ResolveThemeColorAliases( + theme, + { + "editor.color.popup.shortcut", + "editor.color.popup.text_muted", + "editor.color.menu_popup.shortcut", + "editor.color.menu_popup.text_muted", + "editor.color.menu.shortcut", + "editor.color.menu.text_muted" + }, + palette.textMuted); + palette.textDisabled = ResolveThemeColorAliases( + theme, + { + "editor.color.popup.text_disabled", + "editor.color.menu_popup.text_disabled", + "editor.color.menu.text_disabled" + }, + palette.textDisabled); + palette.glyphColor = ResolveThemeColorAliases( + theme, + { + "editor.color.popup.glyph", + "editor.color.menu_popup.glyph", + "editor.color.menu.glyph" + }, + palette.glyphColor); + return palette; +} + +Widgets::UIEditorPropertyGridMetrics ResolveUIEditorPropertyGridMetrics( + const UITheme& theme, + const Widgets::UIEditorPropertyGridMetrics& fallback) { + Widgets::UIEditorPropertyGridMetrics metrics = fallback; + metrics.contentInset = + ResolveUIEditorThemeFloat(theme, "editor.space.property.content_inset", metrics.contentInset); + metrics.sectionGap = + ResolveUIEditorThemeFloat(theme, "editor.space.property.section_gap", metrics.sectionGap); + metrics.sectionHeaderHeight = ResolveUIEditorThemeFloat( + theme, + "editor.size.property.section_header", + metrics.sectionHeaderHeight); + metrics.fieldRowHeight = ResolveThemeFloatAliases( + theme, + { "editor.size.property.field_row", "editor.size.field.row" }, + metrics.fieldRowHeight); + metrics.rowGap = + ResolveUIEditorThemeFloat(theme, "editor.space.property.row_gap", metrics.rowGap); + metrics.horizontalPadding = ResolveThemeFloatAliases( + theme, + { "editor.space.property.padding_x", "editor.space.field.padding_x" }, + metrics.horizontalPadding); + metrics.controlColumnStart = ResolveThemeFloatAliases( + theme, + { "editor.layout.property.control_column", "editor.layout.field.control_column" }, + metrics.controlColumnStart); + metrics.labelControlGap = ResolveThemeFloatAliases( + theme, + { "editor.space.property.label_gap", "editor.space.field.label_gap" }, + metrics.labelControlGap); + metrics.disclosureExtent = ResolveThemeFloatAliases( + theme, + { "editor.size.property.disclosure", "editor.size.property.disclosure_extent" }, + metrics.disclosureExtent); + metrics.disclosureLabelGap = ResolveThemeFloatAliases( + theme, + { "editor.space.property.disclosure_gap", "editor.space.property.disclosure_label_gap" }, + metrics.disclosureLabelGap); + metrics.sectionTextInsetY = ResolveUIEditorThemeFloat( + theme, + "editor.space.property.section_inset_y", + metrics.sectionTextInsetY); + metrics.disclosureGlyphInsetX = ResolveUIEditorThemeFloat( + theme, + "editor.space.property.disclosure_glyph_inset_x", + metrics.disclosureGlyphInsetX); + metrics.disclosureGlyphInsetY = ResolveUIEditorThemeFloat( + theme, + "editor.space.property.disclosure_glyph_inset_y", + metrics.disclosureGlyphInsetY); + metrics.disclosureGlyphFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.property.disclosure", "editor.font.property.disclosure" }, + metrics.disclosureGlyphFontSize); + metrics.sectionFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.property.section", "editor.font.property.section" }, + metrics.sectionFontSize); + metrics.labelTextInsetY = ResolveThemeFloatAliases( + theme, + { "editor.space.property.label_inset_y", "editor.space.field.label_inset_y" }, + metrics.labelTextInsetY); + metrics.valueTextInsetY = ResolveThemeFloatAliases( + theme, + { "editor.space.property.value_inset_y", "editor.space.field.value_inset_y" }, + metrics.valueTextInsetY); + metrics.valueBoxInsetY = ResolveThemeFloatAliases( + theme, + { + "editor.space.property.control_inset_y", + "editor.space.property.value_box_inset_y", + "editor.space.field.control_inset_y" + }, + metrics.valueBoxInsetY); + metrics.valueBoxInsetX = ResolveThemeFloatAliases( + theme, + { + "editor.space.property.value_inset_x", + "editor.space.property.value_box_inset_x", + "editor.space.field.value_inset_x" + }, + metrics.valueBoxInsetX); + metrics.cornerRounding = ResolveThemeFloatAliases( + theme, + { "editor.radius.property.grid", "editor.radius.property.panel" }, + metrics.cornerRounding); + metrics.valueBoxRounding = ResolveThemeFloatAliases( + theme, + { "editor.radius.property.value", "editor.radius.field.control" }, + metrics.valueBoxRounding); + metrics.borderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.property", metrics.borderThickness); + metrics.focusedBorderThickness = + ResolveUIEditorThemeFloat(theme, "editor.border.property.focus", metrics.focusedBorderThickness); + metrics.editOutlineThickness = ResolveThemeFloatAliases( + theme, + { "editor.border.property.edit", "editor.border.property.edit_outline" }, + metrics.editOutlineThickness); + metrics.labelFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.property.label", "editor.font.property.label" }, + metrics.labelFontSize); + metrics.valueFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.property.value", "editor.font.property.value" }, + metrics.valueFontSize); + metrics.tagFontSize = ResolveThemeFloatAliases( + theme, + { "editor.typography.property.tag", "editor.font.property.tag" }, + metrics.tagFontSize); + return metrics; +} + +Widgets::UIEditorPropertyGridPalette ResolveUIEditorPropertyGridPalette( + const UITheme& theme, + const Widgets::UIEditorPropertyGridPalette& fallback) { + Widgets::UIEditorPropertyGridPalette palette = fallback; + palette.surfaceColor = + ResolveUIEditorThemeColor(theme, "editor.color.property.surface", palette.surfaceColor); + palette.borderColor = + ResolveUIEditorThemeColor(theme, "editor.color.property.border", palette.borderColor); + palette.focusedBorderColor = ResolveUIEditorThemeColor( + theme, + "editor.color.property.border_focus", + palette.focusedBorderColor); + palette.sectionHeaderColor = + ResolveUIEditorThemeColor(theme, "editor.color.property.section", palette.sectionHeaderColor); + palette.sectionHeaderHoverColor = ResolveUIEditorThemeColor( + theme, + "editor.color.property.section_hover", + palette.sectionHeaderHoverColor); + palette.fieldHoverColor = ResolveUIEditorThemeColor( + theme, + "editor.color.property.field_hover", + palette.fieldHoverColor); + palette.fieldSelectedColor = ResolveUIEditorThemeColor( + theme, + "editor.color.property.field_selected", + palette.fieldSelectedColor); + palette.fieldSelectedFocusedColor = ResolveUIEditorThemeColor( + theme, + "editor.color.property.field_selected_focused", + palette.fieldSelectedFocusedColor); + palette.valueBoxColor = ResolveThemeColorAliases( + theme, + { "editor.color.property.control", "editor.color.property.value" }, + palette.valueBoxColor); + palette.valueBoxHoverColor = ResolveThemeColorAliases( + theme, + { "editor.color.property.control_hover", "editor.color.property.value_hover" }, + palette.valueBoxHoverColor); + palette.valueBoxEditingColor = ResolveThemeColorAliases( + theme, + { "editor.color.property.control_editing", "editor.color.property.value_editing" }, + palette.valueBoxEditingColor); + palette.valueBoxReadOnlyColor = ResolveThemeColorAliases( + theme, + { "editor.color.property.control_readonly", "editor.color.property.value_readonly" }, + palette.valueBoxReadOnlyColor); + palette.valueBoxBorderColor = ResolveThemeColorAliases( + theme, + { "editor.color.property.control_border", "editor.color.property.value_border" }, + palette.valueBoxBorderColor); + palette.valueBoxEditingBorderColor = ResolveThemeColorAliases( + theme, + { "editor.color.property.control_border_editing", "editor.color.property.value_border_editing" }, + palette.valueBoxEditingBorderColor); + palette.disclosureColor = ResolveUIEditorThemeColor( + theme, + "editor.color.property.disclosure", + palette.disclosureColor); + palette.sectionTextColor = ResolveUIEditorThemeColor( + theme, + "editor.color.property.section_text", + palette.sectionTextColor); + palette.labelTextColor = + ResolveUIEditorThemeColor(theme, "editor.color.property.label", palette.labelTextColor); + palette.valueTextColor = ResolveThemeColorAliases( + theme, + { + "editor.color.property.value_text", + "editor.color.property.text", + "editor.color.property.value" + }, + palette.valueTextColor); + palette.readOnlyValueTextColor = ResolveThemeColorAliases( + theme, + { + "editor.color.property.value_text_readonly", + "editor.color.property.text_readonly", + "editor.color.property.value_readonly" + }, + palette.readOnlyValueTextColor); + palette.editTagColor = + ResolveUIEditorThemeColor(theme, "editor.color.property.edit_tag", palette.editTagColor); + return palette; +} + +Widgets::UIEditorBoolFieldMetrics BuildUIEditorHostedBoolFieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorBoolFieldMetrics& fallback) { + Widgets::UIEditorBoolFieldMetrics hosted = fallback; + hosted.rowHeight = propertyGridMetrics.fieldRowHeight; + hosted.horizontalPadding = propertyGridMetrics.horizontalPadding; + hosted.labelControlGap = propertyGridMetrics.labelControlGap; + hosted.controlColumnStart = propertyGridMetrics.controlColumnStart; + hosted.controlTrailingInset = propertyGridMetrics.valueBoxInsetX; + hosted.labelTextInsetY = propertyGridMetrics.labelTextInsetY; + hosted.cornerRounding = propertyGridMetrics.cornerRounding; + hosted.borderThickness = propertyGridMetrics.borderThickness; + hosted.focusedBorderThickness = propertyGridMetrics.focusedBorderThickness; + hosted.labelFontSize = propertyGridMetrics.labelFontSize; + hosted.checkboxGlyphFontSize = propertyGridMetrics.valueFontSize; + return hosted; +} + +Widgets::UIEditorBoolFieldPalette BuildUIEditorHostedBoolFieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorBoolFieldPalette& fallback) { + Widgets::UIEditorBoolFieldPalette hosted = fallback; + hosted.surfaceColor = kTransparent; + hosted.borderColor = kTransparent; + hosted.focusedBorderColor = kTransparent; + hosted.rowHoverColor = kTransparent; + hosted.rowActiveColor = kTransparent; + hosted.checkboxColor = propertyGridPalette.valueBoxColor; + hosted.checkboxHoverColor = propertyGridPalette.valueBoxHoverColor; + hosted.checkboxReadOnlyColor = propertyGridPalette.valueBoxReadOnlyColor; + hosted.checkboxBorderColor = propertyGridPalette.valueBoxBorderColor; + hosted.checkboxMarkColor = propertyGridPalette.valueTextColor; + hosted.labelColor = propertyGridPalette.labelTextColor; + return hosted; +} + +Widgets::UIEditorNumberFieldMetrics BuildUIEditorHostedNumberFieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorNumberFieldMetrics& fallback) { + Widgets::UIEditorNumberFieldMetrics 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.valueTextInsetX = propertyGridMetrics.valueBoxInsetX; + hosted.valueTextInsetY = propertyGridMetrics.valueTextInsetY; + hosted.cornerRounding = propertyGridMetrics.cornerRounding; + hosted.valueBoxRounding = propertyGridMetrics.valueBoxRounding; + hosted.borderThickness = propertyGridMetrics.borderThickness; + hosted.focusedBorderThickness = propertyGridMetrics.focusedBorderThickness; + hosted.labelFontSize = propertyGridMetrics.labelFontSize; + hosted.valueFontSize = propertyGridMetrics.valueFontSize; + return hosted; +} + +Widgets::UIEditorNumberFieldPalette BuildUIEditorHostedNumberFieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorNumberFieldPalette& fallback) { + Widgets::UIEditorNumberFieldPalette hosted = fallback; + hosted.surfaceColor = kTransparent; + hosted.borderColor = kTransparent; + hosted.focusedBorderColor = kTransparent; + hosted.rowHoverColor = kTransparent; + hosted.rowActiveColor = kTransparent; + hosted.valueBoxColor = propertyGridPalette.valueBoxColor; + hosted.valueBoxHoverColor = propertyGridPalette.valueBoxHoverColor; + hosted.valueBoxEditingColor = propertyGridPalette.valueBoxEditingColor; + hosted.readOnlyColor = propertyGridPalette.valueBoxReadOnlyColor; + hosted.controlBorderColor = propertyGridPalette.valueBoxBorderColor; + hosted.controlFocusedBorderColor = propertyGridPalette.valueBoxEditingBorderColor; + hosted.labelColor = propertyGridPalette.labelTextColor; + hosted.valueColor = propertyGridPalette.valueTextColor; + hosted.readOnlyValueColor = propertyGridPalette.readOnlyValueTextColor; + return hosted; +} + +Widgets::UIEditorTextFieldMetrics BuildUIEditorHostedTextFieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorTextFieldMetrics& fallback) { + Widgets::UIEditorTextFieldMetrics 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.valueTextInsetX = propertyGridMetrics.valueBoxInsetX; + hosted.valueTextInsetY = propertyGridMetrics.valueTextInsetY; + hosted.cornerRounding = propertyGridMetrics.cornerRounding; + hosted.valueBoxRounding = propertyGridMetrics.valueBoxRounding; + hosted.borderThickness = propertyGridMetrics.borderThickness; + hosted.focusedBorderThickness = propertyGridMetrics.focusedBorderThickness; + hosted.labelFontSize = propertyGridMetrics.labelFontSize; + hosted.valueFontSize = propertyGridMetrics.valueFontSize; + return hosted; +} + +Widgets::UIEditorTextFieldPalette BuildUIEditorHostedTextFieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorTextFieldPalette& fallback) { + Widgets::UIEditorTextFieldPalette hosted = fallback; + hosted.surfaceColor = kTransparent; + hosted.borderColor = kTransparent; + hosted.focusedBorderColor = kTransparent; + hosted.rowHoverColor = kTransparent; + hosted.rowActiveColor = kTransparent; + hosted.valueBoxColor = propertyGridPalette.valueBoxColor; + hosted.valueBoxHoverColor = propertyGridPalette.valueBoxHoverColor; + hosted.valueBoxEditingColor = propertyGridPalette.valueBoxEditingColor; + hosted.readOnlyColor = propertyGridPalette.valueBoxReadOnlyColor; + hosted.controlBorderColor = propertyGridPalette.valueBoxBorderColor; + hosted.controlFocusedBorderColor = propertyGridPalette.valueBoxEditingBorderColor; + hosted.labelColor = propertyGridPalette.labelTextColor; + hosted.valueColor = propertyGridPalette.valueTextColor; + hosted.readOnlyValueColor = propertyGridPalette.readOnlyValueTextColor; + return hosted; +} + +Widgets::UIEditorVector2FieldMetrics BuildUIEditorHostedVector2FieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorVector2FieldMetrics& fallback) { + Widgets::UIEditorVector2FieldMetrics 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::UIEditorVector2FieldPalette BuildUIEditorHostedVector2FieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorVector2FieldPalette& fallback) { + Widgets::UIEditorVector2FieldPalette 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::UIEditorVector3FieldMetrics BuildUIEditorHostedVector3FieldMetrics( + const Widgets::UIEditorPropertyGridMetrics& propertyGridMetrics, + const Widgets::UIEditorVector3FieldMetrics& fallback) { + Widgets::UIEditorVector3FieldMetrics 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::UIEditorVector3FieldPalette BuildUIEditorHostedVector3FieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorVector3FieldPalette& fallback) { + Widgets::UIEditorVector3FieldPalette 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) { + Widgets::UIEditorEnumFieldMetrics 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.valueTextInsetX = propertyGridMetrics.valueBoxInsetX; + hosted.valueTextInsetY = propertyGridMetrics.valueTextInsetY; + hosted.cornerRounding = propertyGridMetrics.cornerRounding; + hosted.valueBoxRounding = propertyGridMetrics.valueBoxRounding; + hosted.borderThickness = propertyGridMetrics.borderThickness; + hosted.focusedBorderThickness = propertyGridMetrics.focusedBorderThickness; + hosted.labelFontSize = propertyGridMetrics.labelFontSize; + hosted.valueFontSize = propertyGridMetrics.valueFontSize; + hosted.dropdownArrowFontSize = propertyGridMetrics.valueFontSize; + return hosted; +} + +Widgets::UIEditorEnumFieldPalette BuildUIEditorHostedEnumFieldPalette( + const Widgets::UIEditorPropertyGridPalette& propertyGridPalette, + const Widgets::UIEditorEnumFieldPalette& fallback) { + Widgets::UIEditorEnumFieldPalette hosted = fallback; + hosted.surfaceColor = kTransparent; + hosted.borderColor = kTransparent; + hosted.focusedBorderColor = kTransparent; + hosted.rowHoverColor = kTransparent; + hosted.rowActiveColor = kTransparent; + hosted.valueBoxColor = propertyGridPalette.valueBoxColor; + hosted.valueBoxHoverColor = propertyGridPalette.valueBoxHoverColor; + hosted.readOnlyColor = propertyGridPalette.valueBoxReadOnlyColor; + hosted.controlBorderColor = propertyGridPalette.valueBoxBorderColor; + hosted.labelColor = propertyGridPalette.labelTextColor; + hosted.valueColor = propertyGridPalette.valueTextColor; + hosted.arrowColor = propertyGridPalette.valueTextColor; + return hosted; +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Core/UIEditorVector2FieldInteraction.cpp b/new_editor/src/Core/UIEditorVector2FieldInteraction.cpp new file mode 100644 index 00000000..7b86f717 --- /dev/null +++ b/new_editor/src/Core/UIEditorVector2FieldInteraction.cpp @@ -0,0 +1,563 @@ +#include + +#include + +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPointerButton; +using ::XCEngine::UI::Text::HandleKeyDown; +using ::XCEngine::UI::Text::InsertCharacter; +using ::XCEngine::UI::Editor::Widgets::BuildUIEditorVector2FieldLayout; +using ::XCEngine::UI::Editor::Widgets::FormatUIEditorVector2FieldComponentValue; +using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorVector2Field; +using ::XCEngine::UI::Editor::Widgets::IsUIEditorVector2FieldPointInside; +using ::XCEngine::UI::Editor::Widgets::NormalizeUIEditorVector2FieldComponentValue; +using ::XCEngine::UI::Editor::Widgets::TryParseUIEditorVector2FieldComponentValue; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTarget; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTargetKind; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldInvalidComponentIndex; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldLayout; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldMetrics; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec; + +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 UIEditorVector2FieldInteractionState& state) { + return state.vector2FieldState.selectedComponentIndex == + UIEditorVector2FieldInvalidComponentIndex + ? 0u + : state.vector2FieldState.selectedComponentIndex; +} + +std::string BuildComponentEditFieldId( + const UIEditorVector2FieldSpec& spec, + std::size_t componentIndex) { + return spec.fieldId + "." + std::to_string(componentIndex); +} + +bool IsPermittedCharacter( + const UIEditorVector2FieldSpec& spec, + std::uint32_t character) { + if (character >= static_cast('0') && + character <= static_cast('9')) { + return true; + } + + if (character == static_cast('-') || + character == static_cast('+')) { + return true; + } + + return !spec.integerMode && character == static_cast('.'); +} + +void SyncDisplayTexts( + UIEditorVector2FieldInteractionState& state, + const UIEditorVector2FieldSpec& spec) { + for (std::size_t componentIndex = 0u; + componentIndex < state.vector2FieldState.displayTexts.size(); + ++componentIndex) { + if (state.vector2FieldState.editing && + state.vector2FieldState.selectedComponentIndex == componentIndex) { + continue; + } + + state.vector2FieldState.displayTexts[componentIndex] = + FormatUIEditorVector2FieldComponentValue(spec, componentIndex); + } +} + +void SyncHoverTarget( + UIEditorVector2FieldInteractionState& state, + const UIEditorVector2FieldLayout& layout) { + if (!state.hasPointerPosition) { + state.vector2FieldState.hoveredTarget = UIEditorVector2FieldHitTargetKind::None; + state.vector2FieldState.hoveredComponentIndex = UIEditorVector2FieldInvalidComponentIndex; + return; + } + + const UIEditorVector2FieldHitTarget hitTarget = + HitTestUIEditorVector2Field(layout, state.pointerPosition); + state.vector2FieldState.hoveredTarget = hitTarget.kind; + state.vector2FieldState.hoveredComponentIndex = hitTarget.componentIndex; +} + +bool MoveSelection( + UIEditorVector2FieldInteractionState& state, + int direction, + UIEditorVector2FieldInteractionResult& result) { + const std::size_t before = ResolveFallbackSelectedComponentIndex(state); + const std::size_t after = + direction < 0 + ? (before == 0u ? 0u : before - 1u) + : (before >= 1u ? 1u : before + 1u); + state.vector2FieldState.selectedComponentIndex = after; + result.selectionChanged = before != after; + result.selectedComponentIndex = after; + result.consumed = true; + return true; +} + +bool SelectComponent( + UIEditorVector2FieldInteractionState& state, + std::size_t componentIndex, + UIEditorVector2FieldInteractionResult& result) { + if (componentIndex >= 2u) { + return false; + } + + const std::size_t before = ResolveFallbackSelectedComponentIndex(state); + state.vector2FieldState.selectedComponentIndex = componentIndex; + result.selectionChanged = before != componentIndex; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool BeginEdit( + UIEditorVector2FieldInteractionState& state, + const UIEditorVector2FieldSpec& spec, + std::size_t componentIndex, + bool clearText) { + if (spec.readOnly || componentIndex >= spec.values.size()) { + return false; + } + + const std::string baseline = + FormatUIEditorVector2FieldComponentValue(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.vector2FieldState.editing && + state.vector2FieldState.selectedComponentIndex == componentIndex) { + return false; + } + + state.vector2FieldState.selectedComponentIndex = componentIndex; + state.vector2FieldState.editing = true; + state.textInputState.value = clearText ? std::string() : baseline; + state.textInputState.caret = state.textInputState.value.size(); + state.editModel.UpdateStagedValue(state.textInputState.value); + state.vector2FieldState.displayTexts[componentIndex] = state.textInputState.value; + return true; +} + +bool CommitEdit( + UIEditorVector2FieldInteractionState& state, + UIEditorVector2FieldSpec& spec, + UIEditorVector2FieldInteractionResult& result) { + if (!state.vector2FieldState.editing || + !state.editModel.HasActiveEdit() || + state.vector2FieldState.selectedComponentIndex >= spec.values.size()) { + return false; + } + + const std::size_t componentIndex = state.vector2FieldState.selectedComponentIndex; + double parsedValue = spec.values[componentIndex]; + if (!TryParseUIEditorVector2FieldComponentValue( + spec, + state.textInputState.value, + parsedValue)) { + result.consumed = true; + result.editCommitRejected = true; + return false; + } + + result.valuesBefore = spec.values; + spec.values[componentIndex] = NormalizeUIEditorVector2FieldComponentValue(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 = + FormatUIEditorVector2FieldComponentValue(spec, componentIndex); + + state.editModel.CommitEdit(); + state.textInputState = {}; + state.vector2FieldState.editing = false; + state.vector2FieldState.displayTexts[componentIndex] = result.committedText; + return true; +} + +bool CancelEdit( + UIEditorVector2FieldInteractionState& state, + const UIEditorVector2FieldSpec& spec, + UIEditorVector2FieldInteractionResult& result) { + if (!state.vector2FieldState.editing || + !state.editModel.HasActiveEdit() || + state.vector2FieldState.selectedComponentIndex >= spec.values.size()) { + return false; + } + + const std::size_t componentIndex = state.vector2FieldState.selectedComponentIndex; + state.editModel.CancelEdit(); + state.textInputState = {}; + state.vector2FieldState.editing = false; + state.vector2FieldState.displayTexts[componentIndex] = + FormatUIEditorVector2FieldComponentValue(spec, componentIndex); + result.consumed = true; + result.editCanceled = true; + result.valuesBefore = spec.values; + result.valuesAfter = spec.values; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool ApplyStep( + UIEditorVector2FieldInteractionState& state, + UIEditorVector2FieldSpec& spec, + double direction, + bool snapToEdge, + UIEditorVector2FieldInteractionResult& result) { + if (spec.readOnly) { + return false; + } + + const std::size_t componentIndex = ResolveFallbackSelectedComponentIndex(state); + state.vector2FieldState.selectedComponentIndex = componentIndex; + + if (state.vector2FieldState.editing && + !CommitEdit(state, spec, result)) { + return result.editCommitRejected; + } + + result.valuesBefore = spec.values; + if (snapToEdge) { + spec.values[componentIndex] = + direction < 0.0 + ? NormalizeUIEditorVector2FieldComponentValue(spec, spec.minValue) + : NormalizeUIEditorVector2FieldComponentValue(spec, spec.maxValue); + } else { + const double step = spec.step == 0.0 ? 1.0 : spec.step; + spec.values[componentIndex] = NormalizeUIEditorVector2FieldComponentValue( + 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.vector2FieldState.displayTexts[componentIndex] = + FormatUIEditorVector2FieldComponentValue(spec, componentIndex); + return true; +} + +} // namespace + +UIEditorVector2FieldInteractionFrame UpdateUIEditorVector2FieldInteraction( + UIEditorVector2FieldInteractionState& state, + UIEditorVector2FieldSpec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector& inputEvents, + const UIEditorVector2FieldMetrics& metrics) { + UIEditorVector2FieldLayout layout = BuildUIEditorVector2FieldLayout(bounds, spec, metrics); + SyncDisplayTexts(state, spec); + SyncHoverTarget(state, layout); + + UIEditorVector2FieldInteractionResult interactionResult = {}; + interactionResult.selectedComponentIndex = state.vector2FieldState.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; + } + + UIEditorVector2FieldInteractionResult eventResult = {}; + eventResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; + switch (event.type) { + case UIInputEventType::FocusGained: + eventResult.focusChanged = !state.vector2FieldState.focused; + state.vector2FieldState.focused = true; + if (state.vector2FieldState.selectedComponentIndex == + UIEditorVector2FieldInvalidComponentIndex) { + state.vector2FieldState.selectedComponentIndex = 0u; + } + eventResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; + break; + + case UIInputEventType::FocusLost: + eventResult.focusChanged = state.vector2FieldState.focused; + state.vector2FieldState.focused = false; + state.vector2FieldState.activeTarget = UIEditorVector2FieldHitTargetKind::None; + state.vector2FieldState.activeComponentIndex = UIEditorVector2FieldInvalidComponentIndex; + state.hasPointerPosition = false; + if (state.vector2FieldState.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 UIEditorVector2FieldHitTarget hitTarget = + state.hasPointerPosition + ? HitTestUIEditorVector2Field(layout, state.pointerPosition) + : UIEditorVector2FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + const bool insideField = + state.hasPointerPosition && + IsUIEditorVector2FieldPointInside(layout.bounds, state.pointerPosition); + if (insideField) { + eventResult.focusChanged = !state.vector2FieldState.focused; + state.vector2FieldState.focused = true; + state.vector2FieldState.activeTarget = hitTarget.kind == UIEditorVector2FieldHitTargetKind::None + ? UIEditorVector2FieldHitTargetKind::Row + : hitTarget.kind; + state.vector2FieldState.activeComponentIndex = hitTarget.componentIndex; + if (hitTarget.kind == UIEditorVector2FieldHitTargetKind::Component) { + SelectComponent(state, hitTarget.componentIndex, eventResult); + } else if (state.vector2FieldState.selectedComponentIndex == + UIEditorVector2FieldInvalidComponentIndex) { + state.vector2FieldState.selectedComponentIndex = 0u; + eventResult.selectedComponentIndex = 0u; + } + eventResult.consumed = true; + } else { + if (state.vector2FieldState.editing) { + CommitEdit(state, spec, eventResult); + if (!eventResult.editCommitRejected) { + eventResult.focusChanged = state.vector2FieldState.focused; + state.vector2FieldState.focused = false; + } + } else if (state.vector2FieldState.focused) { + eventResult.focusChanged = true; + state.vector2FieldState.focused = false; + } + state.vector2FieldState.activeTarget = UIEditorVector2FieldHitTargetKind::None; + state.vector2FieldState.activeComponentIndex = UIEditorVector2FieldInvalidComponentIndex; + } + break; + } + + case UIInputEventType::PointerButtonUp: { + const UIEditorVector2FieldHitTarget hitTarget = + state.hasPointerPosition + ? HitTestUIEditorVector2Field(layout, state.pointerPosition) + : UIEditorVector2FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton == UIPointerButton::Left) { + const UIEditorVector2FieldHitTargetKind activeTarget = state.vector2FieldState.activeTarget; + const std::size_t activeComponentIndex = state.vector2FieldState.activeComponentIndex; + state.vector2FieldState.activeTarget = UIEditorVector2FieldHitTargetKind::None; + state.vector2FieldState.activeComponentIndex = UIEditorVector2FieldInvalidComponentIndex; + + if (activeTarget == UIEditorVector2FieldHitTargetKind::Component && + hitTarget.kind == UIEditorVector2FieldHitTargetKind::Component && + activeComponentIndex == hitTarget.componentIndex) { + SelectComponent(state, hitTarget.componentIndex, eventResult); + if (!state.vector2FieldState.editing) { + eventResult.editStarted = + BeginEdit(state, spec, hitTarget.componentIndex, false); + } + eventResult.consumed = true; + } else if (hitTarget.kind == UIEditorVector2FieldHitTargetKind::Row) { + eventResult.consumed = true; + } + } + break; + } + + case UIInputEventType::KeyDown: + if (!state.vector2FieldState.focused) { + break; + } + + if (state.vector2FieldState.editing) { + if (event.keyCode == static_cast(KeyCode::Escape)) { + CancelEdit(state, spec, eventResult); + break; + } + + if (event.keyCode == static_cast(KeyCode::Tab)) { + if (CommitEdit(state, spec, eventResult)) { + MoveSelection( + state, + event.modifiers.shift ? -1 : 1, + eventResult); + } + eventResult.consumed = true; + break; + } + + const auto textResult = + HandleKeyDown(state.textInputState, event.keyCode, event.modifiers); + if (textResult.handled) { + state.editModel.UpdateStagedValue(state.textInputState.value); + state.vector2FieldState.displayTexts[state.vector2FieldState.selectedComponentIndex] = + state.textInputState.value; + eventResult.consumed = true; + eventResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; + if (textResult.submitRequested) { + CommitEdit(state, spec, eventResult); + } + break; + } + } else { + switch (static_cast(event.keyCode)) { + case KeyCode::Left: + MoveSelection(state, -1, eventResult); + break; + + case KeyCode::Right: + case KeyCode::Tab: + MoveSelection( + state, + static_cast(event.keyCode) == KeyCode::Tab && event.modifiers.shift ? -1 : 1, + eventResult); + break; + + case KeyCode::Up: + ApplyStep(state, spec, 1.0, false, eventResult); + break; + + case KeyCode::Down: + ApplyStep(state, spec, -1.0, false, eventResult); + break; + + case KeyCode::Home: + ApplyStep(state, spec, -1.0, true, eventResult); + break; + + case KeyCode::End: + ApplyStep(state, spec, 1.0, true, eventResult); + break; + + case KeyCode::Enter: + eventResult.selectedComponentIndex = ResolveFallbackSelectedComponentIndex(state); + eventResult.editStarted = BeginEdit( + state, + spec, + eventResult.selectedComponentIndex, + false); + eventResult.consumed = eventResult.editStarted; + break; + + default: + break; + } + } + break; + + case UIInputEventType::Character: + if (!state.vector2FieldState.focused || + spec.readOnly || + event.modifiers.control || + event.modifiers.alt || + event.modifiers.super || + !IsPermittedCharacter(spec, event.character)) { + break; + } + + if (state.vector2FieldState.selectedComponentIndex == + UIEditorVector2FieldInvalidComponentIndex) { + state.vector2FieldState.selectedComponentIndex = 0u; + } + eventResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; + + if (!state.vector2FieldState.editing) { + eventResult.editStarted = BeginEdit( + state, + spec, + state.vector2FieldState.selectedComponentIndex, + true); + } + + if (InsertCharacter(state.textInputState, event.character)) { + state.editModel.UpdateStagedValue(state.textInputState.value); + state.vector2FieldState.displayTexts[state.vector2FieldState.selectedComponentIndex] = + state.textInputState.value; + eventResult.consumed = true; + } + break; + + default: + break; + } + + layout = BuildUIEditorVector2FieldLayout(bounds, spec, metrics); + SyncDisplayTexts(state, spec); + SyncHoverTarget(state, layout); + if (eventResult.hitTarget.kind == UIEditorVector2FieldHitTargetKind::None && + state.hasPointerPosition) { + eventResult.hitTarget = HitTestUIEditorVector2Field(layout, state.pointerPosition); + } + if (eventResult.selectedComponentIndex == UIEditorVector2FieldInvalidComponentIndex) { + eventResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; + } + + if (eventResult.consumed || + eventResult.focusChanged || + eventResult.valueChanged || + eventResult.stepApplied || + eventResult.selectionChanged || + eventResult.editStarted || + eventResult.editCommitted || + eventResult.editCommitRejected || + eventResult.editCanceled || + eventResult.hitTarget.kind != UIEditorVector2FieldHitTargetKind::None) { + interactionResult = std::move(eventResult); + } + } + + layout = BuildUIEditorVector2FieldLayout(bounds, spec, metrics); + SyncDisplayTexts(state, spec); + SyncHoverTarget(state, layout); + if (interactionResult.hitTarget.kind == UIEditorVector2FieldHitTargetKind::None && + state.hasPointerPosition) { + interactionResult.hitTarget = HitTestUIEditorVector2Field(layout, state.pointerPosition); + } + if (interactionResult.selectedComponentIndex == UIEditorVector2FieldInvalidComponentIndex) { + interactionResult.selectedComponentIndex = state.vector2FieldState.selectedComponentIndex; + } + + return { + std::move(layout), + std::move(interactionResult) + }; +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Core/UIEditorVector3FieldInteraction.cpp b/new_editor/src/Core/UIEditorVector3FieldInteraction.cpp new file mode 100644 index 00000000..9a3b2ad6 --- /dev/null +++ b/new_editor/src/Core/UIEditorVector3FieldInteraction.cpp @@ -0,0 +1,563 @@ +#include + +#include + +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPointerButton; +using ::XCEngine::UI::Text::HandleKeyDown; +using ::XCEngine::UI::Text::InsertCharacter; +using ::XCEngine::UI::Editor::Widgets::BuildUIEditorVector3FieldLayout; +using ::XCEngine::UI::Editor::Widgets::FormatUIEditorVector3FieldComponentValue; +using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorVector3Field; +using ::XCEngine::UI::Editor::Widgets::IsUIEditorVector3FieldPointInside; +using ::XCEngine::UI::Editor::Widgets::NormalizeUIEditorVector3FieldComponentValue; +using ::XCEngine::UI::Editor::Widgets::TryParseUIEditorVector3FieldComponentValue; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTarget; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTargetKind; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector3FieldInvalidComponentIndex; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector3FieldLayout; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector3FieldMetrics; +using ::XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec; + +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 UIEditorVector3FieldInteractionState& state) { + return state.vector3FieldState.selectedComponentIndex == + UIEditorVector3FieldInvalidComponentIndex + ? 0u + : state.vector3FieldState.selectedComponentIndex; +} + +std::string BuildComponentEditFieldId( + const UIEditorVector3FieldSpec& spec, + std::size_t componentIndex) { + return spec.fieldId + "." + std::to_string(componentIndex); +} + +bool IsPermittedCharacter( + const UIEditorVector3FieldSpec& spec, + std::uint32_t character) { + if (character >= static_cast('0') && + character <= static_cast('9')) { + return true; + } + + if (character == static_cast('-') || + character == static_cast('+')) { + return true; + } + + return !spec.integerMode && character == static_cast('.'); +} + +void SyncDisplayTexts( + UIEditorVector3FieldInteractionState& state, + const UIEditorVector3FieldSpec& spec) { + for (std::size_t componentIndex = 0u; + componentIndex < state.vector3FieldState.displayTexts.size(); + ++componentIndex) { + if (state.vector3FieldState.editing && + state.vector3FieldState.selectedComponentIndex == componentIndex) { + continue; + } + + state.vector3FieldState.displayTexts[componentIndex] = + FormatUIEditorVector3FieldComponentValue(spec, componentIndex); + } +} + +void SyncHoverTarget( + UIEditorVector3FieldInteractionState& state, + const UIEditorVector3FieldLayout& layout) { + if (!state.hasPointerPosition) { + state.vector3FieldState.hoveredTarget = UIEditorVector3FieldHitTargetKind::None; + state.vector3FieldState.hoveredComponentIndex = UIEditorVector3FieldInvalidComponentIndex; + return; + } + + const UIEditorVector3FieldHitTarget hitTarget = + HitTestUIEditorVector3Field(layout, state.pointerPosition); + state.vector3FieldState.hoveredTarget = hitTarget.kind; + state.vector3FieldState.hoveredComponentIndex = hitTarget.componentIndex; +} + +bool MoveSelection( + UIEditorVector3FieldInteractionState& state, + int direction, + UIEditorVector3FieldInteractionResult& result) { + const std::size_t before = ResolveFallbackSelectedComponentIndex(state); + const std::size_t after = + direction < 0 + ? (before == 0u ? 0u : before - 1u) + : (before >= 2u ? 2u : before + 1u); + state.vector3FieldState.selectedComponentIndex = after; + result.selectionChanged = before != after; + result.selectedComponentIndex = after; + result.consumed = true; + return true; +} + +bool SelectComponent( + UIEditorVector3FieldInteractionState& state, + std::size_t componentIndex, + UIEditorVector3FieldInteractionResult& result) { + if (componentIndex >= 3u) { + return false; + } + + const std::size_t before = ResolveFallbackSelectedComponentIndex(state); + state.vector3FieldState.selectedComponentIndex = componentIndex; + result.selectionChanged = before != componentIndex; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool BeginEdit( + UIEditorVector3FieldInteractionState& state, + const UIEditorVector3FieldSpec& spec, + std::size_t componentIndex, + bool clearText) { + if (spec.readOnly || componentIndex >= spec.values.size()) { + return false; + } + + const std::string baseline = + FormatUIEditorVector3FieldComponentValue(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.vector3FieldState.editing && + state.vector3FieldState.selectedComponentIndex == componentIndex) { + return false; + } + + state.vector3FieldState.selectedComponentIndex = componentIndex; + state.vector3FieldState.editing = true; + state.textInputState.value = clearText ? std::string() : baseline; + state.textInputState.caret = state.textInputState.value.size(); + state.editModel.UpdateStagedValue(state.textInputState.value); + state.vector3FieldState.displayTexts[componentIndex] = state.textInputState.value; + return true; +} + +bool CommitEdit( + UIEditorVector3FieldInteractionState& state, + UIEditorVector3FieldSpec& spec, + UIEditorVector3FieldInteractionResult& result) { + if (!state.vector3FieldState.editing || + !state.editModel.HasActiveEdit() || + state.vector3FieldState.selectedComponentIndex >= spec.values.size()) { + return false; + } + + const std::size_t componentIndex = state.vector3FieldState.selectedComponentIndex; + double parsedValue = spec.values[componentIndex]; + if (!TryParseUIEditorVector3FieldComponentValue( + spec, + state.textInputState.value, + parsedValue)) { + result.consumed = true; + result.editCommitRejected = true; + return false; + } + + result.valuesBefore = spec.values; + spec.values[componentIndex] = NormalizeUIEditorVector3FieldComponentValue(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 = + FormatUIEditorVector3FieldComponentValue(spec, componentIndex); + + state.editModel.CommitEdit(); + state.textInputState = {}; + state.vector3FieldState.editing = false; + state.vector3FieldState.displayTexts[componentIndex] = result.committedText; + return true; +} + +bool CancelEdit( + UIEditorVector3FieldInteractionState& state, + const UIEditorVector3FieldSpec& spec, + UIEditorVector3FieldInteractionResult& result) { + if (!state.vector3FieldState.editing || + !state.editModel.HasActiveEdit() || + state.vector3FieldState.selectedComponentIndex >= spec.values.size()) { + return false; + } + + const std::size_t componentIndex = state.vector3FieldState.selectedComponentIndex; + state.editModel.CancelEdit(); + state.textInputState = {}; + state.vector3FieldState.editing = false; + state.vector3FieldState.displayTexts[componentIndex] = + FormatUIEditorVector3FieldComponentValue(spec, componentIndex); + result.consumed = true; + result.editCanceled = true; + result.valuesBefore = spec.values; + result.valuesAfter = spec.values; + result.selectedComponentIndex = componentIndex; + return true; +} + +bool ApplyStep( + UIEditorVector3FieldInteractionState& state, + UIEditorVector3FieldSpec& spec, + double direction, + bool snapToEdge, + UIEditorVector3FieldInteractionResult& result) { + if (spec.readOnly) { + return false; + } + + const std::size_t componentIndex = ResolveFallbackSelectedComponentIndex(state); + state.vector3FieldState.selectedComponentIndex = componentIndex; + + if (state.vector3FieldState.editing && + !CommitEdit(state, spec, result)) { + return result.editCommitRejected; + } + + result.valuesBefore = spec.values; + if (snapToEdge) { + spec.values[componentIndex] = + direction < 0.0 + ? NormalizeUIEditorVector3FieldComponentValue(spec, spec.minValue) + : NormalizeUIEditorVector3FieldComponentValue(spec, spec.maxValue); + } else { + const double step = spec.step == 0.0 ? 1.0 : spec.step; + spec.values[componentIndex] = NormalizeUIEditorVector3FieldComponentValue( + 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.vector3FieldState.displayTexts[componentIndex] = + FormatUIEditorVector3FieldComponentValue(spec, componentIndex); + return true; +} + +} // namespace + +UIEditorVector3FieldInteractionFrame UpdateUIEditorVector3FieldInteraction( + UIEditorVector3FieldInteractionState& state, + UIEditorVector3FieldSpec& spec, + const ::XCEngine::UI::UIRect& bounds, + const std::vector& inputEvents, + const UIEditorVector3FieldMetrics& metrics) { + UIEditorVector3FieldLayout layout = BuildUIEditorVector3FieldLayout(bounds, spec, metrics); + SyncDisplayTexts(state, spec); + SyncHoverTarget(state, layout); + + UIEditorVector3FieldInteractionResult interactionResult = {}; + interactionResult.selectedComponentIndex = state.vector3FieldState.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; + } + + UIEditorVector3FieldInteractionResult eventResult = {}; + eventResult.selectedComponentIndex = state.vector3FieldState.selectedComponentIndex; + switch (event.type) { + case UIInputEventType::FocusGained: + eventResult.focusChanged = !state.vector3FieldState.focused; + state.vector3FieldState.focused = true; + if (state.vector3FieldState.selectedComponentIndex == + UIEditorVector3FieldInvalidComponentIndex) { + state.vector3FieldState.selectedComponentIndex = 0u; + } + eventResult.selectedComponentIndex = state.vector3FieldState.selectedComponentIndex; + break; + + case UIInputEventType::FocusLost: + eventResult.focusChanged = state.vector3FieldState.focused; + state.vector3FieldState.focused = false; + state.vector3FieldState.activeTarget = UIEditorVector3FieldHitTargetKind::None; + state.vector3FieldState.activeComponentIndex = UIEditorVector3FieldInvalidComponentIndex; + state.hasPointerPosition = false; + if (state.vector3FieldState.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 UIEditorVector3FieldHitTarget hitTarget = + state.hasPointerPosition + ? HitTestUIEditorVector3Field(layout, state.pointerPosition) + : UIEditorVector3FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton != UIPointerButton::Left) { + break; + } + + const bool insideField = + state.hasPointerPosition && + IsUIEditorVector3FieldPointInside(layout.bounds, state.pointerPosition); + if (insideField) { + eventResult.focusChanged = !state.vector3FieldState.focused; + state.vector3FieldState.focused = true; + state.vector3FieldState.activeTarget = hitTarget.kind == UIEditorVector3FieldHitTargetKind::None + ? UIEditorVector3FieldHitTargetKind::Row + : hitTarget.kind; + state.vector3FieldState.activeComponentIndex = hitTarget.componentIndex; + if (hitTarget.kind == UIEditorVector3FieldHitTargetKind::Component) { + SelectComponent(state, hitTarget.componentIndex, eventResult); + } else if (state.vector3FieldState.selectedComponentIndex == + UIEditorVector3FieldInvalidComponentIndex) { + state.vector3FieldState.selectedComponentIndex = 0u; + eventResult.selectedComponentIndex = 0u; + } + eventResult.consumed = true; + } else { + if (state.vector3FieldState.editing) { + CommitEdit(state, spec, eventResult); + if (!eventResult.editCommitRejected) { + eventResult.focusChanged = state.vector3FieldState.focused; + state.vector3FieldState.focused = false; + } + } else if (state.vector3FieldState.focused) { + eventResult.focusChanged = true; + state.vector3FieldState.focused = false; + } + state.vector3FieldState.activeTarget = UIEditorVector3FieldHitTargetKind::None; + state.vector3FieldState.activeComponentIndex = UIEditorVector3FieldInvalidComponentIndex; + } + break; + } + + case UIInputEventType::PointerButtonUp: { + const UIEditorVector3FieldHitTarget hitTarget = + state.hasPointerPosition + ? HitTestUIEditorVector3Field(layout, state.pointerPosition) + : UIEditorVector3FieldHitTarget {}; + eventResult.hitTarget = hitTarget; + + if (event.pointerButton == UIPointerButton::Left) { + const UIEditorVector3FieldHitTargetKind activeTarget = state.vector3FieldState.activeTarget; + const std::size_t activeComponentIndex = state.vector3FieldState.activeComponentIndex; + state.vector3FieldState.activeTarget = UIEditorVector3FieldHitTargetKind::None; + state.vector3FieldState.activeComponentIndex = UIEditorVector3FieldInvalidComponentIndex; + + if (activeTarget == UIEditorVector3FieldHitTargetKind::Component && + hitTarget.kind == UIEditorVector3FieldHitTargetKind::Component && + activeComponentIndex == hitTarget.componentIndex) { + SelectComponent(state, hitTarget.componentIndex, eventResult); + if (!state.vector3FieldState.editing) { + eventResult.editStarted = + BeginEdit(state, spec, hitTarget.componentIndex, false); + } + eventResult.consumed = true; + } else if (hitTarget.kind == UIEditorVector3FieldHitTargetKind::Row) { + eventResult.consumed = true; + } + } + break; + } + + case UIInputEventType::KeyDown: + if (!state.vector3FieldState.focused) { + break; + } + + if (state.vector3FieldState.editing) { + if (event.keyCode == static_cast(KeyCode::Escape)) { + CancelEdit(state, spec, eventResult); + break; + } + + if (event.keyCode == static_cast(KeyCode::Tab)) { + if (CommitEdit(state, spec, eventResult)) { + MoveSelection( + state, + event.modifiers.shift ? -1 : 1, + eventResult); + } + eventResult.consumed = true; + break; + } + + const auto textResult = + HandleKeyDown(state.textInputState, event.keyCode, event.modifiers); + if (textResult.handled) { + state.editModel.UpdateStagedValue(state.textInputState.value); + state.vector3FieldState.displayTexts[state.vector3FieldState.selectedComponentIndex] = + state.textInputState.value; + eventResult.consumed = true; + eventResult.selectedComponentIndex = state.vector3FieldState.selectedComponentIndex; + if (textResult.submitRequested) { + CommitEdit(state, spec, eventResult); + } + break; + } + } else { + switch (static_cast(event.keyCode)) { + case KeyCode::Left: + MoveSelection(state, -1, eventResult); + break; + + case KeyCode::Right: + case KeyCode::Tab: + MoveSelection( + state, + static_cast(event.keyCode) == KeyCode::Tab && event.modifiers.shift ? -1 : 1, + eventResult); + break; + + case KeyCode::Up: + ApplyStep(state, spec, 1.0, false, eventResult); + break; + + case KeyCode::Down: + ApplyStep(state, spec, -1.0, false, eventResult); + break; + + case KeyCode::Home: + ApplyStep(state, spec, -1.0, true, eventResult); + break; + + case KeyCode::End: + ApplyStep(state, spec, 1.0, true, eventResult); + break; + + case KeyCode::Enter: + eventResult.selectedComponentIndex = ResolveFallbackSelectedComponentIndex(state); + eventResult.editStarted = BeginEdit( + state, + spec, + eventResult.selectedComponentIndex, + false); + eventResult.consumed = eventResult.editStarted; + break; + + default: + break; + } + } + break; + + case UIInputEventType::Character: + if (!state.vector3FieldState.focused || + spec.readOnly || + event.modifiers.control || + event.modifiers.alt || + event.modifiers.super || + !IsPermittedCharacter(spec, event.character)) { + break; + } + + if (state.vector3FieldState.selectedComponentIndex == + UIEditorVector3FieldInvalidComponentIndex) { + state.vector3FieldState.selectedComponentIndex = 0u; + } + eventResult.selectedComponentIndex = state.vector3FieldState.selectedComponentIndex; + + if (!state.vector3FieldState.editing) { + eventResult.editStarted = BeginEdit( + state, + spec, + state.vector3FieldState.selectedComponentIndex, + true); + } + + if (InsertCharacter(state.textInputState, event.character)) { + state.editModel.UpdateStagedValue(state.textInputState.value); + state.vector3FieldState.displayTexts[state.vector3FieldState.selectedComponentIndex] = + state.textInputState.value; + eventResult.consumed = true; + } + break; + + default: + break; + } + + layout = BuildUIEditorVector3FieldLayout(bounds, spec, metrics); + SyncDisplayTexts(state, spec); + SyncHoverTarget(state, layout); + if (eventResult.hitTarget.kind == UIEditorVector3FieldHitTargetKind::None && + state.hasPointerPosition) { + eventResult.hitTarget = HitTestUIEditorVector3Field(layout, state.pointerPosition); + } + if (eventResult.selectedComponentIndex == UIEditorVector3FieldInvalidComponentIndex) { + eventResult.selectedComponentIndex = state.vector3FieldState.selectedComponentIndex; + } + + if (eventResult.consumed || + eventResult.focusChanged || + eventResult.valueChanged || + eventResult.stepApplied || + eventResult.selectionChanged || + eventResult.editStarted || + eventResult.editCommitted || + eventResult.editCommitRejected || + eventResult.editCanceled || + eventResult.hitTarget.kind != UIEditorVector3FieldHitTargetKind::None) { + interactionResult = std::move(eventResult); + } + } + + layout = BuildUIEditorVector3FieldLayout(bounds, spec, metrics); + SyncDisplayTexts(state, spec); + SyncHoverTarget(state, layout); + if (interactionResult.hitTarget.kind == UIEditorVector3FieldHitTargetKind::None && + state.hasPointerPosition) { + interactionResult.hitTarget = HitTestUIEditorVector3Field(layout, state.pointerPosition); + } + if (interactionResult.selectedComponentIndex == UIEditorVector3FieldInvalidComponentIndex) { + interactionResult.selectedComponentIndex = state.vector3FieldState.selectedComponentIndex; + } + + return { + std::move(layout), + std::move(interactionResult) + }; +} + +} // namespace XCEngine::UI::Editor diff --git a/new_editor/src/Widgets/UIEditorBoolField.cpp b/new_editor/src/Widgets/UIEditorBoolField.cpp index 79887e3a..3b97849c 100644 --- a/new_editor/src/Widgets/UIEditorBoolField.cpp +++ b/new_editor/src/Widgets/UIEditorBoolField.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include @@ -6,6 +8,10 @@ namespace XCEngine::UI::Editor::Widgets { namespace { +float ClampNonNegative(float value) { + return (std::max)(0.0f, value); +} + bool ContainsPoint(const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && @@ -17,37 +23,30 @@ bool ContainsPoint(const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIP UIEditorBoolFieldLayout BuildUIEditorBoolFieldLayout( const ::XCEngine::UI::UIRect& bounds, - const UIEditorBoolFieldSpec& spec, + const UIEditorBoolFieldSpec&, const UIEditorBoolFieldMetrics& metrics) { + const UIEditorFieldRowLayout hostLayout = BuildUIEditorFieldRowLayout( + bounds, + metrics.checkboxSize, + UIEditorFieldRowLayoutMetrics { + metrics.rowHeight, + metrics.horizontalPadding, + metrics.labelControlGap, + metrics.controlColumnStart, + metrics.controlTrailingInset, + 0.0f, + }); + UIEditorBoolFieldLayout layout = {}; - layout.bounds = bounds; - - const float controlX = (std::min)( - bounds.x + bounds.width - metrics.horizontalPadding - metrics.toggleWidth, - bounds.x + metrics.controlColumnStart); - const float labelWidth = (std::max)( - 0.0f, - controlX - bounds.x - metrics.horizontalPadding - metrics.labelControlGap); - layout.labelRect = ::XCEngine::UI::UIRect( - bounds.x + metrics.horizontalPadding, - bounds.y, - labelWidth, - bounds.height); - layout.toggleRect = ::XCEngine::UI::UIRect( - controlX, - bounds.y + (std::max)(0.0f, (bounds.height - metrics.toggleHeight) * 0.5f), - metrics.toggleWidth, - metrics.toggleHeight); - - const float knobSize = (std::max)(0.0f, layout.toggleRect.height - metrics.toggleKnobInset * 2.0f); - const float knobX = spec.value - ? layout.toggleRect.x + layout.toggleRect.width - metrics.toggleKnobInset - knobSize - : layout.toggleRect.x + metrics.toggleKnobInset; - layout.knobRect = ::XCEngine::UI::UIRect( - knobX, - layout.toggleRect.y + metrics.toggleKnobInset, - knobSize, - knobSize); + layout.bounds = hostLayout.bounds; + layout.labelRect = hostLayout.labelRect; + layout.controlRect = hostLayout.controlRect; + layout.checkboxRect = ::XCEngine::UI::UIRect( + layout.controlRect.x, + layout.bounds.y + ClampNonNegative((layout.bounds.height - metrics.checkboxSize) * 0.5f), + metrics.checkboxSize, + metrics.checkboxSize); + layout.checkmarkRect = layout.checkboxRect; return layout; } @@ -58,8 +57,8 @@ UIEditorBoolFieldHitTarget HitTestUIEditorBoolField( return {}; } - if (ContainsPoint(layout.toggleRect, point)) { - return { UIEditorBoolFieldHitTargetKind::Toggle }; + if (ContainsPoint(layout.controlRect, point)) { + return { UIEditorBoolFieldHitTargetKind::Checkbox }; } return { UIEditorBoolFieldHitTargetKind::Row }; @@ -72,27 +71,17 @@ void AppendUIEditorBoolFieldBackground( const UIEditorBoolFieldState& state, const UIEditorBoolFieldPalette& palette, const UIEditorBoolFieldMetrics& metrics) { - const bool hovered = state.hoveredTarget != UIEditorBoolFieldHitTargetKind::None; - const ::XCEngine::UI::UIColor rowColor = - state.active ? palette.rowActiveColor : - (hovered ? palette.rowHoverColor : palette.surfaceColor); - drawList.AddFilledRect(layout.bounds, rowColor, metrics.cornerRounding); + const bool checkboxHovered = state.hoveredTarget == UIEditorBoolFieldHitTargetKind::Checkbox; + const ::XCEngine::UI::UIColor checkboxColor = + spec.readOnly + ? palette.checkboxReadOnlyColor + : (checkboxHovered ? palette.checkboxHoverColor : palette.checkboxColor); + drawList.AddFilledRect(layout.checkboxRect, checkboxColor, metrics.checkboxRounding); drawList.AddRectOutline( - layout.bounds, - state.focused ? palette.focusedBorderColor : palette.borderColor, - state.focused ? metrics.focusedBorderThickness : metrics.borderThickness, - metrics.cornerRounding); - - const ::XCEngine::UI::UIColor toggleColor = - spec.readOnly ? palette.toggleReadOnlyColor : - (spec.value ? palette.toggleOnColor : palette.toggleOffColor); - drawList.AddFilledRect(layout.toggleRect, toggleColor, layout.toggleRect.height * 0.5f); - drawList.AddRectOutline( - layout.toggleRect, - palette.toggleBorderColor, + layout.checkboxRect, + palette.checkboxBorderColor, metrics.borderThickness, - layout.toggleRect.height * 0.5f); - drawList.AddFilledRect(layout.knobRect, palette.knobColor, layout.knobRect.height * 0.5f); + metrics.checkboxRounding); } void AppendUIEditorBoolFieldForeground( @@ -101,21 +90,28 @@ void AppendUIEditorBoolFieldForeground( const UIEditorBoolFieldSpec& spec, const UIEditorBoolFieldPalette& palette, const UIEditorBoolFieldMetrics& metrics) { - drawList.PushClipRect(layout.labelRect); - drawList.AddText( - ::XCEngine::UI::UIPoint(layout.labelRect.x, layout.labelRect.y + metrics.textInsetY), - spec.label, - palette.labelColor, - 12.0f); - drawList.PopClipRect(); - + drawList.PushClipRect(ResolveUIEditorTextClipRect(layout.labelRect, metrics.labelFontSize)); drawList.AddText( ::XCEngine::UI::UIPoint( - layout.toggleRect.x - 42.0f, - layout.bounds.y + metrics.textInsetY), - spec.value ? "On" : "Off", - palette.valueColor, - 12.0f); + layout.labelRect.x, + ResolveUIEditorTextTop(layout.labelRect, metrics.labelFontSize, metrics.labelTextInsetY)), + spec.label, + palette.labelColor, + metrics.labelFontSize); + drawList.PopClipRect(); + + if (spec.value) { + drawList.AddText( + ::XCEngine::UI::UIPoint( + layout.checkmarkRect.x + metrics.checkboxGlyphInsetX, + ResolveUIEditorTextTop( + layout.checkmarkRect, + metrics.checkboxGlyphFontSize, + metrics.checkboxGlyphInsetY)), + "V", + palette.checkboxMarkColor, + metrics.checkboxGlyphFontSize); + } } void AppendUIEditorBoolField( diff --git a/new_editor/src/Widgets/UIEditorEnumField.cpp b/new_editor/src/Widgets/UIEditorEnumField.cpp index df59f4bc..308bbf31 100644 --- a/new_editor/src/Widgets/UIEditorEnumField.cpp +++ b/new_editor/src/Widgets/UIEditorEnumField.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include @@ -6,6 +8,10 @@ namespace XCEngine::UI::Editor::Widgets { namespace { +float ClampNonNegative(float value) { + return (std::max)(0.0f, value); +} + bool ContainsPoint(const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && @@ -33,45 +39,33 @@ std::string ResolveUIEditorEnumFieldValueText(const UIEditorEnumFieldSpec& spec) UIEditorEnumFieldLayout BuildUIEditorEnumFieldLayout( const ::XCEngine::UI::UIRect& bounds, - const UIEditorEnumFieldSpec& spec, + const UIEditorEnumFieldSpec&, const UIEditorEnumFieldMetrics& metrics) { + const UIEditorFieldRowLayout hostLayout = BuildUIEditorFieldRowLayout( + bounds, + metrics.valueBoxMinWidth, + UIEditorFieldRowLayoutMetrics { + metrics.rowHeight, + metrics.horizontalPadding, + metrics.labelControlGap, + metrics.controlColumnStart, + metrics.controlTrailingInset, + metrics.controlInsetY, + }); + UIEditorEnumFieldLayout layout = {}; - layout.bounds = bounds; - layout.bounds.height = bounds.height > 0.0f ? bounds.height : metrics.rowHeight; - - const float controlWidth = - metrics.buttonWidth * 2.0f + metrics.controlGap * 2.0f + metrics.valueBoxMinWidth; - const float controlX = (std::min)( - layout.bounds.x + layout.bounds.width - metrics.horizontalPadding - controlWidth, - bounds.x + metrics.controlColumnStart); - const float labelWidth = (std::max)( - 0.0f, - controlX - layout.bounds.x - metrics.horizontalPadding - metrics.labelControlGap); - layout.labelRect = ::XCEngine::UI::UIRect( - layout.bounds.x + metrics.horizontalPadding, - layout.bounds.y, - labelWidth, - layout.bounds.height); - - const float controlY = - layout.bounds.y + (std::max)(0.0f, (layout.bounds.height - metrics.valueBoxHeight) * 0.5f); - layout.controlRect = ::XCEngine::UI::UIRect( - controlX, - controlY, - controlWidth, - metrics.valueBoxHeight); - layout.previousRect = ::XCEngine::UI::UIRect(controlX, controlY, metrics.buttonWidth, metrics.valueBoxHeight); - const float valueX = layout.previousRect.x + layout.previousRect.width + metrics.controlGap; - layout.nextRect = ::XCEngine::UI::UIRect( - controlX + controlWidth - metrics.buttonWidth, - controlY, - metrics.buttonWidth, - metrics.valueBoxHeight); - layout.valueRect = ::XCEngine::UI::UIRect( - valueX, - controlY, - (std::max)(0.0f, layout.nextRect.x - valueX - metrics.controlGap), - metrics.valueBoxHeight); + layout.bounds = hostLayout.bounds; + layout.labelRect = hostLayout.labelRect; + layout.controlRect = hostLayout.controlRect; + layout.valueRect = layout.controlRect; + const float arrowWidth = (std::min)( + (std::max)(metrics.dropdownArrowWidth, layout.valueRect.height), + layout.valueRect.width); + layout.arrowRect = ::XCEngine::UI::UIRect( + layout.valueRect.x + (std::max)(0.0f, layout.valueRect.width - arrowWidth), + layout.valueRect.y, + arrowWidth, + layout.valueRect.height); return layout; } @@ -81,11 +75,8 @@ UIEditorEnumFieldHitTarget HitTestUIEditorEnumField( if (!ContainsPoint(layout.bounds, point)) { return {}; } - if (ContainsPoint(layout.previousRect, point)) { - return { UIEditorEnumFieldHitTargetKind::PreviousButton }; - } - if (ContainsPoint(layout.nextRect, point)) { - return { UIEditorEnumFieldHitTargetKind::NextButton }; + if (ContainsPoint(layout.arrowRect, point)) { + return { UIEditorEnumFieldHitTargetKind::DropdownArrow }; } if (ContainsPoint(layout.valueRect, point)) { return { UIEditorEnumFieldHitTargetKind::ValueBox }; @@ -100,33 +91,19 @@ void AppendUIEditorEnumFieldBackground( const UIEditorEnumFieldState& state, const UIEditorEnumFieldPalette& palette, const UIEditorEnumFieldMetrics& metrics) { - const bool hovered = state.hoveredTarget != UIEditorEnumFieldHitTargetKind::None; - drawList.AddFilledRect( - layout.bounds, - state.active ? palette.rowActiveColor : (hovered ? palette.rowHoverColor : palette.surfaceColor), - metrics.cornerRounding); + const bool controlHovered = + state.hoveredTarget == UIEditorEnumFieldHitTargetKind::ValueBox || + state.hoveredTarget == UIEditorEnumFieldHitTargetKind::DropdownArrow; + const ::XCEngine::UI::UIColor controlColor = + spec.readOnly + ? palette.readOnlyColor + : (controlHovered || state.popupOpen ? palette.valueBoxHoverColor : palette.valueBoxColor); + drawList.AddFilledRect(layout.valueRect, controlColor, metrics.valueBoxRounding); drawList.AddRectOutline( - layout.bounds, - state.focused ? palette.focusedBorderColor : palette.borderColor, - state.focused ? metrics.focusedBorderThickness : metrics.borderThickness, - metrics.cornerRounding); - - const ::XCEngine::UI::UIColor controlColor = spec.readOnly ? palette.readOnlyColor : palette.valueBoxColor; - drawList.AddFilledRect(layout.valueRect, controlColor, metrics.cornerRounding); - drawList.AddRectOutline(layout.valueRect, palette.controlBorderColor, metrics.borderThickness, metrics.cornerRounding); - - const ::XCEngine::UI::UIColor prevColor = - state.hoveredTarget == UIEditorEnumFieldHitTargetKind::PreviousButton - ? palette.buttonHoverColor - : palette.buttonColor; - const ::XCEngine::UI::UIColor nextColor = - state.hoveredTarget == UIEditorEnumFieldHitTargetKind::NextButton - ? palette.buttonHoverColor - : palette.buttonColor; - drawList.AddFilledRect(layout.previousRect, spec.readOnly ? palette.readOnlyColor : prevColor, metrics.cornerRounding); - drawList.AddFilledRect(layout.nextRect, spec.readOnly ? palette.readOnlyColor : nextColor, metrics.cornerRounding); - drawList.AddRectOutline(layout.previousRect, palette.controlBorderColor, metrics.borderThickness, metrics.cornerRounding); - drawList.AddRectOutline(layout.nextRect, palette.controlBorderColor, metrics.borderThickness, metrics.cornerRounding); + layout.valueRect, + state.popupOpen ? palette.focusedBorderColor : palette.controlBorderColor, + state.popupOpen ? metrics.focusedBorderThickness : metrics.borderThickness, + metrics.valueBoxRounding); } void AppendUIEditorEnumFieldForeground( @@ -135,38 +112,43 @@ void AppendUIEditorEnumFieldForeground( const UIEditorEnumFieldSpec& spec, const UIEditorEnumFieldPalette& palette, const UIEditorEnumFieldMetrics& metrics) { - drawList.PushClipRect(layout.labelRect); + drawList.PushClipRect(ResolveUIEditorTextClipRect(layout.labelRect, metrics.labelFontSize)); drawList.AddText( - ::XCEngine::UI::UIPoint(layout.labelRect.x, layout.labelRect.y + metrics.labelTextInsetY), + ::XCEngine::UI::UIPoint( + layout.labelRect.x, + ResolveUIEditorTextTop(layout.labelRect, metrics.labelFontSize, metrics.labelTextInsetY)), spec.label, palette.labelColor, - 12.0f); + metrics.labelFontSize); drawList.PopClipRect(); - drawList.PushClipRect(layout.valueRect); + const ::XCEngine::UI::UIRect valueTextClipRect( + layout.valueRect.x + metrics.valueTextInsetX, + layout.valueRect.y, + ClampNonNegative(layout.arrowRect.x - layout.valueRect.x - metrics.valueTextInsetX), + layout.valueRect.height); + drawList.PushClipRect(ResolveUIEditorTextClipRect(valueTextClipRect, metrics.valueFontSize)); drawList.AddText( ::XCEngine::UI::UIPoint( layout.valueRect.x + metrics.valueTextInsetX, - layout.valueRect.y + metrics.valueTextInsetY), + ResolveUIEditorTextTop(layout.valueRect, metrics.valueFontSize, metrics.valueTextInsetY)), ResolveUIEditorEnumFieldValueText(spec), palette.valueColor, - 12.0f); + metrics.valueFontSize); drawList.PopClipRect(); drawList.AddText( ::XCEngine::UI::UIPoint( - layout.previousRect.x + metrics.buttonTextInsetX, - layout.previousRect.y + metrics.buttonTextInsetY), - "<", - palette.valueColor, - 12.0f); - drawList.AddText( - ::XCEngine::UI::UIPoint( - layout.nextRect.x + metrics.buttonTextInsetX, - layout.nextRect.y + metrics.buttonTextInsetY), - ">", - palette.valueColor, - 12.0f); + layout.arrowRect.x + + ClampNonNegative((layout.arrowRect.width - metrics.dropdownArrowFontSize) * 0.5f) + + metrics.dropdownArrowInsetX, + ResolveUIEditorTextTop( + layout.arrowRect, + metrics.dropdownArrowFontSize, + metrics.dropdownArrowInsetY)), + "V", + palette.arrowColor, + metrics.dropdownArrowFontSize); } void AppendUIEditorEnumField( diff --git a/new_editor/src/Widgets/UIEditorFieldRowLayout.cpp b/new_editor/src/Widgets/UIEditorFieldRowLayout.cpp new file mode 100644 index 00000000..342d999a --- /dev/null +++ b/new_editor/src/Widgets/UIEditorFieldRowLayout.cpp @@ -0,0 +1,51 @@ +#include + +#include + +namespace XCEngine::UI::Editor::Widgets { + +namespace { + +float ClampNonNegative(float value) { + return (std::max)(0.0f, value); +} + +} // namespace + +UIEditorFieldRowLayout BuildUIEditorFieldRowLayout( + const ::XCEngine::UI::UIRect& bounds, + float minimumControlWidth, + const UIEditorFieldRowLayoutMetrics& metrics) { + const float rowHeight = bounds.height > 0.0f ? bounds.height : metrics.rowHeight; + const ::XCEngine::UI::UIRect rowBounds(bounds.x, bounds.y, bounds.width, rowHeight); + + const float resolvedMinimumControlWidth = + ClampNonNegative((std::min)(minimumControlWidth, rowBounds.width)); + const float preferredControlX = rowBounds.x + metrics.controlColumnStart; + const float maximumControlX = + rowBounds.x + rowBounds.width - metrics.controlTrailingInset - resolvedMinimumControlWidth; + const float controlX = + (std::clamp)( + (std::min)(preferredControlX, maximumControlX), + rowBounds.x, + rowBounds.x + rowBounds.width - metrics.controlTrailingInset); + const float controlInsetY = (std::min)(metrics.controlInsetY, rowBounds.height * 0.25f); + const float controlWidth = + ClampNonNegative(rowBounds.x + rowBounds.width - metrics.controlTrailingInset - controlX); + + UIEditorFieldRowLayout layout = {}; + layout.bounds = rowBounds; + layout.labelRect = ::XCEngine::UI::UIRect( + rowBounds.x + metrics.horizontalPadding, + rowBounds.y, + ClampNonNegative(controlX - metrics.labelControlGap - rowBounds.x - metrics.horizontalPadding), + rowBounds.height); + layout.controlRect = ::XCEngine::UI::UIRect( + controlX, + rowBounds.y + controlInsetY, + controlWidth, + ClampNonNegative(rowBounds.height - controlInsetY * 2.0f)); + return layout; +} + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Widgets/UIEditorMenuPopup.cpp b/new_editor/src/Widgets/UIEditorMenuPopup.cpp index 76646169..bd61751e 100644 --- a/new_editor/src/Widgets/UIEditorMenuPopup.cpp +++ b/new_editor/src/Widgets/UIEditorMenuPopup.cpp @@ -12,9 +12,6 @@ using ::XCEngine::UI::UIPoint; using ::XCEngine::UI::UIRect; using ::XCEngine::UI::Editor::UIEditorMenuItemKind; -constexpr float kPopupFontSize = 13.0f; -constexpr float kGlyphFontSize = 12.0f; - float ClampNonNegative(float value) { return (std::max)(value, 0.0f); } @@ -39,11 +36,11 @@ bool IsInteractiveItem(const UIEditorMenuPopupItem& item) { } float ResolveRowTextTop(const UIRect& rect, const UIEditorMenuPopupMetrics& metrics) { - return rect.y + (std::max)(0.0f, (rect.height - kPopupFontSize) * 0.5f) + metrics.labelInsetY; + return rect.y + (std::max)(0.0f, (rect.height - metrics.labelFontSize) * 0.5f) + metrics.labelInsetY; } -float ResolveGlyphTop(const UIRect& rect) { - return rect.y + (std::max)(0.0f, (rect.height - kGlyphFontSize) * 0.5f) - 0.5f; +float ResolveGlyphTop(const UIRect& rect, const UIEditorMenuPopupMetrics& metrics) { + return rect.y + (std::max)(0.0f, (rect.height - metrics.glyphFontSize) * 0.5f) - 0.5f; } bool IsHighlighted(const UIEditorMenuPopupState& state, std::size_t index) { @@ -209,10 +206,10 @@ void AppendUIEditorMenuPopupForeground( const float checkLeft = rect.x + 6.0f; if (item.checked) { drawList.AddText( - UIPoint(checkLeft, ResolveGlyphTop(rect)), + UIPoint(checkLeft, ResolveGlyphTop(rect, metrics)), "*", palette.glyphColor, - kGlyphFontSize); + metrics.glyphFontSize); } const float labelLeft = @@ -241,7 +238,7 @@ void AppendUIEditorMenuPopupForeground( UIPoint(labelLeft, ResolveRowTextTop(rect, metrics)), item.label, mainColor, - kPopupFontSize); + metrics.labelFontSize); drawList.PopClipRect(); if (!item.shortcutText.empty()) { @@ -258,17 +255,17 @@ void AppendUIEditorMenuPopupForeground( UIPoint(shortcutLeft, ResolveRowTextTop(rect, metrics)), item.shortcutText, secondaryColor, - kPopupFontSize); + metrics.labelFontSize); } if (item.hasSubmenu) { drawList.AddText( UIPoint( rect.x + rect.width - ClampNonNegative(metrics.shortcutInsetRight), - ResolveGlyphTop(rect)), + ResolveGlyphTop(rect, metrics)), ">", palette.glyphColor, - kGlyphFontSize); + metrics.glyphFontSize); } } } diff --git a/new_editor/src/Widgets/UIEditorNumberField.cpp b/new_editor/src/Widgets/UIEditorNumberField.cpp index 5047da17..fa748521 100644 --- a/new_editor/src/Widgets/UIEditorNumberField.cpp +++ b/new_editor/src/Widgets/UIEditorNumberField.cpp @@ -1,9 +1,12 @@ #include +#include +#include #include #include #include #include +#include namespace XCEngine::UI::Editor::Widgets { @@ -18,13 +21,7 @@ float ClampNonNegative(float value) { } double NormalizeRangeValue(const UIEditorNumberFieldSpec& spec, double value) { - const double minValue = (std::min)(spec.minValue, spec.maxValue); - const double maxValue = (std::max)(spec.minValue, spec.maxValue); - value = (std::clamp)(value, minValue, maxValue); - if (spec.integerMode) { - value = static_cast(std::llround(value)); - } - return value; + return NormalizeUIEditorNumberFieldValue(spec, value); } std::string FormatNumberValue(double value, bool integerMode) { @@ -73,21 +70,39 @@ std::string FormatNumberValue(double value, bool integerMode) { return palette.valueBoxColor; } -::XCEngine::UI::UIColor ResolveButtonFillColor( +void AppendLabelText( + UIDrawList& drawList, + const UIEditorNumberFieldLayout& layout, + std::string_view text, + const UIEditorNumberFieldPalette& palette, + const UIEditorNumberFieldMetrics& metrics) { + drawList.PushClipRect(ResolveUIEditorTextClipRect(layout.labelRect, metrics.labelFontSize)); + drawList.AddText( + UIPoint( + layout.labelRect.x, + ResolveUIEditorTextTop(layout.labelRect, metrics.labelFontSize, metrics.labelTextInsetY)), + std::string(text), + palette.labelColor, + metrics.labelFontSize); + drawList.PopClipRect(); +} + +void AppendValueText( + UIDrawList& drawList, + const UIEditorNumberFieldLayout& layout, + std::string_view text, const UIEditorNumberFieldSpec& spec, - const UIEditorNumberFieldState& state, - UIEditorNumberFieldHitTargetKind targetKind, - const UIEditorNumberFieldPalette& palette) { - if (spec.readOnly) { - return palette.readOnlyColor; - } - if (state.activeTarget == targetKind) { - return palette.buttonActiveColor; - } - if (state.hoveredTarget == targetKind) { - return palette.buttonHoverColor; - } - return palette.buttonColor; + const UIEditorNumberFieldPalette& palette, + const UIEditorNumberFieldMetrics& metrics) { + drawList.PushClipRect(ResolveUIEditorTextClipRect(layout.valueRect, metrics.valueFontSize)); + drawList.AddText( + UIPoint( + layout.valueRect.x + metrics.valueTextInsetX, + ResolveUIEditorTextTop(layout.valueRect, metrics.valueFontSize, metrics.valueTextInsetY)), + std::string(text), + spec.readOnly ? palette.readOnlyValueColor : palette.valueColor, + metrics.valueFontSize); + drawList.PopClipRect(); } } // namespace @@ -101,6 +116,40 @@ bool IsUIEditorNumberFieldPointInside( point.y <= rect.y + rect.height; } +double NormalizeUIEditorNumberFieldValue( + const UIEditorNumberFieldSpec& spec, + double value) { + const double minValue = (std::min)(spec.minValue, spec.maxValue); + const double maxValue = (std::max)(spec.minValue, spec.maxValue); + value = (std::clamp)(value, minValue, maxValue); + if (spec.integerMode) { + value = static_cast(std::llround(value)); + } + return value; +} + +bool TryParseUIEditorNumberFieldValue( + const UIEditorNumberFieldSpec& spec, + std::string_view text, + double& outValue) { + if (text.empty()) { + return false; + } + + try { + std::size_t consumed = 0u; + const double parsed = std::stod(std::string(text), &consumed); + if (consumed != text.size()) { + return false; + } + + outValue = NormalizeUIEditorNumberFieldValue(spec, parsed); + return true; + } catch (...) { + return false; + } +} + std::string FormatUIEditorNumberFieldValue(const UIEditorNumberFieldSpec& spec) { return FormatNumberValue(NormalizeRangeValue(spec, spec.value), spec.integerMode); } @@ -109,62 +158,29 @@ UIEditorNumberFieldLayout BuildUIEditorNumberFieldLayout( const UIRect& bounds, const UIEditorNumberFieldSpec&, const UIEditorNumberFieldMetrics& metrics) { - const float rowHeight = bounds.height > 0.0f ? bounds.height : metrics.rowHeight; - const UIRect rowBounds(bounds.x, bounds.y, bounds.width, rowHeight); - - const float controlStartX = - rowBounds.x + (std::min)(metrics.controlColumnStart, rowBounds.width * 0.6f); - const float controlInsetY = (std::min)(metrics.controlInsetY, rowBounds.height * 0.25f); - const float controlHeight = ClampNonNegative(rowBounds.height - controlInsetY * 2.0f); - const float controlWidth = ClampNonNegative( - rowBounds.width - (controlStartX - rowBounds.x) - metrics.horizontalPadding); - const float maxButtonWidth = (std::max)(0.0f, (controlWidth - metrics.buttonGap * 2.0f) * 0.2f); - const float buttonWidth = ClampNonNegative((std::min)(metrics.buttonWidth, maxButtonWidth)); - const float remainingValueWidth = - ClampNonNegative(controlWidth - buttonWidth * 2.0f - metrics.buttonGap * 2.0f); - const float valueWidth = (std::min)( - (std::max)(metrics.valueBoxMinWidth, remainingValueWidth), - remainingValueWidth); - const float controlActualWidth = - buttonWidth * 2.0f + metrics.buttonGap * 2.0f + valueWidth; - const float controlX = - rowBounds.x + rowBounds.width - metrics.horizontalPadding - controlActualWidth; + const UIEditorFieldRowLayout hostLayout = BuildUIEditorFieldRowLayout( + bounds, + metrics.valueBoxMinWidth, + UIEditorFieldRowLayoutMetrics { + metrics.rowHeight, + metrics.horizontalPadding, + metrics.labelControlGap, + metrics.controlColumnStart, + metrics.controlTrailingInset, + metrics.controlInsetY, + }); UIEditorNumberFieldLayout layout = {}; - layout.bounds = rowBounds; - layout.labelRect = UIRect( - rowBounds.x + metrics.horizontalPadding, - rowBounds.y, - ClampNonNegative(controlX - metrics.labelControlGap - rowBounds.x - metrics.horizontalPadding), - rowBounds.height); - layout.controlRect = UIRect(controlX, rowBounds.y + controlInsetY, controlActualWidth, controlHeight); - layout.decrementRect = UIRect( - layout.controlRect.x, - layout.controlRect.y, - buttonWidth, - layout.controlRect.height); - layout.valueRect = UIRect( - layout.decrementRect.x + layout.decrementRect.width + metrics.buttonGap, - layout.controlRect.y, - valueWidth, - layout.controlRect.height); - layout.incrementRect = UIRect( - layout.valueRect.x + layout.valueRect.width + metrics.buttonGap, - layout.controlRect.y, - buttonWidth, - layout.controlRect.height); + layout.bounds = hostLayout.bounds; + layout.labelRect = hostLayout.labelRect; + layout.controlRect = hostLayout.controlRect; + layout.valueRect = layout.controlRect; return layout; } UIEditorNumberFieldHitTarget HitTestUIEditorNumberField( const UIEditorNumberFieldLayout& layout, const UIPoint& point) { - if (IsUIEditorNumberFieldPointInside(layout.decrementRect, point)) { - return { UIEditorNumberFieldHitTargetKind::DecrementButton }; - } - if (IsUIEditorNumberFieldPointInside(layout.incrementRect, point)) { - return { UIEditorNumberFieldHitTargetKind::IncrementButton }; - } if (IsUIEditorNumberFieldPointInside(layout.valueRect, point)) { return { UIEditorNumberFieldHitTargetKind::ValueBox }; } @@ -181,12 +197,18 @@ void AppendUIEditorNumberFieldBackground( const UIEditorNumberFieldState& state, const UIEditorNumberFieldPalette& palette, const UIEditorNumberFieldMetrics& metrics) { - drawList.AddFilledRect(layout.bounds, ResolveRowFillColor(state, palette), metrics.cornerRounding); - drawList.AddRectOutline( - layout.bounds, - state.focused ? palette.focusedBorderColor : palette.borderColor, - state.focused ? metrics.focusedBorderThickness : metrics.borderThickness, - metrics.cornerRounding); + const auto rowFillColor = ResolveRowFillColor(state, palette); + if (rowFillColor.a > 0.0f) { + drawList.AddFilledRect(layout.bounds, rowFillColor, metrics.cornerRounding); + } + const auto rowBorderColor = state.focused ? palette.focusedBorderColor : palette.borderColor; + if (rowBorderColor.a > 0.0f) { + drawList.AddRectOutline( + layout.bounds, + rowBorderColor, + state.focused ? metrics.focusedBorderThickness : metrics.borderThickness, + metrics.cornerRounding); + } drawList.AddFilledRect( layout.valueRect, @@ -194,63 +216,26 @@ void AppendUIEditorNumberFieldBackground( metrics.valueBoxRounding); drawList.AddRectOutline( layout.valueRect, - palette.controlBorderColor, - metrics.borderThickness, + state.editing ? palette.controlFocusedBorderColor : palette.controlBorderColor, + state.editing ? metrics.focusedBorderThickness : metrics.borderThickness, metrics.valueBoxRounding); - - drawList.AddFilledRect( - layout.decrementRect, - ResolveButtonFillColor(spec, state, UIEditorNumberFieldHitTargetKind::DecrementButton, palette), - metrics.buttonRounding); - drawList.AddRectOutline( - layout.decrementRect, - palette.controlBorderColor, - metrics.borderThickness, - metrics.buttonRounding); - - drawList.AddFilledRect( - layout.incrementRect, - ResolveButtonFillColor(spec, state, UIEditorNumberFieldHitTargetKind::IncrementButton, palette), - metrics.buttonRounding); - drawList.AddRectOutline( - layout.incrementRect, - palette.controlBorderColor, - metrics.borderThickness, - metrics.buttonRounding); } void AppendUIEditorNumberFieldForeground( UIDrawList& drawList, const UIEditorNumberFieldLayout& layout, const UIEditorNumberFieldSpec& spec, + const UIEditorNumberFieldState& state, const UIEditorNumberFieldPalette& palette, const UIEditorNumberFieldMetrics& metrics) { - drawList.PushClipRect(layout.labelRect); - drawList.AddText( - UIPoint(layout.labelRect.x, layout.labelRect.y + metrics.labelTextInsetY), - spec.label, - palette.labelColor, - 13.0f); - drawList.PopClipRect(); - - drawList.AddText( - UIPoint(layout.decrementRect.x + 8.0f, layout.decrementRect.y + metrics.buttonTextInsetY), - "-", - palette.stepTextColor, - 13.0f); - drawList.AddText( - UIPoint(layout.incrementRect.x + 7.0f, layout.incrementRect.y + metrics.buttonTextInsetY), - "+", - palette.stepTextColor, - 13.0f); - - drawList.PushClipRect(layout.valueRect); - drawList.AddText( - UIPoint(layout.valueRect.x + metrics.valueTextInsetX, layout.valueRect.y + metrics.valueTextInsetY), - FormatUIEditorNumberFieldValue(spec), - spec.readOnly ? palette.readOnlyValueColor : palette.valueColor, - 13.0f); - drawList.PopClipRect(); + AppendLabelText(drawList, layout, spec.label, palette, metrics); + AppendValueText( + drawList, + layout, + state.editing ? std::string_view(state.displayText) : std::string_view(FormatUIEditorNumberFieldValue(spec)), + spec, + palette, + metrics); } void AppendUIEditorNumberField( @@ -262,23 +247,17 @@ void AppendUIEditorNumberField( const UIEditorNumberFieldMetrics& metrics) { const UIEditorNumberFieldLayout layout = BuildUIEditorNumberFieldLayout(bounds, spec, metrics); AppendUIEditorNumberFieldBackground(drawList, layout, spec, state, palette, metrics); - AppendUIEditorNumberFieldForeground(drawList, layout, spec, palette, metrics); - if (state.editing) { - drawList.AddFilledRect(layout.valueRect, palette.valueBoxEditingColor, metrics.valueBoxRounding); - drawList.AddRectOutline( - layout.valueRect, - palette.controlBorderColor, - metrics.borderThickness, - metrics.valueBoxRounding); - drawList.PushClipRect(layout.valueRect); - drawList.AddText( - UIPoint(layout.valueRect.x + metrics.valueTextInsetX, layout.valueRect.y + metrics.valueTextInsetY), - state.displayText, - palette.valueColor, - 13.0f); - drawList.PopClipRect(); + UIEditorNumberFieldSpec editingSpec = spec; + if (double parsedValue = 0.0; TryParseUIEditorNumberFieldValue(spec, state.displayText, parsedValue)) { + editingSpec.value = parsedValue; + } + + AppendUIEditorNumberFieldForeground(drawList, layout, editingSpec, state, palette, metrics); + return; } + + AppendUIEditorNumberFieldForeground(drawList, layout, spec, state, palette, metrics); } } // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Widgets/UIEditorPropertyGrid.cpp b/new_editor/src/Widgets/UIEditorPropertyGrid.cpp index 10b84d4b..bc3e524d 100644 --- a/new_editor/src/Widgets/UIEditorPropertyGrid.cpp +++ b/new_editor/src/Widgets/UIEditorPropertyGrid.cpp @@ -1,11 +1,30 @@ #include +#include +#include +#include +#include +#include +#include +#include + +#include + #include +#include namespace XCEngine::UI::Editor::Widgets { namespace { +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; +using ::XCEngine::UI::UISize; +using ::XCEngine::UI::Widgets::ResolvePopupPlacementRect; +using ::XCEngine::UI::Widgets::UIPopupPlacement; +using ::XCEngine::UI::Editor::UIEditorMenuItemKind; + float ClampNonNegative(float value) { return (std::max)(value, 0.0f); } @@ -26,16 +45,105 @@ float ResolveFieldRowHeight( : metrics.fieldRowHeight; } -::XCEngine::UI::UIPoint ResolveDisclosureGlyphPosition( - const ::XCEngine::UI::UIRect& rect, - float textInsetY) { - return ::XCEngine::UI::UIPoint(rect.x + 2.0f, rect.y + textInsetY - 1.0f); +UIPoint ResolveDisclosureGlyphPosition( + const UIRect& rect, + const UIEditorPropertyGridMetrics& metrics) { + return UIPoint( + rect.x + metrics.disclosureGlyphInsetX, + rect.y + metrics.sectionTextInsetY + metrics.disclosureGlyphInsetY); } -const std::string& ResolveDisplayedFieldValue( +UIEditorBoolFieldSpec BuildBoolFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorBoolFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.value = field.boolValue; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorNumberFieldSpec BuildNumberFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorNumberFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.value = field.numberValue.value; + spec.step = field.numberValue.step; + spec.minValue = field.numberValue.minValue; + spec.maxValue = field.numberValue.maxValue; + spec.integerMode = field.numberValue.integerMode; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorEnumFieldSpec BuildEnumFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorEnumFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.options = field.enumValue.options; + spec.selectedIndex = field.enumValue.selectedIndex; + spec.readOnly = field.readOnly; + return spec; +} + +UIEditorTextFieldSpec BuildTextFieldSpec(const UIEditorPropertyGridField& field) { + UIEditorTextFieldSpec spec = {}; + spec.fieldId = field.fieldId; + spec.label = field.label; + spec.value = field.valueText; + spec.readOnly = field.readOnly; + return spec; +} + +struct UIEditorPropertyGridFieldRects { + UIRect labelRect = {}; + UIRect valueRect = {}; +}; + +UIEditorPropertyGridFieldRects ResolveFieldRects( + const UIRect& rowRect, + const UIEditorPropertyGridField& field, + const UIEditorPropertyGridMetrics& metrics) { + switch (field.kind) { + case UIEditorPropertyGridFieldKind::Bool: { + const UIEditorBoolFieldLayout fieldLayout = BuildUIEditorBoolFieldLayout( + rowRect, + BuildBoolFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorHostedBoolFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.controlRect }; + } + + case UIEditorPropertyGridFieldKind::Number: { + const UIEditorNumberFieldLayout fieldLayout = BuildUIEditorNumberFieldLayout( + rowRect, + BuildNumberFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.valueRect }; + } + + case UIEditorPropertyGridFieldKind::Enum: { + const UIEditorEnumFieldLayout fieldLayout = BuildUIEditorEnumFieldLayout( + rowRect, + BuildEnumFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorHostedEnumFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.valueRect }; + } + + case UIEditorPropertyGridFieldKind::Text: + default: { + const UIEditorTextFieldLayout fieldLayout = BuildUIEditorTextFieldLayout( + rowRect, + BuildTextFieldSpec(field), + ::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(metrics)); + return { fieldLayout.labelRect, fieldLayout.valueRect }; + } + } +} + +const std::string& ResolveDisplayedTextFieldValue( const UIEditorPropertyGridField& field, const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel) { - if (propertyEditModel.HasActiveEdit() && + if (field.kind == UIEditorPropertyGridFieldKind::Text && + propertyEditModel.HasActiveEdit() && propertyEditModel.GetActiveFieldId() == field.fieldId) { return propertyEditModel.GetStagedValue(); } @@ -43,11 +151,140 @@ const std::string& ResolveDisplayedFieldValue( return field.valueText; } +UIEditorBoolFieldHitTargetKind ResolveBoolHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorBoolFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorBoolFieldHitTargetKind::Checkbox + : UIEditorBoolFieldHitTargetKind::Row; +} + +UIEditorNumberFieldHitTargetKind ResolveNumberHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorNumberFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorNumberFieldHitTargetKind::ValueBox + : UIEditorNumberFieldHitTargetKind::Row; +} + +UIEditorEnumFieldHitTargetKind ResolveEnumHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorEnumFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorEnumFieldHitTargetKind::ValueBox + : UIEditorEnumFieldHitTargetKind::Row; +} + +UIEditorTextFieldHitTargetKind ResolveTextHoveredTarget( + const UIEditorPropertyGridState& state, + const UIEditorPropertyGridField& field) { + if (state.hoveredFieldId != field.fieldId) { + return UIEditorTextFieldHitTargetKind::None; + } + + return state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorTextFieldHitTargetKind::ValueBox + : UIEditorTextFieldHitTargetKind::Row; +} + +std::vector BuildEnumPopupItems( + const UIEditorPropertyGridField& field) { + std::vector items = {}; + if (field.kind != UIEditorPropertyGridFieldKind::Enum) { + return items; + } + + items.reserve(field.enumValue.options.size()); + const std::size_t selectedIndex = + field.enumValue.options.empty() + ? 0u + : (std::min)(field.enumValue.selectedIndex, field.enumValue.options.size() - 1u); + for (std::size_t index = 0u; index < field.enumValue.options.size(); ++index) { + UIEditorMenuPopupItem item = {}; + item.itemId = field.fieldId + "." + std::to_string(index); + item.kind = UIEditorMenuItemKind::Command; + item.label = field.enumValue.options[index]; + item.enabled = !field.readOnly; + item.checked = index == selectedIndex; + items.push_back(std::move(item)); + } + + return items; +} + +UIRect ResolvePopupViewportRect(const UIRect& bounds) { + return UIRect(bounds.x - 4096.0f, bounds.y - 4096.0f, 8192.0f, 8192.0f); +} + +bool BuildEnumPopupRuntime( + const UIEditorPropertyGridLayout& layout, + const std::vector& sections, + const UIEditorPropertyGridState& state, + const UIEditorMenuPopupMetrics& popupMetrics, + UIEditorMenuPopupLayout& popupLayout, + UIEditorMenuPopupState& popupState, + std::vector& popupItems) { + if (state.popupFieldId.empty()) { + return false; + } + + const std::size_t visibleFieldIndex = + FindUIEditorPropertyGridVisibleFieldIndex(layout, state.popupFieldId, sections); + if (visibleFieldIndex == UIEditorPropertyGridInvalidIndex || + visibleFieldIndex >= layout.visibleFieldSectionIndices.size() || + visibleFieldIndex >= layout.visibleFieldIndices.size()) { + return false; + } + + const std::size_t sectionIndex = layout.visibleFieldSectionIndices[visibleFieldIndex]; + const std::size_t fieldIndex = layout.visibleFieldIndices[visibleFieldIndex]; + if (sectionIndex >= sections.size() || + fieldIndex >= sections[sectionIndex].fields.size()) { + return false; + } + + const UIEditorPropertyGridField& field = sections[sectionIndex].fields[fieldIndex]; + popupItems = BuildEnumPopupItems(field); + if (popupItems.empty()) { + return false; + } + + const float popupWidth = (std::max)( + layout.fieldValueRects[visibleFieldIndex].width, + ResolveUIEditorMenuPopupDesiredWidth(popupItems, popupMetrics)); + const float popupHeight = MeasureUIEditorMenuPopupHeight(popupItems, popupMetrics); + const auto placement = ResolvePopupPlacementRect( + layout.fieldValueRects[visibleFieldIndex], + UISize(popupWidth, popupHeight), + ResolvePopupViewportRect(layout.bounds), + UIPopupPlacement::BottomStart); + + popupLayout = BuildUIEditorMenuPopupLayout(placement.rect, popupItems, popupMetrics); + popupState.focused = state.focused || !state.popupFieldId.empty(); + popupState.hoveredIndex = + state.popupHighlightedIndex < popupItems.size() + ? state.popupHighlightedIndex + : UIEditorMenuPopupInvalidIndex; + return true; +} + } // namespace bool IsUIEditorPropertyGridPointInside( - const ::XCEngine::UI::UIRect& rect, - const ::XCEngine::UI::UIPoint& point) { + const UIRect& rect, + const UIPoint& point) { return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && @@ -81,6 +318,24 @@ UIEditorPropertyGridFieldLocation FindUIEditorPropertyGridFieldLocation( return {}; } +std::string ResolveUIEditorPropertyGridFieldValueText( + const UIEditorPropertyGridField& field) { + switch (field.kind) { + case UIEditorPropertyGridFieldKind::Bool: + return field.boolValue ? "true" : "false"; + + case UIEditorPropertyGridFieldKind::Number: + return FormatUIEditorNumberFieldValue(BuildNumberFieldSpec(field)); + + case UIEditorPropertyGridFieldKind::Enum: + return ResolveUIEditorEnumFieldValueText(BuildEnumFieldSpec(field)); + + case UIEditorPropertyGridFieldKind::Text: + default: + return field.valueText; + } +} + std::size_t FindUIEditorPropertyGridVisibleFieldIndex( const UIEditorPropertyGridLayout& layout, std::string_view fieldId, @@ -104,12 +359,12 @@ std::size_t FindUIEditorPropertyGridVisibleFieldIndex( } UIEditorPropertyGridLayout BuildUIEditorPropertyGridLayout( - const ::XCEngine::UI::UIRect& bounds, + const UIRect& bounds, const std::vector& sections, const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel, const UIEditorPropertyGridMetrics& metrics) { UIEditorPropertyGridLayout layout = {}; - layout.bounds = ::XCEngine::UI::UIRect( + layout.bounds = UIRect( bounds.x, bounds.y, ClampNonNegative(bounds.width), @@ -126,19 +381,19 @@ UIEditorPropertyGridLayout BuildUIEditorPropertyGridLayout( const float headerHeight = ResolveSectionHeaderHeight(section, metrics); const bool expanded = expansionModel.IsExpanded(section.sectionId); - const ::XCEngine::UI::UIRect headerRect( + const UIRect headerRect( contentX, cursorY, contentWidth, headerHeight); - const ::XCEngine::UI::UIRect disclosureRect( + const UIRect disclosureRect( headerRect.x + metrics.horizontalPadding, headerRect.y + (headerRect.height - metrics.disclosureExtent) * 0.5f, metrics.disclosureExtent, metrics.disclosureExtent); const float titleX = disclosureRect.x + disclosureRect.width + metrics.disclosureLabelGap; - const ::XCEngine::UI::UIRect titleRect( + const UIRect titleRect( titleX, headerRect.y, (std::max)(0.0f, headerRect.x + headerRect.width - titleX - metrics.horizontalPadding), @@ -155,37 +410,19 @@ UIEditorPropertyGridLayout BuildUIEditorPropertyGridLayout( for (std::size_t fieldIndex = 0u; fieldIndex < section.fields.size(); ++fieldIndex) { const UIEditorPropertyGridField& field = section.fields[fieldIndex]; const float rowHeight = ResolveFieldRowHeight(field, metrics); - const ::XCEngine::UI::UIRect rowRect( + const UIRect rowRect( contentX, cursorY, contentWidth, rowHeight); - const float controlX = - (std::min)( - rowRect.x + rowRect.width - metrics.horizontalPadding, - rowRect.x + metrics.controlColumnStart); - const float labelWidth = - (std::max)( - 0.0f, - controlX - rowRect.x - metrics.horizontalPadding - metrics.labelControlGap); - const ::XCEngine::UI::UIRect labelRect( - rowRect.x + metrics.horizontalPadding, - rowRect.y, - labelWidth, - rowRect.height); - const ::XCEngine::UI::UIRect valueRect( - controlX, - rowRect.y + metrics.valueBoxInsetY, - (std::max)( - 0.0f, - rowRect.x + rowRect.width - controlX - metrics.horizontalPadding), - (std::max)(0.0f, rowRect.height - metrics.valueBoxInsetY * 2.0f)); + const UIEditorPropertyGridFieldRects fieldRects = + ResolveFieldRects(rowRect, field, metrics); layout.visibleFieldSectionIndices.push_back(sectionIndex); layout.visibleFieldIndices.push_back(fieldIndex); layout.fieldRowRects.push_back(rowRect); - layout.fieldLabelRects.push_back(labelRect); - layout.fieldValueRects.push_back(valueRect); + layout.fieldLabelRects.push_back(fieldRects.labelRect); + layout.fieldValueRects.push_back(fieldRects.valueRect); layout.fieldReadOnly.push_back(field.readOnly); cursorY += rowHeight + metrics.rowGap; @@ -204,7 +441,7 @@ UIEditorPropertyGridLayout BuildUIEditorPropertyGridLayout( UIEditorPropertyGridHitTarget HitTestUIEditorPropertyGrid( const UIEditorPropertyGridLayout& layout, - const ::XCEngine::UI::UIPoint& point) { + const UIPoint& point) { for (std::size_t sectionVisibleIndex = 0u; sectionVisibleIndex < layout.sectionHeaderRects.size(); ++sectionVisibleIndex) { @@ -239,11 +476,11 @@ UIEditorPropertyGridHitTarget HitTestUIEditorPropertyGrid( } void AppendUIEditorPropertyGridBackground( - ::XCEngine::UI::UIDrawList& drawList, + UIDrawList& drawList, const UIEditorPropertyGridLayout& layout, const std::vector& sections, const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, - const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, + const ::XCEngine::UI::Widgets::UIPropertyEditModel&, const UIEditorPropertyGridState& state, const UIEditorPropertyGridPalette& palette, const UIEditorPropertyGridMetrics& metrics) { @@ -277,8 +514,6 @@ void AppendUIEditorPropertyGridBackground( section.fields[layout.visibleFieldIndices[visibleFieldIndex]]; const bool selected = selectionModel.IsSelected(field.fieldId); const bool hovered = state.hoveredFieldId == field.fieldId; - const bool editing = propertyEditModel.HasActiveEdit() && - propertyEditModel.GetActiveFieldId() == field.fieldId; if (selected || hovered) { drawList.AddFilledRect( @@ -288,32 +523,19 @@ void AppendUIEditorPropertyGridBackground( : palette.fieldHoverColor, metrics.cornerRounding); } - - const ::XCEngine::UI::UIColor valueBoxColor = - field.readOnly - ? palette.valueBoxReadOnlyColor - : (editing - ? palette.valueBoxEditingColor - : (hovered ? palette.valueBoxHoverColor : palette.valueBoxColor)); - drawList.AddFilledRect( - layout.fieldValueRects[visibleFieldIndex], - valueBoxColor, - metrics.valueBoxRounding); - drawList.AddRectOutline( - layout.fieldValueRects[visibleFieldIndex], - editing ? palette.valueBoxEditingBorderColor : palette.valueBoxBorderColor, - editing ? metrics.editOutlineThickness : metrics.borderThickness, - metrics.valueBoxRounding); } } void AppendUIEditorPropertyGridForeground( - ::XCEngine::UI::UIDrawList& drawList, + UIDrawList& drawList, const UIEditorPropertyGridLayout& layout, const std::vector& sections, + const UIEditorPropertyGridState& state, const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const UIEditorPropertyGridPalette& palette, - const UIEditorPropertyGridMetrics& metrics) { + const UIEditorPropertyGridMetrics& metrics, + const UIEditorMenuPopupPalette& popupPalette, + const UIEditorMenuPopupMetrics& popupMetrics) { drawList.PushClipRect(layout.bounds); for (std::size_t sectionVisibleIndex = 0u; @@ -324,19 +546,36 @@ void AppendUIEditorPropertyGridForeground( drawList.AddText( ResolveDisclosureGlyphPosition( layout.sectionDisclosureRects[sectionVisibleIndex], - metrics.sectionTextInsetY), + metrics), layout.sectionExpanded[sectionVisibleIndex] ? "v" : ">", palette.disclosureColor, - 12.0f); + metrics.disclosureGlyphFontSize); drawList.AddText( - ::XCEngine::UI::UIPoint( + UIPoint( layout.sectionTitleRects[sectionVisibleIndex].x, layout.sectionTitleRects[sectionVisibleIndex].y + metrics.sectionTextInsetY), section.title, palette.sectionTextColor, - 12.0f); + metrics.sectionFontSize); } + const UIEditorBoolFieldMetrics boolMetrics = + ::XCEngine::UI::Editor::BuildUIEditorHostedBoolFieldMetrics(metrics); + const UIEditorBoolFieldPalette boolPalette = + ::XCEngine::UI::Editor::BuildUIEditorHostedBoolFieldPalette(palette); + const UIEditorNumberFieldMetrics numberMetrics = + ::XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldMetrics(metrics); + const UIEditorNumberFieldPalette numberPalette = + ::XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldPalette(palette); + const UIEditorTextFieldMetrics textMetrics = + ::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(metrics); + const UIEditorTextFieldPalette textPalette = + ::XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette(palette); + const UIEditorEnumFieldMetrics enumMetrics = + ::XCEngine::UI::Editor::BuildUIEditorHostedEnumFieldMetrics(metrics); + const UIEditorEnumFieldPalette enumPalette = + ::XCEngine::UI::Editor::BuildUIEditorHostedEnumFieldPalette(palette); + for (std::size_t visibleFieldIndex = 0u; visibleFieldIndex < layout.fieldRowRects.size(); ++visibleFieldIndex) { @@ -346,53 +585,133 @@ void AppendUIEditorPropertyGridForeground( section.fields[layout.visibleFieldIndices[visibleFieldIndex]]; const bool editing = propertyEditModel.HasActiveEdit() && propertyEditModel.GetActiveFieldId() == field.fieldId; - const std::string& displayedValue = - ResolveDisplayedFieldValue(field, propertyEditModel); - drawList.PushClipRect(layout.fieldLabelRects[visibleFieldIndex]); - drawList.AddText( - ::XCEngine::UI::UIPoint( - layout.fieldLabelRects[visibleFieldIndex].x, - layout.fieldLabelRects[visibleFieldIndex].y + metrics.labelTextInsetY), - field.label, - palette.labelTextColor, - 12.0f); - drawList.PopClipRect(); - - drawList.PushClipRect(layout.fieldValueRects[visibleFieldIndex]); - drawList.AddText( - ::XCEngine::UI::UIPoint( - layout.fieldValueRects[visibleFieldIndex].x + metrics.valueBoxInsetX, - layout.fieldValueRects[visibleFieldIndex].y + metrics.valueTextInsetY), - displayedValue, - field.readOnly ? palette.readOnlyValueTextColor : palette.valueTextColor, - 12.0f); - if (editing) { - drawList.AddText( - ::XCEngine::UI::UIPoint( - layout.fieldValueRects[visibleFieldIndex].x + - (std::max)(0.0f, layout.fieldValueRects[visibleFieldIndex].width - 34.0f), - layout.fieldValueRects[visibleFieldIndex].y + metrics.valueTextInsetY), - "EDIT", - palette.editTagColor, - 11.0f); + switch (field.kind) { + case UIEditorPropertyGridFieldKind::Bool: { + UIEditorBoolFieldState fieldState = {}; + fieldState.hoveredTarget = ResolveBoolHoveredTarget(state, field); + fieldState.focused = state.focused; + fieldState.active = state.pressedFieldId == field.fieldId; + AppendUIEditorBoolField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + BuildBoolFieldSpec(field), + fieldState, + boolPalette, + boolMetrics); + break; + } + + case UIEditorPropertyGridFieldKind::Number: { + UIEditorNumberFieldState fieldState = {}; + fieldState.hoveredTarget = ResolveNumberHoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? UIEditorNumberFieldHitTargetKind::ValueBox + : UIEditorNumberFieldHitTargetKind::None; + fieldState.focused = state.focused; + fieldState.editing = editing; + fieldState.displayText = editing + ? propertyEditModel.GetStagedValue() + : ResolveUIEditorPropertyGridFieldValueText(field); + AppendUIEditorNumberField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + BuildNumberFieldSpec(field), + fieldState, + numberPalette, + numberMetrics); + break; + } + + case UIEditorPropertyGridFieldKind::Enum: { + UIEditorEnumFieldState fieldState = {}; + fieldState.hoveredTarget = ResolveEnumHoveredTarget(state, field); + fieldState.focused = state.focused; + fieldState.active = state.pressedFieldId == field.fieldId; + fieldState.popupOpen = state.popupFieldId == field.fieldId; + AppendUIEditorEnumField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + BuildEnumFieldSpec(field), + fieldState, + enumPalette, + enumMetrics); + break; + } + + case UIEditorPropertyGridFieldKind::Text: + default: { + const std::string& displayedValue = + ResolveDisplayedTextFieldValue(field, propertyEditModel); + UIEditorTextFieldState fieldState = {}; + fieldState.hoveredTarget = ResolveTextHoveredTarget(state, field); + fieldState.activeTarget = + state.pressedFieldId == field.fieldId + ? (state.hoveredHitTarget == UIEditorPropertyGridHitTargetKind::ValueBox + ? UIEditorTextFieldHitTargetKind::ValueBox + : UIEditorTextFieldHitTargetKind::Row) + : UIEditorTextFieldHitTargetKind::None; + fieldState.focused = state.focused; + fieldState.editing = editing; + fieldState.displayText = displayedValue; + + UIEditorTextFieldSpec fieldSpec = BuildTextFieldSpec(field); + fieldSpec.value = editing ? std::string() : displayedValue; + AppendUIEditorTextField( + drawList, + layout.fieldRowRects[visibleFieldIndex], + fieldSpec, + fieldState, + textPalette, + textMetrics); + + if (editing) { + drawList.PushClipRect( + ResolveUIEditorTextClipRect( + layout.fieldValueRects[visibleFieldIndex], + metrics.tagFontSize)); + drawList.AddText( + UIPoint( + layout.fieldValueRects[visibleFieldIndex].x + + (std::max)(0.0f, layout.fieldValueRects[visibleFieldIndex].width - 34.0f), + ResolveUIEditorTextTop( + layout.fieldValueRects[visibleFieldIndex], + metrics.tagFontSize, + metrics.valueTextInsetY)), + "EDIT", + palette.editTagColor, + metrics.tagFontSize); + drawList.PopClipRect(); + } + break; + } } - drawList.PopClipRect(); } drawList.PopClipRect(); + + UIEditorMenuPopupLayout popupLayout = {}; + UIEditorMenuPopupState popupState = {}; + std::vector popupItems = {}; + if (BuildEnumPopupRuntime(layout, sections, state, popupMetrics, popupLayout, popupState, popupItems)) { + AppendUIEditorMenuPopupBackground(drawList, popupLayout, popupItems, popupState, popupPalette, popupMetrics); + AppendUIEditorMenuPopupForeground(drawList, popupLayout, popupItems, popupState, popupPalette, popupMetrics); + } } void AppendUIEditorPropertyGrid( - ::XCEngine::UI::UIDrawList& drawList, - const ::XCEngine::UI::UIRect& bounds, + UIDrawList& drawList, + const UIRect& bounds, const std::vector& sections, const ::XCEngine::UI::Widgets::UISelectionModel& selectionModel, const ::XCEngine::UI::Widgets::UIExpansionModel& expansionModel, const ::XCEngine::UI::Widgets::UIPropertyEditModel& propertyEditModel, const UIEditorPropertyGridState& state, const UIEditorPropertyGridPalette& palette, - const UIEditorPropertyGridMetrics& metrics) { + const UIEditorPropertyGridMetrics& metrics, + const UIEditorMenuPopupPalette& popupPalette, + const UIEditorMenuPopupMetrics& popupMetrics) { const UIEditorPropertyGridLayout layout = BuildUIEditorPropertyGridLayout(bounds, sections, expansionModel, metrics); AppendUIEditorPropertyGridBackground( @@ -408,9 +727,12 @@ void AppendUIEditorPropertyGrid( drawList, layout, sections, + state, propertyEditModel, palette, - metrics); + metrics, + popupPalette, + popupMetrics); } } // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Widgets/UIEditorTextField.cpp b/new_editor/src/Widgets/UIEditorTextField.cpp new file mode 100644 index 00000000..be154b29 --- /dev/null +++ b/new_editor/src/Widgets/UIEditorTextField.cpp @@ -0,0 +1,165 @@ +#include +#include +#include + +#include + +namespace XCEngine::UI::Editor::Widgets { + +namespace { + +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; + +float ClampNonNegative(float value) { + return (std::max)(0.0f, value); +} + +::XCEngine::UI::UIColor ResolveRowFillColor( + const UIEditorTextFieldState& state, + const UIEditorTextFieldPalette& palette) { + if (state.activeTarget != UIEditorTextFieldHitTargetKind::None) { + return palette.rowActiveColor; + } + if (state.hoveredTarget != UIEditorTextFieldHitTargetKind::None) { + return palette.rowHoverColor; + } + return palette.surfaceColor; +} + +::XCEngine::UI::UIColor ResolveValueFillColor( + const UIEditorTextFieldSpec& spec, + const UIEditorTextFieldState& state, + const UIEditorTextFieldPalette& palette) { + if (spec.readOnly) { + return palette.readOnlyColor; + } + if (state.editing) { + return palette.valueBoxEditingColor; + } + if (state.hoveredTarget == UIEditorTextFieldHitTargetKind::ValueBox) { + return palette.valueBoxHoverColor; + } + return palette.valueBoxColor; +} + +} // namespace + +bool IsUIEditorTextFieldPointInside( + 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; +} + +UIEditorTextFieldLayout BuildUIEditorTextFieldLayout( + const UIRect& bounds, + const UIEditorTextFieldSpec&, + const UIEditorTextFieldMetrics& metrics) { + const UIEditorFieldRowLayout hostLayout = BuildUIEditorFieldRowLayout( + bounds, + metrics.valueBoxMinWidth, + UIEditorFieldRowLayoutMetrics { + metrics.rowHeight, + metrics.horizontalPadding, + metrics.labelControlGap, + metrics.controlColumnStart, + metrics.controlTrailingInset, + metrics.controlInsetY, + }); + + UIEditorTextFieldLayout layout = {}; + layout.bounds = hostLayout.bounds; + layout.labelRect = hostLayout.labelRect; + layout.controlRect = hostLayout.controlRect; + layout.valueRect = layout.controlRect; + return layout; +} + +UIEditorTextFieldHitTarget HitTestUIEditorTextField( + const UIEditorTextFieldLayout& layout, + const UIPoint& point) { + if (IsUIEditorTextFieldPointInside(layout.valueRect, point)) { + return { UIEditorTextFieldHitTargetKind::ValueBox }; + } + if (IsUIEditorTextFieldPointInside(layout.bounds, point)) { + return { UIEditorTextFieldHitTargetKind::Row }; + } + return {}; +} + +void AppendUIEditorTextFieldBackground( + UIDrawList& drawList, + const UIEditorTextFieldLayout& layout, + const UIEditorTextFieldSpec& spec, + const UIEditorTextFieldState& state, + const UIEditorTextFieldPalette& palette, + const UIEditorTextFieldMetrics& metrics) { + const auto rowFillColor = ResolveRowFillColor(state, palette); + if (rowFillColor.a > 0.0f) { + drawList.AddFilledRect(layout.bounds, rowFillColor, metrics.cornerRounding); + } + const auto rowBorderColor = state.focused ? palette.focusedBorderColor : palette.borderColor; + if (rowBorderColor.a > 0.0f) { + drawList.AddRectOutline( + layout.bounds, + rowBorderColor, + state.focused ? metrics.focusedBorderThickness : metrics.borderThickness, + metrics.cornerRounding); + } + + drawList.AddFilledRect( + layout.valueRect, + ResolveValueFillColor(spec, state, palette), + metrics.valueBoxRounding); + drawList.AddRectOutline( + layout.valueRect, + state.editing ? palette.controlFocusedBorderColor : palette.controlBorderColor, + state.editing ? metrics.focusedBorderThickness : metrics.borderThickness, + metrics.valueBoxRounding); +} + +void AppendUIEditorTextFieldForeground( + UIDrawList& drawList, + const UIEditorTextFieldLayout& layout, + const UIEditorTextFieldSpec& spec, + const UIEditorTextFieldState& state, + const UIEditorTextFieldPalette& palette, + const UIEditorTextFieldMetrics& 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(); + + drawList.PushClipRect(ResolveUIEditorTextClipRect(layout.valueRect, metrics.valueFontSize)); + drawList.AddText( + UIPoint( + layout.valueRect.x + metrics.valueTextInsetX, + ResolveUIEditorTextTop(layout.valueRect, metrics.valueFontSize, metrics.valueTextInsetY)), + state.editing ? state.displayText : spec.value, + spec.readOnly ? palette.readOnlyValueColor : palette.valueColor, + metrics.valueFontSize); + drawList.PopClipRect(); +} + +void AppendUIEditorTextField( + UIDrawList& drawList, + const UIRect& bounds, + const UIEditorTextFieldSpec& spec, + const UIEditorTextFieldState& state, + const UIEditorTextFieldPalette& palette, + const UIEditorTextFieldMetrics& metrics) { + const UIEditorTextFieldLayout layout = BuildUIEditorTextFieldLayout(bounds, spec, metrics); + AppendUIEditorTextFieldBackground(drawList, layout, spec, state, palette, metrics); + AppendUIEditorTextFieldForeground(drawList, layout, spec, state, palette, metrics); +} + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Widgets/UIEditorVector2Field.cpp b/new_editor/src/Widgets/UIEditorVector2Field.cpp new file mode 100644 index 00000000..2a36e296 --- /dev/null +++ b/new_editor/src/Widgets/UIEditorVector2Field.cpp @@ -0,0 +1,275 @@ +#include + +#include +#include +#include + +#include + +namespace XCEngine::UI::Editor::Widgets { + +namespace { + +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; + +float ClampNonNegative(float value) { + return (std::max)(0.0f, value); +} + +float ApproximateTextWidth(float fontSize, std::size_t characterCount) { + return fontSize * 0.55f * static_cast(characterCount); +} + +UIEditorNumberFieldSpec BuildComponentNumberSpec( + const UIEditorVector2FieldSpec& 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 UIEditorVector2FieldState& state, + const UIEditorVector2FieldPalette& palette) { + if (state.activeTarget != UIEditorVector2FieldHitTargetKind::None) { + return palette.rowActiveColor; + } + if (state.hoveredTarget != UIEditorVector2FieldHitTargetKind::None) { + return palette.rowHoverColor; + } + return palette.surfaceColor; +} + +::XCEngine::UI::UIColor ResolveComponentFillColor( + const UIEditorVector2FieldSpec& spec, + const UIEditorVector2FieldState& state, + const UIEditorVector2FieldPalette& 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 UIEditorVector2FieldState& state, + const UIEditorVector2FieldPalette& palette, + std::size_t componentIndex) { + if (state.editing && state.selectedComponentIndex == componentIndex) { + return palette.componentFocusedBorderColor; + } + return palette.componentBorderColor; +} + +::XCEngine::UI::UIColor ResolveAxisColor( + const UIEditorVector2FieldPalette& palette, + std::size_t componentIndex) { + return componentIndex == 0u ? palette.axisXColor : palette.axisYColor; +} + +} // namespace + +bool IsUIEditorVector2FieldPointInside( + 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 NormalizeUIEditorVector2FieldComponentValue( + const UIEditorVector2FieldSpec& spec, + double value) { + return NormalizeUIEditorNumberFieldValue(BuildComponentNumberSpec(spec, 0u), value); +} + +bool TryParseUIEditorVector2FieldComponentValue( + const UIEditorVector2FieldSpec& spec, + std::string_view text, + double& outValue) { + return TryParseUIEditorNumberFieldValue(BuildComponentNumberSpec(spec, 0u), text, outValue); +} + +std::string FormatUIEditorVector2FieldComponentValue( + const UIEditorVector2FieldSpec& spec, + std::size_t componentIndex) { + return FormatUIEditorNumberFieldValue(BuildComponentNumberSpec(spec, componentIndex)); +} + +UIEditorVector2FieldLayout BuildUIEditorVector2FieldLayout( + const UIRect& bounds, + const UIEditorVector2FieldSpec&, + const UIEditorVector2FieldMetrics& metrics) { + const float requiredControlWidth = metrics.componentMinWidth * 2.0f + metrics.componentGap; + 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) / 2.0f); + + UIEditorVector2FieldLayout layout = {}; + layout.bounds = hostLayout.bounds; + layout.labelRect = hostLayout.labelRect; + layout.controlRect = hostLayout.controlRect; + + for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { + const float componentX = + layout.controlRect.x + (componentWidth + metrics.componentGap) * static_cast(componentIndex); + const UIRect componentRect(componentX, layout.controlRect.y, componentWidth, layout.controlRect.height); + const float prefixWidth = (std::min)(metrics.componentPrefixWidth, componentRect.width); + const float labelGap = ClampNonNegative(metrics.componentLabelGap); + const float valueX = componentRect.x + prefixWidth + labelGap; + layout.componentRects[componentIndex] = componentRect; + layout.componentPrefixRects[componentIndex] = + UIRect(componentRect.x, componentRect.y, prefixWidth, componentRect.height); + layout.componentValueRects[componentIndex] = + UIRect( + valueX, + componentRect.y, + ClampNonNegative(componentRect.width - prefixWidth - labelGap), + componentRect.height); + } + + return layout; +} + +UIEditorVector2FieldHitTarget HitTestUIEditorVector2Field( + const UIEditorVector2FieldLayout& layout, + const UIPoint& point) { + for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { + if (IsUIEditorVector2FieldPointInside(layout.componentRects[componentIndex], point)) { + return { UIEditorVector2FieldHitTargetKind::Component, componentIndex }; + } + } + if (IsUIEditorVector2FieldPointInside(layout.bounds, point)) { + return { UIEditorVector2FieldHitTargetKind::Row, UIEditorVector2FieldInvalidComponentIndex }; + } + return {}; +} + +void AppendUIEditorVector2FieldBackground( + UIDrawList& drawList, + const UIEditorVector2FieldLayout& layout, + const UIEditorVector2FieldSpec& spec, + const UIEditorVector2FieldState& state, + const UIEditorVector2FieldPalette& palette, + const UIEditorVector2FieldMetrics& 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 AppendUIEditorVector2FieldForeground( + UIDrawList& drawList, + const UIEditorVector2FieldLayout& layout, + const UIEditorVector2FieldSpec& spec, + const UIEditorVector2FieldState& state, + const UIEditorVector2FieldPalette& palette, + const UIEditorVector2FieldMetrics& 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] + : FormatUIEditorVector2FieldComponentValue(spec, componentIndex), + spec.readOnly ? palette.readOnlyValueColor : palette.valueColor, + metrics.valueFontSize); + drawList.PopClipRect(); + } +} + +void AppendUIEditorVector2Field( + UIDrawList& drawList, + const UIRect& bounds, + const UIEditorVector2FieldSpec& spec, + const UIEditorVector2FieldState& state, + const UIEditorVector2FieldPalette& palette, + const UIEditorVector2FieldMetrics& metrics) { + const UIEditorVector2FieldLayout layout = BuildUIEditorVector2FieldLayout(bounds, spec, metrics); + AppendUIEditorVector2FieldBackground(drawList, layout, spec, state, palette, metrics); + AppendUIEditorVector2FieldForeground(drawList, layout, spec, state, palette, metrics); +} + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/new_editor/src/Widgets/UIEditorVector3Field.cpp b/new_editor/src/Widgets/UIEditorVector3Field.cpp new file mode 100644 index 00000000..4b649271 --- /dev/null +++ b/new_editor/src/Widgets/UIEditorVector3Field.cpp @@ -0,0 +1,283 @@ +#include + +#include +#include +#include + +#include + +namespace XCEngine::UI::Editor::Widgets { + +namespace { + +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; + +float ClampNonNegative(float value) { + return (std::max)(0.0f, value); +} + +float ApproximateTextWidth(float fontSize, std::size_t characterCount) { + return fontSize * 0.55f * static_cast(characterCount); +} + +UIEditorNumberFieldSpec BuildComponentNumberSpec( + const UIEditorVector3FieldSpec& 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 UIEditorVector3FieldState& state, + const UIEditorVector3FieldPalette& palette) { + if (state.activeTarget != UIEditorVector3FieldHitTargetKind::None) { + return palette.rowActiveColor; + } + if (state.hoveredTarget != UIEditorVector3FieldHitTargetKind::None) { + return palette.rowHoverColor; + } + return palette.surfaceColor; +} + +::XCEngine::UI::UIColor ResolveComponentFillColor( + const UIEditorVector3FieldSpec& spec, + const UIEditorVector3FieldState& state, + const UIEditorVector3FieldPalette& 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 UIEditorVector3FieldState& state, + const UIEditorVector3FieldPalette& palette, + std::size_t componentIndex) { + if (state.editing && state.selectedComponentIndex == componentIndex) { + return palette.componentFocusedBorderColor; + } + return palette.componentBorderColor; +} + +::XCEngine::UI::UIColor ResolveAxisColor( + const UIEditorVector3FieldPalette& palette, + std::size_t componentIndex) { + switch (componentIndex) { + case 0u: + return palette.axisXColor; + case 1u: + return palette.axisYColor; + case 2u: + default: + return palette.axisZColor; + } +} + +} // namespace + +bool IsUIEditorVector3FieldPointInside( + 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 NormalizeUIEditorVector3FieldComponentValue( + const UIEditorVector3FieldSpec& spec, + double value) { + return NormalizeUIEditorNumberFieldValue(BuildComponentNumberSpec(spec, 0u), value); +} + +bool TryParseUIEditorVector3FieldComponentValue( + const UIEditorVector3FieldSpec& spec, + std::string_view text, + double& outValue) { + return TryParseUIEditorNumberFieldValue(BuildComponentNumberSpec(spec, 0u), text, outValue); +} + +std::string FormatUIEditorVector3FieldComponentValue( + const UIEditorVector3FieldSpec& spec, + std::size_t componentIndex) { + return FormatUIEditorNumberFieldValue(BuildComponentNumberSpec(spec, componentIndex)); +} + +UIEditorVector3FieldLayout BuildUIEditorVector3FieldLayout( + const UIRect& bounds, + const UIEditorVector3FieldSpec&, + const UIEditorVector3FieldMetrics& metrics) { + const float requiredControlWidth = metrics.componentMinWidth * 3.0f + metrics.componentGap * 2.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 * 2.0f) / 3.0f); + + UIEditorVector3FieldLayout layout = {}; + layout.bounds = hostLayout.bounds; + layout.labelRect = hostLayout.labelRect; + layout.controlRect = hostLayout.controlRect; + + for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { + const float componentX = + layout.controlRect.x + (componentWidth + metrics.componentGap) * static_cast(componentIndex); + const UIRect componentRect(componentX, layout.controlRect.y, componentWidth, layout.controlRect.height); + const float prefixWidth = (std::min)(metrics.componentPrefixWidth, componentRect.width); + const float labelGap = ClampNonNegative(metrics.componentLabelGap); + const float valueX = componentRect.x + prefixWidth + labelGap; + layout.componentRects[componentIndex] = componentRect; + layout.componentPrefixRects[componentIndex] = + UIRect(componentRect.x, componentRect.y, prefixWidth, componentRect.height); + layout.componentValueRects[componentIndex] = + UIRect( + valueX, + componentRect.y, + ClampNonNegative(componentRect.width - prefixWidth - labelGap), + componentRect.height); + } + + return layout; +} + +UIEditorVector3FieldHitTarget HitTestUIEditorVector3Field( + const UIEditorVector3FieldLayout& layout, + const UIPoint& point) { + for (std::size_t componentIndex = 0u; componentIndex < layout.componentRects.size(); ++componentIndex) { + if (IsUIEditorVector3FieldPointInside(layout.componentRects[componentIndex], point)) { + return { UIEditorVector3FieldHitTargetKind::Component, componentIndex }; + } + } + if (IsUIEditorVector3FieldPointInside(layout.bounds, point)) { + return { UIEditorVector3FieldHitTargetKind::Row, UIEditorVector3FieldInvalidComponentIndex }; + } + return {}; +} + +void AppendUIEditorVector3FieldBackground( + UIDrawList& drawList, + const UIEditorVector3FieldLayout& layout, + const UIEditorVector3FieldSpec& spec, + const UIEditorVector3FieldState& state, + const UIEditorVector3FieldPalette& palette, + const UIEditorVector3FieldMetrics& 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 AppendUIEditorVector3FieldForeground( + UIDrawList& drawList, + const UIEditorVector3FieldLayout& layout, + const UIEditorVector3FieldSpec& spec, + const UIEditorVector3FieldState& state, + const UIEditorVector3FieldPalette& palette, + const UIEditorVector3FieldMetrics& 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] + : FormatUIEditorVector3FieldComponentValue(spec, componentIndex), + spec.readOnly ? palette.readOnlyValueColor : palette.valueColor, + metrics.valueFontSize); + drawList.PopClipRect(); + } +} + +void AppendUIEditorVector3Field( + UIDrawList& drawList, + const UIRect& bounds, + const UIEditorVector3FieldSpec& spec, + const UIEditorVector3FieldState& state, + const UIEditorVector3FieldPalette& palette, + const UIEditorVector3FieldMetrics& metrics) { + const UIEditorVector3FieldLayout layout = BuildUIEditorVector3FieldLayout(bounds, spec, metrics); + AppendUIEditorVector3FieldBackground(drawList, layout, spec, state, palette, metrics); + AppendUIEditorVector3FieldForeground(drawList, layout, spec, state, palette, metrics); +} + +} // namespace XCEngine::UI::Editor::Widgets diff --git a/tests/UI/Editor/integration/CMakeLists.txt b/tests/UI/Editor/integration/CMakeLists.txt index 70a491fd..7de645f9 100644 --- a/tests/UI/Editor/integration/CMakeLists.txt +++ b/tests/UI/Editor/integration/CMakeLists.txt @@ -68,6 +68,21 @@ if(TARGET editor_ui_number_field_basic_validation) editor_ui_number_field_basic_validation) endif() +if(TARGET editor_ui_text_field_basic_validation) + list(APPEND EDITOR_UI_INTEGRATION_TARGETS + editor_ui_text_field_basic_validation) +endif() + +if(TARGET editor_ui_vector2_field_basic_validation) + list(APPEND EDITOR_UI_INTEGRATION_TARGETS + editor_ui_vector2_field_basic_validation) +endif() + +if(TARGET editor_ui_vector3_field_basic_validation) + list(APPEND EDITOR_UI_INTEGRATION_TARGETS + editor_ui_vector3_field_basic_validation) +endif() + if(TARGET editor_ui_enum_field_basic_validation) list(APPEND EDITOR_UI_INTEGRATION_TARGETS editor_ui_enum_field_basic_validation) diff --git a/tests/UI/Editor/integration/shared/src/EditorValidationTheme.h b/tests/UI/Editor/integration/shared/src/EditorValidationTheme.h new file mode 100644 index 00000000..95c8e2ab --- /dev/null +++ b/tests/UI/Editor/integration/shared/src/EditorValidationTheme.h @@ -0,0 +1,218 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace XCEngine::Tests::EditorUI { + +struct EditorValidationThemeLoadResult { + ::XCEngine::UI::Style::UITheme theme = {}; + std::string error = {}; + bool succeeded = false; +}; + +struct EditorValidationShellPalette { + ::XCEngine::UI::UIColor windowBackground = ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f); + ::XCEngine::UI::UIColor cardBackground = ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f); + ::XCEngine::UI::UIColor cardBorder = ::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f); + ::XCEngine::UI::UIColor textPrimary = ::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 1.0f); + ::XCEngine::UI::UIColor textMuted = ::XCEngine::UI::UIColor(0.72f, 0.72f, 0.72f, 1.0f); + ::XCEngine::UI::UIColor textWeak = ::XCEngine::UI::UIColor(0.56f, 0.56f, 0.56f, 1.0f); + ::XCEngine::UI::UIColor textSuccess = ::XCEngine::UI::UIColor(0.63f, 0.76f, 0.63f, 1.0f); + ::XCEngine::UI::UIColor buttonBackground = ::XCEngine::UI::UIColor(0.25f, 0.25f, 0.25f, 1.0f); + ::XCEngine::UI::UIColor buttonHoverBackground = ::XCEngine::UI::UIColor(0.32f, 0.32f, 0.32f, 1.0f); +}; + +struct EditorValidationShellMetrics { + float margin = 20.0f; + float gap = 16.0f; + float cardRadius = 10.0f; + float buttonRadius = 8.0f; + float titleFontSize = 17.0f; + float bodyFontSize = 12.0f; +}; + +inline ::XCEngine::UI::UIColor ToUIColor(const ::XCEngine::Math::Color& color) { + return ::XCEngine::UI::UIColor(color.r, color.g, color.b, color.a); +} + +inline bool TryResolveThemeFloat( + const ::XCEngine::UI::Style::UITheme& theme, + std::string_view tokenName, + float& outValue) { + const auto resolution = + theme.ResolveToken(std::string(tokenName), ::XCEngine::UI::Style::UIStyleValueType::Float); + if (resolution.status != ::XCEngine::UI::Style::UITokenResolveStatus::Resolved) { + return false; + } + + const float* value = resolution.value.TryGetFloat(); + if (value == nullptr) { + return false; + } + + outValue = *value; + return true; +} + +inline bool TryResolveThemeColor( + const ::XCEngine::UI::Style::UITheme& theme, + std::string_view tokenName, + ::XCEngine::UI::UIColor& outColor) { + const auto resolution = + theme.ResolveToken(std::string(tokenName), ::XCEngine::UI::Style::UIStyleValueType::Color); + if (resolution.status != ::XCEngine::UI::Style::UITokenResolveStatus::Resolved) { + return false; + } + + const ::XCEngine::Math::Color* value = resolution.value.TryGetColor(); + if (value == nullptr) { + return false; + } + + outColor = ToUIColor(*value); + return true; +} + +inline float ResolveThemeFloatAliases( + const ::XCEngine::UI::Style::UITheme& theme, + std::initializer_list tokenNames, + float fallbackValue) { + float resolvedValue = fallbackValue; + for (std::string_view tokenName : tokenNames) { + if (TryResolveThemeFloat(theme, tokenName, resolvedValue)) { + return resolvedValue; + } + } + + return fallbackValue; +} + +inline ::XCEngine::UI::UIColor ResolveThemeColorAliases( + const ::XCEngine::UI::Style::UITheme& theme, + std::initializer_list tokenNames, + const ::XCEngine::UI::UIColor& fallbackValue) { + ::XCEngine::UI::UIColor resolvedValue = fallbackValue; + for (std::string_view tokenName : tokenNames) { + if (TryResolveThemeColor(theme, tokenName, resolvedValue)) { + return resolvedValue; + } + } + + return fallbackValue; +} + +inline EditorValidationThemeLoadResult LoadEditorValidationTheme( + const std::filesystem::path& themePath) { + EditorValidationThemeLoadResult result = {}; + + ::XCEngine::Resources::UIDocumentCompileResult compileResult = {}; + const ::XCEngine::Containers::String pathString(themePath.generic_string().c_str()); + if (!::XCEngine::Resources::CompileUIDocument( + ::XCEngine::Resources::UIDocumentCompileRequest { + ::XCEngine::Resources::UIDocumentKind::Theme, + pathString, + ::XCEngine::Resources::GetUIDocumentDefaultRootTag( + ::XCEngine::Resources::UIDocumentKind::Theme) + }, + compileResult)) { + result.error = compileResult.errorMessage.Empty() + ? std::string("Failed to compile editor validation theme document.") + : std::string(compileResult.errorMessage.CStr()); + return result; + } + + const auto styleCompileResult = + ::XCEngine::UI::Style::CompileDocumentStyle(compileResult.document); + if (!styleCompileResult.succeeded) { + result.error = styleCompileResult.errorMessage; + return result; + } + + result.theme = styleCompileResult.theme; + result.succeeded = true; + return result; +} + +inline EditorValidationShellPalette ResolveEditorValidationShellPalette( + const ::XCEngine::UI::Style::UITheme& theme) { + EditorValidationShellPalette palette = {}; + palette.windowBackground = ResolveThemeColorAliases( + theme, + { "editor.color.validation.window", "color.bg.workspace" }, + palette.windowBackground); + palette.cardBackground = ResolveThemeColorAliases( + theme, + { "editor.color.validation.card", "color.bg.panel" }, + palette.cardBackground); + palette.cardBorder = ResolveThemeColorAliases( + theme, + { "editor.color.validation.card_border", "editor.color.menu_popup.border" }, + palette.cardBorder); + palette.textPrimary = ResolveThemeColorAliases( + theme, + { "editor.color.validation.text_primary", "color.text.primary" }, + palette.textPrimary); + palette.textMuted = ResolveThemeColorAliases( + theme, + { "editor.color.validation.text_muted", "color.text.muted" }, + palette.textMuted); + palette.textWeak = ResolveThemeColorAliases( + theme, + { "editor.color.validation.text_weak", "color.text.muted" }, + palette.textWeak); + palette.textSuccess = ResolveThemeColorAliases( + theme, + { "editor.color.validation.text_success" }, + palette.textSuccess); + palette.buttonBackground = ResolveThemeColorAliases( + theme, + { "editor.color.validation.button", "color.bg.selection" }, + palette.buttonBackground); + palette.buttonHoverBackground = ResolveThemeColorAliases( + theme, + { "editor.color.validation.button_hover", "editor.color.validation.button", "color.bg.selection" }, + palette.buttonHoverBackground); + return palette; +} + +inline EditorValidationShellMetrics ResolveEditorValidationShellMetrics( + const ::XCEngine::UI::Style::UITheme& theme) { + EditorValidationShellMetrics metrics = {}; + metrics.margin = ResolveThemeFloatAliases( + theme, + { "editor.space.validation.margin", "space.shell" }, + metrics.margin); + metrics.gap = ResolveThemeFloatAliases( + theme, + { "editor.space.validation.gap" }, + metrics.gap); + metrics.cardRadius = ResolveThemeFloatAliases( + theme, + { "editor.radius.validation.card", "radius.panel" }, + metrics.cardRadius); + metrics.buttonRadius = ResolveThemeFloatAliases( + theme, + { "editor.radius.validation.button", "radius.control" }, + metrics.buttonRadius); + metrics.titleFontSize = ResolveThemeFloatAliases( + theme, + { "editor.font.validation.title" }, + metrics.titleFontSize); + metrics.bodyFontSize = ResolveThemeFloatAliases( + theme, + { "editor.font.validation.body", "editor.font.field.value" }, + metrics.bodyFontSize); + return metrics; +} + +} // namespace XCEngine::Tests::EditorUI diff --git a/tests/UI/Editor/integration/shared/themes/editor_validation.xctheme b/tests/UI/Editor/integration/shared/themes/editor_validation.xctheme index 66325bac..e58c984a 100644 --- a/tests/UI/Editor/integration/shared/themes/editor_validation.xctheme +++ b/tests/UI/Editor/integration/shared/themes/editor_validation.xctheme @@ -5,10 +5,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/UI/Editor/integration/shell/CMakeLists.txt b/tests/UI/Editor/integration/shell/CMakeLists.txt index 294b0e85..21445ab8 100644 --- a/tests/UI/Editor/integration/shell/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/CMakeLists.txt @@ -25,6 +25,15 @@ endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/number_field_basic/CMakeLists.txt") add_subdirectory(number_field_basic) endif() +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/text_field_basic/CMakeLists.txt") + add_subdirectory(text_field_basic) +endif() +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vector2_field_basic/CMakeLists.txt") + add_subdirectory(vector2_field_basic) +endif() +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vector3_field_basic/CMakeLists.txt") + add_subdirectory(vector3_field_basic) +endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/enum_field_basic/CMakeLists.txt") add_subdirectory(enum_field_basic) endif() diff --git a/tests/UI/Editor/integration/shell/bool_field_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/bool_field_basic/CMakeLists.txt index 055feab7..e05c9c07 100644 --- a/tests/UI/Editor/integration/shell/bool_field_basic/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/bool_field_basic/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(editor_ui_bool_field_basic_validation WIN32 ) target_include_directories(editor_ui_bool_field_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src ${CMAKE_SOURCE_DIR}/new_editor/include ${CMAKE_SOURCE_DIR}/new_editor/app ${CMAKE_SOURCE_DIR}/engine/include diff --git a/tests/UI/Editor/integration/shell/bool_field_basic/main.cpp b/tests/UI/Editor/integration/shell/bool_field_basic/main.cpp index 0f986a8c..c302669d 100644 --- a/tests/UI/Editor/integration/shell/bool_field_basic/main.cpp +++ b/tests/UI/Editor/integration/shell/bool_field_basic/main.cpp @@ -3,7 +3,9 @@ #endif #include +#include #include +#include "EditorValidationTheme.h" #include "Host/AutoScreenshot.h" #include "Host/NativeRenderer.h" @@ -45,19 +47,11 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorBoolField; using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTarget; using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind; using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec; +namespace Style = XCEngine::UI::Style; constexpr const wchar_t* kWindowClassName = L"XCUIEditorBoolFieldBasicValidation"; constexpr const wchar_t* kWindowTitle = L"XCUI Editor | BoolField Basic"; -constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f); -constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f); -constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f); -constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f); -constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f); -constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f); -constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f); -constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f); - enum class ActionId : unsigned char { Reset = 0, Capture @@ -83,10 +77,14 @@ std::filesystem::path ResolveRepoRootPath() { if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { root = root.substr(1u, root.size() - 2u); } - return std::filesystem::path(root).lexically_normal(); } +std::filesystem::path ResolveValidationThemePath() { + return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme") + .lexically_normal(); +} + bool ContainsPoint(const UIRect& rect, float x, float y) { return x >= rect.x && x <= rect.x + rect.width && @@ -105,10 +103,13 @@ std::int32_t MapBoolFieldKey(UINT keyCode) { } } -ScenarioLayout BuildScenarioLayout(float width, float height) { - constexpr float margin = 20.0f; +ScenarioLayout BuildScenarioLayout( + float width, + float height, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) { + const float margin = shellMetrics.margin; constexpr float leftWidth = 430.0f; - constexpr float gap = 16.0f; + const float gap = shellMetrics.gap; ScenarioLayout layout = {}; layout.introRect = UIRect(margin, margin, leftWidth, 214.0f); @@ -141,33 +142,48 @@ ScenarioLayout BuildScenarioLayout(float width, float height) { void DrawCard( UIDrawList& drawList, const UIRect& rect, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, std::string_view title, std::string_view subtitle = {}) { - drawList.AddFilledRect(rect, kCardBg, 10.0f); - drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f); - drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f); + drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius); + drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 14.0f), + std::string(title), + shellPalette.textPrimary, + shellMetrics.titleFontSize); if (!subtitle.empty()) { - drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 40.0f), + std::string(subtitle), + shellPalette.textMuted, + shellMetrics.bodyFontSize); } } void DrawButton( UIDrawList& drawList, const ButtonLayout& button, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, bool hovered) { - drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f); - drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f); + drawList.AddFilledRect( + button.rect, + hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground, + shellMetrics.buttonRadius); + drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius); drawList.AddText( UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), button.label, - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); } std::string DescribeHitTarget(const UIEditorBoolFieldHitTarget& hitTarget) { switch (hitTarget.kind) { - case UIEditorBoolFieldHitTargetKind::Toggle: - return "toggle"; + case UIEditorBoolFieldHitTargetKind::Checkbox: + return "checkbox"; case UIEditorBoolFieldHitTargetKind::Row: return "row"; case UIEditorBoolFieldHitTargetKind::None: @@ -355,6 +371,14 @@ private: m_captureRoot = ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/bool_field_basic/captures"; m_autoScreenshot.Initialize(m_captureRoot); + const auto themeLoad = + XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath()); + if (themeLoad.succeeded) { + m_theme = themeLoad.theme; + m_themeStatus = "loaded"; + } else { + m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error; + } ResetScenario(); return true; @@ -380,7 +404,10 @@ private: GetClientRect(m_hwnd, &clientRect); const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); - return BuildScenarioLayout(width, height); + return BuildScenarioLayout( + width, + height, + XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme)); } void ResetScenario() { @@ -403,12 +430,14 @@ private: } const ScenarioLayout layout = GetLayout(); + const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme); m_frame = UpdateUIEditorBoolFieldInteraction( m_interactionState, m_value, layout.fieldRect, m_spec, - {}); + {}, + metrics); } void OnResize(UINT width, UINT height) { @@ -502,18 +531,20 @@ private: UIEditorBoolFieldInteractionResult PumpEvents(std::vector events) { const ScenarioLayout layout = GetLayout(); + const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme); m_frame = UpdateUIEditorBoolFieldInteraction( m_interactionState, m_value, layout.fieldRect, m_spec, - std::move(events)); + std::move(events), + metrics); return m_frame.result; } void UpdateResultText(const UIEditorBoolFieldInteractionResult& result) { if (result.valueChanged) { - m_lastResult = std::string("值已切换到 ") + (m_value ? "true" : "false"); + m_lastResult = std::string("值已切换到: ") + (m_value ? "true" : "false"); return; } @@ -547,76 +578,90 @@ private: GetClientRect(m_hwnd, &clientRect); const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); - const ScenarioLayout layout = BuildScenarioLayout(width, height); + const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme); + const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme); + const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics); RefreshFrame(); const UIEditorBoolFieldHitTarget currentHit = HitTestUIEditorBoolField(m_frame.layout, m_mousePosition); + const auto boolMetrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme); + const auto boolPalette = XCEngine::UI::Editor::ResolveUIEditorBoolFieldPalette(m_theme); UIDrawData drawData = {}; UIDrawList& drawList = drawData.EmplaceDrawList("EditorBoolFieldBasic"); - drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground); DrawCard( drawList, layout.introRect, + shellPalette, + shellMetrics, "这个测试在验证什么功能", - "只验证 Editor BoolField 的基础交互契约,不涉及 PropertyGrid 或任何业务 Inspector。"); + "只验证 Editor BoolField 的基础交互契约,不涉及 PropertyGrid 或业务 Inspector。"); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f), - "1. 点击 row 或 toggle,检查 true / false 是否稳定切换。", - kTextPrimary, - 12.0f); + "1. 点击 row 或 checkbox,检查 true / false 是否稳定切换。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), "2. 控件获得 focus 后按 Space / Enter,也必须能切换值。", - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), "3. 检查 Hover / Focus / Value / Result 是否同步更新。", - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), "4. 按 F12 或点击截图按钮,确认自动截图路径正确。", - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); - DrawCard(drawList, layout.controlRect, "操作"); + DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作"); for (const ButtonLayout& button : layout.buttons) { DrawButton( drawList, button, + shellPalette, + shellMetrics, m_hasHoveredAction && m_hoveredAction == button.action); } - DrawCard(drawList, layout.stateRect, "状态摘要", "重点检查 hit / focus / value / result。"); + DrawCard( + drawList, + layout.stateRect, + shellPalette, + shellMetrics, + "状态摘要", + "重点检查 hit / focus / value / result。"); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f), "Hover: " + DescribeHitTarget(currentHit), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f), std::string("Focused: ") + (m_interactionState.fieldState.focused ? "开" : "关"), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f), std::string("Active: ") + (m_interactionState.fieldState.active ? "开" : "关"), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f), std::string("Value: ") + (m_value ? "true" : "false"), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f), "Result: " + m_lastResult, - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); const std::string captureSummary = m_autoScreenshot.HasPendingCapture() @@ -627,17 +672,30 @@ private: drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f), captureSummary, - kTextWeak, - 12.0f); + shellPalette.textWeak, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f), + "Theme: " + m_themeStatus, + shellPalette.textWeak, + shellMetrics.bodyFontSize); - DrawCard(drawList, layout.previewRect, "BoolField 预览", "这里只放一个 BoolField。"); + DrawCard( + drawList, + layout.previewRect, + shellPalette, + shellMetrics, + "BoolField 预览", + "这里只放一个 Unity 风格 BoolField。"); UIEditorBoolFieldSpec previewSpec = m_spec; previewSpec.value = m_value; AppendUIEditorBoolField( drawList, layout.fieldRect, previewSpec, - m_interactionState.fieldState); + m_interactionState.fieldState, + boolPalette, + boolMetrics); const bool framePresented = m_renderer.Render(drawData); m_autoScreenshot.CaptureIfRequested( @@ -657,10 +715,12 @@ private: bool m_value = false; UIEditorBoolFieldInteractionState m_interactionState = {}; UIEditorBoolFieldInteractionFrame m_frame = {}; + Style::UITheme m_theme = {}; UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); ActionId m_hoveredAction = ActionId::Reset; bool m_hasHoveredAction = false; std::string m_lastResult = {}; + std::string m_themeStatus = "fallback"; }; } // namespace diff --git a/tests/UI/Editor/integration/shell/enum_field_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/enum_field_basic/CMakeLists.txt index afe19f80..54e5790e 100644 --- a/tests/UI/Editor/integration/shell/enum_field_basic/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/enum_field_basic/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(editor_ui_enum_field_basic_validation WIN32 ) target_include_directories(editor_ui_enum_field_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src ${CMAKE_SOURCE_DIR}/new_editor/include ${CMAKE_SOURCE_DIR}/new_editor/app ${CMAKE_SOURCE_DIR}/engine/include diff --git a/tests/UI/Editor/integration/shell/enum_field_basic/main.cpp b/tests/UI/Editor/integration/shell/enum_field_basic/main.cpp index 5621981a..e38eb6e7 100644 --- a/tests/UI/Editor/integration/shell/enum_field_basic/main.cpp +++ b/tests/UI/Editor/integration/shell/enum_field_basic/main.cpp @@ -3,7 +3,10 @@ #endif #include +#include #include +#include +#include "EditorValidationTheme.h" #include "Host/AutoScreenshot.h" #include "Host/NativeRenderer.h" @@ -41,24 +44,18 @@ using XCEngine::UI::Editor::UIEditorEnumFieldInteractionResult; using XCEngine::UI::Editor::UIEditorEnumFieldInteractionState; using XCEngine::UI::Editor::UpdateUIEditorEnumFieldInteraction; using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumField; +using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuPopup; using XCEngine::UI::Editor::Widgets::HitTestUIEditorEnumField; using XCEngine::UI::Editor::Widgets::ResolveUIEditorEnumFieldValueText; using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTarget; using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind; using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec; +using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupInvalidIndex; +namespace Style = XCEngine::UI::Style; constexpr const wchar_t* kWindowClassName = L"XCUIEditorEnumFieldBasicValidation"; constexpr const wchar_t* kWindowTitle = L"XCUI Editor | EnumField Basic"; -constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f); -constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f); -constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f); -constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f); -constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f); -constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f); -constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f); -constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f); - enum class ActionId : unsigned char { Reset = 0, Capture @@ -87,6 +84,11 @@ std::filesystem::path ResolveRepoRootPath() { return std::filesystem::path(root).lexically_normal(); } +std::filesystem::path ResolveValidationThemePath() { + return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme") + .lexically_normal(); +} + bool ContainsPoint(const UIRect& rect, float x, float y) { return x >= rect.x && x <= rect.x + rect.width && @@ -96,34 +98,41 @@ bool ContainsPoint(const UIRect& rect, float x, float y) { std::int32_t MapEnumFieldKey(UINT keyCode) { switch (keyCode) { - case VK_LEFT: - return static_cast(KeyCode::Left); - case VK_RIGHT: - return static_cast(KeyCode::Right); + case VK_UP: + return static_cast(KeyCode::Up); + case VK_DOWN: + return static_cast(KeyCode::Down); case VK_HOME: return static_cast(KeyCode::Home); case VK_END: return static_cast(KeyCode::End); case VK_RETURN: return static_cast(KeyCode::Enter); + case VK_SPACE: + return static_cast(KeyCode::Space); + case VK_ESCAPE: + return static_cast(KeyCode::Escape); default: return static_cast(KeyCode::None); } } -ScenarioLayout BuildScenarioLayout(float width, float height) { - constexpr float margin = 20.0f; - constexpr float leftWidth = 430.0f; - constexpr float gap = 16.0f; +ScenarioLayout BuildScenarioLayout( + float width, + float height, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) { + const float margin = shellMetrics.margin; + constexpr float leftWidth = 440.0f; + const float gap = shellMetrics.gap; ScenarioLayout layout = {}; - layout.introRect = UIRect(margin, margin, leftWidth, 220.0f); + layout.introRect = UIRect(margin, margin, leftWidth, 240.0f); layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f); layout.stateRect = UIRect( margin, layout.controlRect.y + layout.controlRect.height + gap, leftWidth, - (std::max)(220.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin)); + (std::max)(250.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin)); layout.previewRect = UIRect( leftWidth + margin * 2.0f, margin, @@ -132,7 +141,7 @@ ScenarioLayout BuildScenarioLayout(float width, float height) { layout.fieldRect = UIRect( layout.previewRect.x + 24.0f, layout.previewRect.y + 82.0f, - 320.0f, + 340.0f, 32.0f); const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f; @@ -147,31 +156,48 @@ ScenarioLayout BuildScenarioLayout(float width, float height) { void DrawCard( UIDrawList& drawList, const UIRect& rect, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, std::string_view title, std::string_view subtitle = {}) { - drawList.AddFilledRect(rect, kCardBg, 10.0f); - drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f); - drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f); + drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius); + drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 14.0f), + std::string(title), + shellPalette.textPrimary, + shellMetrics.titleFontSize); if (!subtitle.empty()) { - drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 40.0f), + std::string(subtitle), + shellPalette.textMuted, + shellMetrics.bodyFontSize); } } void DrawButton( UIDrawList& drawList, const ButtonLayout& button, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, bool hovered) { - drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f); - drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f); - drawList.AddText(UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f); + drawList.AddFilledRect( + button.rect, + hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground, + shellMetrics.buttonRadius); + drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius); + drawList.AddText( + UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), + button.label, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); } std::string DescribeHitTarget(const UIEditorEnumFieldHitTarget& hitTarget) { switch (hitTarget.kind) { - case UIEditorEnumFieldHitTargetKind::PreviousButton: - return "previous"; - case UIEditorEnumFieldHitTargetKind::NextButton: - return "next"; + case UIEditorEnumFieldHitTargetKind::DropdownArrow: + return "dropdown_arrow"; case UIEditorEnumFieldHitTargetKind::ValueBox: return "value_box"; case UIEditorEnumFieldHitTargetKind::Row: @@ -361,6 +387,14 @@ private: m_captureRoot = ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/enum_field_basic/captures"; m_autoScreenshot.Initialize(m_captureRoot); + const auto themeLoad = + XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath()); + if (themeLoad.succeeded) { + m_theme = themeLoad.theme; + m_themeStatus = "loaded"; + } else { + m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error; + } ResetScenario(); return true; @@ -386,7 +420,20 @@ private: GetClientRect(m_hwnd, &clientRect); const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); - return BuildScenarioLayout(width, height); + return BuildScenarioLayout( + width, + height, + XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme)); + } + + UIRect GetViewportRect() const { + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + return UIRect( + 0.0f, + 0.0f, + static_cast((std::max)(1L, clientRect.right - clientRect.left)), + static_cast((std::max)(1L, clientRect.bottom - clientRect.top))); } void ResetScenario() { @@ -410,14 +457,19 @@ private: return; } - m_spec.selectedIndex = m_selectedIndex; const ScenarioLayout layout = GetLayout(); + const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme); + const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme); + m_spec.selectedIndex = m_selectedIndex; m_frame = UpdateUIEditorEnumFieldInteraction( m_interactionState, m_selectedIndex, layout.fieldRect, m_spec, - {}); + {}, + fieldMetrics, + popupMetrics, + GetViewportRect()); m_spec.selectedIndex = m_selectedIndex; } @@ -511,13 +563,18 @@ private: UIEditorEnumFieldInteractionResult PumpEvents(std::vector events) { const ScenarioLayout layout = GetLayout(); + const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme); + const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme); m_spec.selectedIndex = m_selectedIndex; m_frame = UpdateUIEditorEnumFieldInteraction( m_interactionState, m_selectedIndex, layout.fieldRect, m_spec, - std::move(events)); + std::move(events), + fieldMetrics, + popupMetrics, + GetViewportRect()); m_spec.selectedIndex = m_selectedIndex; return m_frame.result; } @@ -527,6 +584,14 @@ private: m_lastResult = std::string("已切换到: ") + m_spec.options[m_selectedIndex]; return; } + if (result.popupOpened) { + m_lastResult = "下拉菜单已展开"; + return; + } + if (result.popupClosed) { + m_lastResult = "下拉菜单已关闭"; + return; + } if (result.consumed) { m_lastResult = "控件已消费输入"; return; @@ -534,6 +599,16 @@ private: m_lastResult = "等待交互"; } + std::string ResolveHighlightedText() const { + if (!m_frame.popupOpen || + m_frame.popupState.hoveredIndex == UIEditorMenuPopupInvalidIndex || + m_frame.popupState.hoveredIndex >= m_spec.options.size()) { + return "(none)"; + } + + return m_spec.options[m_frame.popupState.hoveredIndex]; + } + void ExecuteAction(ActionId action) { switch (action) { case ActionId::Reset: @@ -551,77 +626,103 @@ private: return; } - RECT clientRect = {}; - GetClientRect(m_hwnd, &clientRect); - const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); - const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); - const ScenarioLayout layout = BuildScenarioLayout(width, height); + const UIRect viewportRect = GetViewportRect(); + const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme); + const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme); + const ScenarioLayout layout = BuildScenarioLayout(viewportRect.width, viewportRect.height, shellMetrics); RefreshFrame(); const UIEditorEnumFieldHitTarget currentHit = HitTestUIEditorEnumField(m_frame.layout, m_mousePosition); + const auto enumMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme); + const auto enumPalette = XCEngine::UI::Editor::ResolveUIEditorEnumFieldPalette(m_theme); + const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme); + const auto popupPalette = XCEngine::UI::Editor::ResolveUIEditorMenuPopupPalette(m_theme); UIDrawData drawData = {}; UIDrawList& drawList = drawData.EmplaceDrawList("EditorEnumFieldBasic"); - drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); + drawList.AddFilledRect(viewportRect, shellPalette.windowBackground); DrawCard( drawList, layout.introRect, + shellPalette, + shellMetrics, "这个测试在验证什么功能", - "只验证 Editor EnumField 基础控件,不涉及 PropertyGrid 或业务 Inspector。"); + "只验证 Editor EnumField 的基础交互契约,不涉及 PropertyGrid 或业务 Inspector。"); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f), - "1. 点击 < / > 按钮,检查枚举选项是否稳定切换。", - kTextPrimary, - 12.0f); + "1. 点击 value box 或 dropdown arrow,检查下拉菜单是否展开/收起。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), - "2. 控件获得 focus 后按 Left / Right / Home / End,检查键盘切换。", - kTextPrimary, - 12.0f); + "2. 展开后 hover 列表项,检查高亮是否稳定跟随。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), - "3. 检查 Hover / Focus / Selected / Result 是否同步更新。", - kTextPrimary, - 12.0f); + "3. 获得 focus 后按 Up / Down / Home / End,再按 Enter / Space 选中,Esc 关闭。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), - "4. 这个场景只覆盖基础交互契约,不提前承载任何业务面板。", - kTextPrimary, - 12.0f); + "4. 检查 Hover / Popup / Highlight / Selected / Result 是否同步更新。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f), + "5. 按 F12 或点击截图按钮,确认自动截图路径正确。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); - DrawCard(drawList, layout.controlRect, "操作"); + DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作"); for (const ButtonLayout& button : layout.buttons) { - DrawButton(drawList, button, m_hasHoveredAction && m_hoveredAction == button.action); + DrawButton( + drawList, + button, + shellPalette, + shellMetrics, + m_hasHoveredAction && m_hoveredAction == button.action); } - DrawCard(drawList, layout.stateRect, "状态摘要"); + DrawCard( + drawList, + layout.stateRect, + shellPalette, + shellMetrics, + "状态摘要", + "重点检查 hit / popup / highlight / selected / result。"); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f), "Hover: " + DescribeHitTarget(currentHit), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f), std::string("Focused: ") + (m_interactionState.fieldState.focused ? "是" : "否"), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f), - std::string("Active: ") + (m_interactionState.fieldState.active ? "是" : "否"), - kTextPrimary, - 12.0f); + std::string("Popup: ") + (m_frame.popupOpen ? "open" : "closed"), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f), - "Selected: " + ResolveUIEditorEnumFieldValueText(m_spec), - kTextPrimary, - 12.0f); + "Highlight: " + ResolveHighlightedText(), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f), + "Selected: " + ResolveUIEditorEnumFieldValueText(m_spec), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f), "Result: " + m_lastResult, - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); const std::string captureSummary = m_autoScreenshot.HasPendingCapture() @@ -630,24 +731,46 @@ private: ? std::string("F12 -> tests/UI/Editor/integration/shell/enum_field_basic/captures/") : m_autoScreenshot.GetLastCaptureSummary()); drawList.AddText( - UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f), + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f), captureSummary, - kTextWeak, - 12.0f); + shellPalette.textWeak, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f), + "Theme: " + m_themeStatus, + shellPalette.textWeak, + shellMetrics.bodyFontSize); - DrawCard(drawList, layout.previewRect, "EnumField 预览", "这里只放一个 EnumField。"); + DrawCard( + drawList, + layout.previewRect, + shellPalette, + shellMetrics, + "EnumField 预览", + "这里只放一个 Unity 风格 EnumField。"); AppendUIEditorEnumField( drawList, layout.fieldRect, m_spec, - m_interactionState.fieldState); + m_interactionState.fieldState, + enumPalette, + enumMetrics); + if (m_frame.popupOpen) { + AppendUIEditorMenuPopup( + drawList, + m_frame.popupLayout.popupRect, + m_frame.popupItems, + m_frame.popupState, + popupPalette, + popupMetrics); + } const bool framePresented = m_renderer.Render(drawData); m_autoScreenshot.CaptureIfRequested( m_renderer, drawData, - static_cast(width), - static_cast(height), + static_cast(viewportRect.width), + static_cast(viewportRect.height), framePresented); } @@ -660,10 +783,12 @@ private: std::size_t m_selectedIndex = 0u; UIEditorEnumFieldInteractionState m_interactionState = {}; UIEditorEnumFieldInteractionFrame m_frame = {}; + Style::UITheme m_theme = {}; UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); ActionId m_hoveredAction = ActionId::Reset; bool m_hasHoveredAction = false; std::string m_lastResult = {}; + std::string m_themeStatus = "fallback"; }; } // namespace diff --git a/tests/UI/Editor/integration/shell/number_field_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/number_field_basic/CMakeLists.txt index 599277b1..92860fac 100644 --- a/tests/UI/Editor/integration/shell/number_field_basic/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/number_field_basic/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(editor_ui_number_field_basic_validation WIN32 ) target_include_directories(editor_ui_number_field_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src ${CMAKE_SOURCE_DIR}/new_editor/include ${CMAKE_SOURCE_DIR}/new_editor/app ${CMAKE_SOURCE_DIR}/engine/include diff --git a/tests/UI/Editor/integration/shell/number_field_basic/main.cpp b/tests/UI/Editor/integration/shell/number_field_basic/main.cpp index b0d56790..2e10f757 100644 --- a/tests/UI/Editor/integration/shell/number_field_basic/main.cpp +++ b/tests/UI/Editor/integration/shell/number_field_basic/main.cpp @@ -3,7 +3,9 @@ #endif #include +#include #include +#include "EditorValidationTheme.h" #include "Host/AutoScreenshot.h" #include "Host/NativeRenderer.h" @@ -46,19 +48,11 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField; using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTarget; using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTargetKind; using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec; +namespace Style = XCEngine::UI::Style; constexpr const wchar_t* kWindowClassName = L"XCUIEditorNumberFieldBasicValidation"; constexpr const wchar_t* kWindowTitle = L"XCUI Editor | NumberField Basic"; -constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f); -constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f); -constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f); -constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f); -constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f); -constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f); -constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f); -constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f); - enum class ActionId : unsigned char { Reset = 0, Capture @@ -75,6 +69,9 @@ struct ScenarioLayout { UIRect controlRect = {}; UIRect stateRect = {}; UIRect previewRect = {}; + UIRect inspectorRect = {}; + UIRect inspectorHeaderRect = {}; + UIRect sectionRect = {}; UIRect fieldRect = {}; std::vector buttons = {}; }; @@ -87,6 +84,11 @@ std::filesystem::path ResolveRepoRootPath() { return std::filesystem::path(root).lexically_normal(); } +std::filesystem::path ResolveValidationThemePath() { + return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme") + .lexically_normal(); +} + bool ContainsPoint(const UIRect& rect, float x, float y) { return x >= rect.x && x <= rect.x + rect.width && @@ -117,13 +119,16 @@ std::int32_t MapNumberFieldKey(UINT keyCode) { } } -ScenarioLayout BuildScenarioLayout(float width, float height) { - constexpr float margin = 20.0f; - constexpr float leftWidth = 430.0f; - constexpr float gap = 16.0f; +ScenarioLayout BuildScenarioLayout( + float width, + float height, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) { + const float margin = shellMetrics.margin; + constexpr float leftWidth = 460.0f; + const float gap = shellMetrics.gap; ScenarioLayout layout = {}; - layout.introRect = UIRect(margin, margin, leftWidth, 232.0f); + layout.introRect = UIRect(margin, margin, leftWidth, 250.0f); layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f); layout.stateRect = UIRect( margin, @@ -135,11 +140,26 @@ ScenarioLayout BuildScenarioLayout(float width, float height) { margin, (std::max)(420.0f, width - leftWidth - margin * 3.0f), height - margin * 2.0f); + layout.inspectorRect = UIRect( + layout.previewRect.x + 18.0f, + layout.previewRect.y + 54.0f, + (std::min)(392.0f, layout.previewRect.width - 36.0f), + 150.0f); + layout.inspectorHeaderRect = UIRect( + layout.inspectorRect.x, + layout.inspectorRect.y, + layout.inspectorRect.width, + 24.0f); + layout.sectionRect = UIRect( + layout.inspectorRect.x, + layout.inspectorRect.y + layout.inspectorHeaderRect.height, + layout.inspectorRect.width, + 24.0f); layout.fieldRect = UIRect( - layout.previewRect.x + 24.0f, - layout.previewRect.y + 82.0f, - 320.0f, - 32.0f); + layout.inspectorRect.x, + layout.sectionRect.y + layout.sectionRect.height + 2.0f, + layout.inspectorRect.width, + 22.0f); const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f; const float buttonY = layout.controlRect.y + 32.0f; @@ -150,34 +170,63 @@ ScenarioLayout BuildScenarioLayout(float width, float height) { return layout; } +XCEngine::UI::Editor::Widgets::UIEditorNumberFieldMetrics ResolveHostedNumberFieldMetrics( + const Style::UITheme& theme) { + const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(theme); + const auto numberMetrics = XCEngine::UI::Editor::ResolveUIEditorNumberFieldMetrics(theme); + return XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldMetrics(propertyMetrics, numberMetrics); +} + +XCEngine::UI::Editor::Widgets::UIEditorNumberFieldPalette ResolveHostedNumberFieldPalette( + const Style::UITheme& theme) { + const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(theme); + const auto numberPalette = XCEngine::UI::Editor::ResolveUIEditorNumberFieldPalette(theme); + return XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldPalette(propertyPalette, numberPalette); +} + void DrawCard( UIDrawList& drawList, const UIRect& rect, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, std::string_view title, std::string_view subtitle = {}) { - drawList.AddFilledRect(rect, kCardBg, 10.0f); - drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f); - drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f); + drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius); + drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 14.0f), + std::string(title), + shellPalette.textPrimary, + shellMetrics.titleFontSize); if (!subtitle.empty()) { - drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 40.0f), + std::string(subtitle), + shellPalette.textMuted, + shellMetrics.bodyFontSize); } } void DrawButton( UIDrawList& drawList, const ButtonLayout& button, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, bool hovered) { - drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f); - drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f); - drawList.AddText(UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f); + drawList.AddFilledRect( + button.rect, + hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground, + shellMetrics.buttonRadius); + drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius); + drawList.AddText( + UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), + button.label, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); } std::string DescribeHitTarget(const UIEditorNumberFieldHitTarget& hitTarget) { switch (hitTarget.kind) { - case UIEditorNumberFieldHitTargetKind::DecrementButton: - return "decrement"; - case UIEditorNumberFieldHitTargetKind::IncrementButton: - return "increment"; case UIEditorNumberFieldHitTargetKind::ValueBox: return "value_box"; case UIEditorNumberFieldHitTargetKind::Row: @@ -381,6 +430,14 @@ private: m_captureRoot = ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/number_field_basic/captures"; m_autoScreenshot.Initialize(m_captureRoot); + const auto themeLoad = + XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath()); + if (themeLoad.succeeded) { + m_theme = themeLoad.theme; + m_themeStatus = "loaded"; + } else { + m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error; + } ResetScenario(); return true; @@ -406,7 +463,10 @@ private: GetClientRect(m_hwnd, &clientRect); const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); - return BuildScenarioLayout(width, height); + return BuildScenarioLayout( + width, + height, + XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme)); } void ResetScenario() { @@ -434,11 +494,13 @@ private: } const ScenarioLayout layout = GetLayout(); + const auto metrics = ResolveHostedNumberFieldMetrics(m_theme); m_frame = UpdateUIEditorNumberFieldInteraction( m_interactionState, m_spec, layout.fieldRect, - {}); + {}, + metrics); } void OnResize(UINT width, UINT height) { @@ -542,17 +604,19 @@ private: UIEditorNumberFieldInteractionResult PumpEvents(std::vector events) { const ScenarioLayout layout = GetLayout(); + const auto metrics = ResolveHostedNumberFieldMetrics(m_theme); m_frame = UpdateUIEditorNumberFieldInteraction( m_interactionState, m_spec, layout.fieldRect, - std::move(events)); + std::move(events), + metrics); return m_frame.result; } void UpdateResultText(const UIEditorNumberFieldInteractionResult& result) { if (result.editCommitRejected) { - m_lastResult = "提交失败,仍保留在编辑态"; + m_lastResult = "提交失败,仍保持在编辑态"; return; } if (result.editCommitted) { @@ -600,83 +664,101 @@ private: GetClientRect(m_hwnd, &clientRect); const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); - const ScenarioLayout layout = BuildScenarioLayout(width, height); + const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme); + const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme); + const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics); RefreshFrame(); const UIEditorNumberFieldHitTarget currentHit = HitTestUIEditorNumberField(m_frame.layout, m_mousePosition); + const auto numberMetrics = ResolveHostedNumberFieldMetrics(m_theme); + const auto numberPalette = ResolveHostedNumberFieldPalette(m_theme); + const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme); UIDrawData drawData = {}; UIDrawList& drawList = drawData.EmplaceDrawList("EditorNumberFieldBasic"); - drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground); DrawCard( drawList, layout.introRect, + shellPalette, + shellMetrics, "这个测试在验证什么功能", - "只验证 Editor NumberField 的基础交互契约,不涉及 PropertyGrid 或任何业务 Inspector。"); + "验证 Inspector 宿主中的 NumberField 交互契约和默认宿主风格。"); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f), - "1. 点击 +/- 按钮,检查步进和上下界钳制是否稳定。", - kTextPrimary, - 12.0f); + "1. 点击 value box,检查是否进入编辑态,外观应是 Unity 风格单输入框。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), - "2. 控件获得 focus 后,按 Left / Right / Up / Down / Home / End,检查键盘步进。", - kTextPrimary, - 12.0f); + "2. 获得 focus 后按 Left / Right / Up / Down / Home / End,检查键盘步进。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), "3. 按 Enter 进入编辑态,直接输入字符,Enter commit,Esc cancel。", - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), "4. 检查 Hover / Focus / Editing / Value / Result 是否同步更新。", - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f), "5. 按 F12 或点击截图按钮,确认自动截图路径正确。", - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); - DrawCard(drawList, layout.controlRect, "操作"); + DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作"); for (const ButtonLayout& button : layout.buttons) { - DrawButton(drawList, button, m_hasHoveredAction && m_hoveredAction == button.action); + DrawButton( + drawList, + button, + shellPalette, + shellMetrics, + m_hasHoveredAction && m_hoveredAction == button.action); } - DrawCard(drawList, layout.stateRect, "状态摘要", "重点检查 hit / focus / editing / value / result。"); + DrawCard( + drawList, + layout.stateRect, + shellPalette, + shellMetrics, + "状态摘要", + "重点检查 hit / focus / editing / value / result。"); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f), "Hover: " + DescribeHitTarget(currentHit), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f), std::string("Focused: ") + (m_interactionState.numberFieldState.focused ? "是" : "否"), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f), std::string("Editing: ") + (m_interactionState.numberFieldState.editing ? "是" : "否"), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f), "Value: " + FormatUIEditorNumberFieldValue(m_spec), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f), "Display: " + m_interactionState.numberFieldState.displayText, - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f), "Result: " + m_lastResult, - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); const std::string captureSummary = m_autoScreenshot.HasPendingCapture() @@ -687,15 +769,44 @@ private: drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f), captureSummary, - kTextWeak, - 12.0f); + shellPalette.textWeak, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f), + "Theme: " + m_themeStatus, + shellPalette.textWeak, + shellMetrics.bodyFontSize); - DrawCard(drawList, layout.previewRect, "NumberField 预览", "这里只放一个 NumberField。"); + DrawCard( + drawList, + layout.previewRect, + shellPalette, + shellMetrics, + "NumberField 预览", + "这里仅预览 Inspector 宿主中的 Unity 风格 Number 字段。"); + drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor); + drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f); + drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground); + drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f); + drawList.AddText( + UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f), + "Inspector", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor); + drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f); + drawList.AddText( + UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f), + "v Transform", + propertyPalette.sectionTextColor, + shellMetrics.bodyFontSize); AppendUIEditorNumberField( drawList, layout.fieldRect, m_spec, - m_interactionState.numberFieldState); + m_interactionState.numberFieldState, + numberPalette, + numberMetrics); const bool framePresented = m_renderer.Render(drawData); m_autoScreenshot.CaptureIfRequested( @@ -714,10 +825,12 @@ private: UIEditorNumberFieldSpec m_spec = {}; UIEditorNumberFieldInteractionState m_interactionState = {}; UIEditorNumberFieldInteractionFrame m_frame = {}; + Style::UITheme m_theme = {}; UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); ActionId m_hoveredAction = ActionId::Reset; bool m_hasHoveredAction = false; std::string m_lastResult = {}; + std::string m_themeStatus = "fallback"; }; } // namespace diff --git a/tests/UI/Editor/integration/shell/property_grid_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/property_grid_basic/CMakeLists.txt index ad5b11c0..3950119c 100644 --- a/tests/UI/Editor/integration/shell/property_grid_basic/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/property_grid_basic/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(editor_ui_property_grid_basic_validation WIN32 ) target_include_directories(editor_ui_property_grid_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src ${CMAKE_SOURCE_DIR}/new_editor/include ${CMAKE_SOURCE_DIR}/new_editor/app ${CMAKE_SOURCE_DIR}/engine/include diff --git a/tests/UI/Editor/integration/shell/property_grid_basic/main.cpp b/tests/UI/Editor/integration/shell/property_grid_basic/main.cpp index 02416eaf..cea7a6ef 100644 --- a/tests/UI/Editor/integration/shell/property_grid_basic/main.cpp +++ b/tests/UI/Editor/integration/shell/property_grid_basic/main.cpp @@ -3,7 +3,9 @@ #endif #include +#include #include +#include "EditorValidationTheme.h" #include "Host/AutoScreenshot.h" #include "Host/NativeRenderer.h" @@ -48,24 +50,17 @@ using XCEngine::UI::Editor::UpdateUIEditorPropertyGridInteraction; using XCEngine::UI::Editor::Widgets::AppendUIEditorPropertyGridBackground; using XCEngine::UI::Editor::Widgets::AppendUIEditorPropertyGridForeground; using XCEngine::UI::Editor::Widgets::HitTestUIEditorPropertyGrid; +using XCEngine::UI::Editor::Widgets::ResolveUIEditorPropertyGridFieldValueText; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField; +using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTarget; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTargetKind; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection; +namespace Style = XCEngine::UI::Style; constexpr const wchar_t* kWindowClassName = L"XCUIEditorPropertyGridBasicValidation"; constexpr const wchar_t* kWindowTitle = L"XCUI Editor | PropertyGrid Basic"; -constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f); -constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f); -constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f); -constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f); -constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f); -constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f); -constexpr UIColor kTextSuccess(0.63f, 0.76f, 0.63f, 1.0f); -constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f); -constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f); - enum class ActionId : unsigned char { Reset = 0, Capture @@ -95,6 +90,11 @@ std::filesystem::path ResolveRepoRootPath() { return std::filesystem::path(root).lexically_normal(); } +std::filesystem::path ResolveValidationThemePath() { + return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme") + .lexically_normal(); +} + bool ContainsPoint(const UIRect& rect, float x, float y) { return x >= rect.x && x <= rect.x + rect.width && @@ -129,10 +129,13 @@ std::int32_t MapEditorKey(UINT keyCode) { } } -ScenarioLayout BuildScenarioLayout(float width, float height) { - constexpr float margin = 20.0f; +ScenarioLayout BuildScenarioLayout( + float width, + float height, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) { + const float margin = shellMetrics.margin; constexpr float leftWidth = 430.0f; - constexpr float gap = 16.0f; + const float gap = shellMetrics.gap; ScenarioLayout layout = {}; layout.introRect = UIRect(margin, margin, leftWidth, 214.0f); @@ -166,48 +169,109 @@ ScenarioLayout BuildScenarioLayout(float width, float height) { void DrawCard( UIDrawList& drawList, const UIRect& rect, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, std::string_view title, std::string_view subtitle = {}) { - drawList.AddFilledRect(rect, kCardBg, 10.0f); - drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f); - drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f); + drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius); + drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 14.0f), + std::string(title), + shellPalette.textPrimary, + shellMetrics.titleFontSize); if (!subtitle.empty()) { - drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 40.0f), + std::string(subtitle), + shellPalette.textMuted, + shellMetrics.bodyFontSize); } } void DrawButton( UIDrawList& drawList, const ButtonLayout& button, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, bool hovered) { - drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f); - drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f); + drawList.AddFilledRect( + button.rect, + hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground, + shellMetrics.buttonRadius); + drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius); drawList.AddText( UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), button.label, - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); +} + +UIEditorPropertyGridField MakeTextField( + std::string id, + std::string label, + std::string value, + bool readOnly = false) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.valueText = std::move(value); + field.readOnly = readOnly; + return field; +} + +UIEditorPropertyGridField MakeBoolField( + std::string id, + std::string label, + bool value) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Bool; + field.boolValue = value; + return field; +} + +UIEditorPropertyGridField MakeNumberField( + std::string id, + std::string label, + double value) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Number; + field.numberValue.value = value; + field.numberValue.step = 1.0; + field.numberValue.minValue = 0.0; + field.numberValue.maxValue = 5000.0; + field.numberValue.integerMode = true; + return field; +} + +UIEditorPropertyGridField MakeEnumField( + std::string id, + std::string label, + std::vector options, + std::size_t selectedIndex) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Enum; + field.enumValue.options = std::move(options); + field.enumValue.selectedIndex = selectedIndex; + return field; } std::vector BuildSections() { return { { - "transform", - "Transform", + "inspector", + "Inspector", { - { "position", "Position", "0, 0, 0", false, 0.0f }, - { "rotation", "Rotation", "0, 45, 0", false, 0.0f }, - { "scale", "Scale", "1, 1, 1", false, 0.0f } - }, - 0.0f - }, - { - "material", - "Material", - { - { "shader", "Shader", "Standard/Lit", false, 0.0f }, - { "queue", "Render Queue", "2000", false, 0.0f }, - { "guid", "GUID", "asset-guid-001", true, 0.0f } + MakeBoolField("enabled", "Enabled", true), + MakeNumberField("render_queue", "Render Queue", 2000.0), + MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u), + MakeTextField("tag", "Tag", "Player") }, 0.0f }, @@ -215,8 +279,7 @@ std::vector BuildSections() { "metadata", "Metadata", { - { "tag", "Tag", "", false, 0.0f }, - { "layer", "Layer", "Default", false, 0.0f } + MakeTextField("guid", "GUID", "asset-guid-001", true) }, 0.0f } @@ -246,8 +309,7 @@ std::string DescribeHitTarget( std::string BuildExpandedSummary(const UIExpansionModel& expansionModel) { std::ostringstream stream = {}; - stream << (expansionModel.IsExpanded("transform") ? "Transform" : "-"); - stream << " / " << (expansionModel.IsExpanded("material") ? "Material" : "-"); + stream << (expansionModel.IsExpanded("inspector") ? "Inspector" : "-"); stream << " / " << (expansionModel.IsExpanded("metadata") ? "Metadata" : "-"); return stream.str(); } @@ -467,6 +529,14 @@ private: m_captureRoot = ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/property_grid_basic/captures"; m_autoScreenshot.Initialize(m_captureRoot); + const auto themeLoad = + XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath()); + if (themeLoad.succeeded) { + m_theme = themeLoad.theme; + m_themeStatus = "loaded"; + } else { + m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error; + } ResetScenario(); return true; @@ -492,16 +562,18 @@ private: GetClientRect(m_hwnd, &clientRect); const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); - return BuildScenarioLayout(width, height); + return BuildScenarioLayout( + width, + height, + XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme)); } void ResetScenario() { m_sections = BuildSections(); m_selectionModel = {}; - m_selectionModel.SetSelection("rotation"); + m_selectionModel.SetSelection("render_queue"); m_expansionModel = {}; - m_expansionModel.Expand("transform"); - m_expansionModel.Expand("material"); + m_expansionModel.Expand("inspector"); m_expansionModel.Expand("metadata"); m_propertyEditModel = {}; m_interactionState = {}; @@ -520,6 +592,7 @@ private: } const ScenarioLayout layout = GetLayout(); + const auto metrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme); m_gridFrame = UpdateUIEditorPropertyGridInteraction( m_interactionState, @@ -528,23 +601,16 @@ private: m_propertyEditModel, layout.gridRect, m_sections, - {}); + {}, + metrics); } - void ApplyCommittedValue(const UIEditorPropertyGridInteractionResult& result) { - if (!result.editCommitted || result.committedFieldId.empty()) { + void UpdateLastCommit(const UIEditorPropertyGridInteractionResult& result) { + if (!result.fieldValueChanged || result.changedFieldId.empty()) { return; } - for (UIEditorPropertyGridSection& section : m_sections) { - for (UIEditorPropertyGridField& field : section.fields) { - if (field.fieldId == result.committedFieldId) { - field.valueText = result.committedValue; - m_lastCommit = result.committedFieldId + " = " + result.committedValue; - return; - } - } - } + m_lastCommit = result.changedFieldId + " = " + result.changedValue; } void OnResize(UINT width, UINT height) { @@ -661,6 +727,7 @@ private: UIEditorPropertyGridInteractionResult PumpGridEvents(std::vector events) { const ScenarioLayout layout = GetLayout(); + const auto metrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme); m_gridFrame = UpdateUIEditorPropertyGridInteraction( m_interactionState, @@ -669,8 +736,9 @@ private: m_propertyEditModel, layout.gridRect, m_sections, - std::move(events)); - ApplyCommittedValue(m_gridFrame.result); + std::move(events), + metrics); + UpdateLastCommit(m_gridFrame.result); return m_gridFrame.result; } @@ -678,6 +746,26 @@ private: const UIEditorPropertyGridInteractionResult& result, bool wasFocused, bool insideGrid) { + if (result.popupOpened && !m_interactionState.propertyGridState.popupFieldId.empty()) { + m_lastResult = "打开枚举下拉: " + m_interactionState.propertyGridState.popupFieldId; + return; + } + + if (result.popupClosed && !result.fieldValueChanged) { + m_lastResult = "关闭枚举下拉"; + return; + } + + if (result.fieldValueChanged) { + m_lastResult = "字段已更新: " + result.changedFieldId + " = " + result.changedValue; + return; + } + + if (result.editCommitRejected && !result.activeFieldId.empty()) { + m_lastResult = "提交被拒绝: " + result.activeFieldId; + return; + } + if (result.editCommitted) { m_lastResult = "提交字段: " + result.committedFieldId + " = " + result.committedValue; return; @@ -753,99 +841,124 @@ private: GetClientRect(m_hwnd, &clientRect); const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); - const ScenarioLayout layout = BuildScenarioLayout(width, height); + const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme); + const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme); + const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics); RefreshGridFrame(); const UIEditorPropertyGridHitTarget currentHit = HitTestUIEditorPropertyGrid(m_gridFrame.layout, m_mousePosition); + const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme); + const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme); + const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme); + const auto popupPalette = XCEngine::UI::Editor::ResolveUIEditorMenuPopupPalette(m_theme); UIDrawData drawData = {}; UIDrawList& drawList = drawData.EmplaceDrawList("EditorPropertyGridBasic"); - drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground); DrawCard( drawList, layout.introRect, + shellPalette, + shellMetrics, "这个测试在验证什么功能", - "只验证 Editor PropertyGrid 基础控件,不涉及任何 Inspector 业务逻辑。"); + "只验证 Editor PropertyGrid 作为 typed 属性宿主的基础契约,不涉及任何 Inspector 业务逻辑。"); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f), "1. 点击 section header,检查展开/折叠是否稳定,字段布局不能歪。", - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), - "2. 点击 field row 只切换 selection;点击 value box 才进入编辑态。", - kTextPrimary, - 12.0f); + "2. 点击 value host:Bool toggle、Number/Text edit、Enum popup。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), - "3. 编辑态输入字符,按 Enter commit,按 Esc cancel,read-only 字段不能进编辑。", - kTextPrimary, - 12.0f); + "3. Number / Text 编辑后按 Enter commit、Esc cancel;GUID 只读。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), "4. Grid 获得 focus 后按 Up / Down / Home / End,检查字段导航和 selection 同步。", - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f), "5. 按 F12 手动截图;设置 XCUI_AUTO_CAPTURE_ON_STARTUP=1 可自动截图。", - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); - DrawCard(drawList, layout.controlRect, "操作"); + DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作"); for (const ButtonLayout& button : layout.buttons) { DrawButton( drawList, button, + shellPalette, + shellMetrics, m_hasHoveredAction && m_hoveredAction == button.action); } - DrawCard(drawList, layout.stateRect, "状态摘要", "重点检查 hit / focus / selection / edit / commit。"); + DrawCard( + drawList, + layout.stateRect, + shellPalette, + shellMetrics, + "状态摘要", + "重点检查 hit / focus / selection / edit / popup / commit。"); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f), "Hover: " + DescribeHitTarget(currentHit, m_sections), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f), std::string("Focused: ") + (m_interactionState.propertyGridState.focused ? "开" : "关"), - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f), "Selected: " + (m_selectionModel.HasSelection() ? m_selectionModel.GetSelectedId() : std::string("(none)")), - kTextSuccess, - 12.0f); + shellPalette.textSuccess, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f), "Active Edit: " + (m_propertyEditModel.HasActiveEdit() ? m_propertyEditModel.GetActiveFieldId() : std::string("(none)")), - kTextMuted, - 12.0f); + shellPalette.textMuted, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f), "Staged: " + (m_propertyEditModel.HasActiveEdit() ? m_propertyEditModel.GetStagedValue() : std::string("(none)")), - kTextMuted, - 12.0f); + shellPalette.textMuted, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f), - "Expanded: " + BuildExpandedSummary(m_expansionModel), - kTextMuted, - 12.0f); + "Popup: " + + (m_interactionState.propertyGridState.popupFieldId.empty() + ? std::string("(none)") + : (m_interactionState.propertyGridState.popupFieldId + " / index " + + std::to_string(m_interactionState.propertyGridState.popupHighlightedIndex))), + shellPalette.textMuted, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f), - "Last Commit: " + m_lastCommit, - kTextPrimary, - 12.0f); + "Expanded: " + BuildExpandedSummary(m_expansionModel), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); drawList.AddText( UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f), + "Last Commit: " + m_lastCommit, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f), "Result: " + m_lastResult, - kTextPrimary, - 12.0f); + shellPalette.textPrimary, + shellMetrics.bodyFontSize); const std::string captureSummary = m_autoScreenshot.HasPendingCapture() @@ -854,24 +967,42 @@ private: ? std::string("F12 -> tests/UI/Editor/integration/shell/property_grid_basic/captures/") : m_autoScreenshot.GetLastCaptureSummary()); drawList.AddText( - UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f), + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 286.0f), captureSummary, - kTextWeak, - 12.0f); + shellPalette.textWeak, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 310.0f), + "Theme: " + m_themeStatus, + shellPalette.textWeak, + shellMetrics.bodyFontSize); - DrawCard(drawList, layout.previewRect, "PropertyGrid 预览", "这里只放一个 PropertyGrid,不混入任何业务面板。"); + DrawCard( + drawList, + layout.previewRect, + shellPalette, + shellMetrics, + "PropertyGrid 预览", + "这里只放一个 PropertyGrid,用来验证 typed 属性宿主。"); AppendUIEditorPropertyGridBackground( drawList, m_gridFrame.layout, m_sections, m_selectionModel, m_propertyEditModel, - m_interactionState.propertyGridState); + m_interactionState.propertyGridState, + propertyPalette, + propertyMetrics); AppendUIEditorPropertyGridForeground( drawList, m_gridFrame.layout, m_sections, - m_propertyEditModel); + m_interactionState.propertyGridState, + m_propertyEditModel, + propertyPalette, + propertyMetrics, + popupPalette, + popupMetrics); const bool framePresented = m_renderer.Render(drawData); m_autoScreenshot.CaptureIfRequested( @@ -893,11 +1024,13 @@ private: UIPropertyEditModel m_propertyEditModel = {}; UIEditorPropertyGridInteractionState m_interactionState = {}; UIEditorPropertyGridInteractionFrame m_gridFrame = {}; + Style::UITheme m_theme = {}; UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); ActionId m_hoveredAction = ActionId::Reset; bool m_hasHoveredAction = false; std::string m_lastResult = {}; std::string m_lastCommit = {}; + std::string m_themeStatus = "fallback"; }; } // namespace diff --git a/tests/UI/Editor/integration/shell/text_field_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/text_field_basic/CMakeLists.txt new file mode 100644 index 00000000..86d99a62 --- /dev/null +++ b/tests/UI/Editor/integration/shell/text_field_basic/CMakeLists.txt @@ -0,0 +1,31 @@ +add_executable(editor_ui_text_field_basic_validation WIN32 + main.cpp +) + +target_include_directories(editor_ui_text_field_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src + ${CMAKE_SOURCE_DIR}/new_editor/include + ${CMAKE_SOURCE_DIR}/new_editor/app + ${CMAKE_SOURCE_DIR}/engine/include +) + +target_compile_definitions(editor_ui_text_field_basic_validation PRIVATE + UNICODE + _UNICODE + XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}" +) + +if(MSVC) + target_compile_options(editor_ui_text_field_basic_validation PRIVATE /utf-8 /FS) + set_property(TARGET editor_ui_text_field_basic_validation PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +target_link_libraries(editor_ui_text_field_basic_validation PRIVATE + XCUIEditorLib + XCUIEditorHost +) + +set_target_properties(editor_ui_text_field_basic_validation PROPERTIES + OUTPUT_NAME "XCUIEditorTextFieldBasicValidation" +) diff --git a/tests/UI/Editor/integration/shell/text_field_basic/main.cpp b/tests/UI/Editor/integration/shell/text_field_basic/main.cpp new file mode 100644 index 00000000..7d9707de --- /dev/null +++ b/tests/UI/Editor/integration/shell/text_field_basic/main.cpp @@ -0,0 +1,842 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include +#include "EditorValidationTheme.h" +#include "Host/AutoScreenshot.h" +#include "Host/NativeRenderer.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT +#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "." +#endif + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Host::AutoScreenshotController; +using XCEngine::UI::Editor::Host::NativeRenderer; +using XCEngine::UI::Editor::UIEditorTextFieldInteractionFrame; +using XCEngine::UI::Editor::UIEditorTextFieldInteractionResult; +using XCEngine::UI::Editor::UIEditorTextFieldInteractionState; +using XCEngine::UI::Editor::UpdateUIEditorTextFieldInteraction; +using XCEngine::UI::Editor::Widgets::AppendUIEditorTextField; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField; +using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTarget; +using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec; +namespace Style = XCEngine::UI::Style; + +constexpr const wchar_t* kWindowClassName = L"XCUIEditorTextFieldBasicValidation"; +constexpr const wchar_t* kWindowTitle = L"XCUI Editor | TextField Basic"; + +enum class ActionId : unsigned char { + Reset = 0, + Capture +}; + +struct ButtonLayout { + ActionId action = ActionId::Reset; + const char* label = ""; + UIRect rect = {}; +}; + +struct ScenarioLayout { + UIRect introRect = {}; + UIRect controlRect = {}; + UIRect stateRect = {}; + UIRect previewRect = {}; + UIRect inspectorRect = {}; + UIRect inspectorHeaderRect = {}; + UIRect sectionRect = {}; + UIRect fieldRect = {}; + std::vector buttons = {}; +}; + +std::filesystem::path ResolveRepoRootPath() { + std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT; + if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { + root = root.substr(1u, root.size() - 2u); + } + + return std::filesystem::path(root).lexically_normal(); +} + +std::filesystem::path ResolveValidationThemePath() { + return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme") + .lexically_normal(); +} + +bool ContainsPoint(const UIRect& rect, float x, float y) { + return x >= rect.x && + x <= rect.x + rect.width && + y >= rect.y && + y <= rect.y + rect.height; +} + +std::int32_t MapTextFieldKey(UINT keyCode) { + switch (keyCode) { + case VK_RETURN: + return static_cast(KeyCode::Enter); + case VK_ESCAPE: + return static_cast(KeyCode::Escape); + default: + return static_cast(KeyCode::None); + } +} + +ScenarioLayout BuildScenarioLayout( + float width, + float height, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) { + const float margin = shellMetrics.margin; + constexpr float leftWidth = 460.0f; + const float gap = shellMetrics.gap; + + ScenarioLayout layout = {}; + layout.introRect = UIRect(margin, margin, leftWidth, 252.0f); + layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f); + layout.stateRect = UIRect( + margin, + layout.controlRect.y + layout.controlRect.height + gap, + leftWidth, + (std::max)(244.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin)); + layout.previewRect = UIRect( + leftWidth + margin * 2.0f, + margin, + (std::max)(420.0f, width - leftWidth - margin * 3.0f), + height - margin * 2.0f); + layout.inspectorRect = UIRect( + layout.previewRect.x + 18.0f, + layout.previewRect.y + 54.0f, + (std::min)(392.0f, layout.previewRect.width - 36.0f), + 150.0f); + layout.inspectorHeaderRect = UIRect( + layout.inspectorRect.x, + layout.inspectorRect.y, + layout.inspectorRect.width, + 24.0f); + layout.sectionRect = UIRect( + layout.inspectorRect.x, + layout.inspectorRect.y + layout.inspectorHeaderRect.height, + layout.inspectorRect.width, + 24.0f); + layout.fieldRect = UIRect( + layout.inspectorRect.x, + layout.sectionRect.y + layout.sectionRect.height + 2.0f, + layout.inspectorRect.width, + 22.0f); + + const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f; + const float buttonY = layout.controlRect.y + 32.0f; + layout.buttons = { + { ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) }, + { ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) } + }; + return layout; +} + +XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics ResolveHostedTextFieldMetrics( + const Style::UITheme& theme) { + const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(theme); + const auto textMetrics = XCEngine::UI::Editor::ResolveUIEditorTextFieldMetrics(theme); + return XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(propertyMetrics, textMetrics); +} + +XCEngine::UI::Editor::Widgets::UIEditorTextFieldPalette ResolveHostedTextFieldPalette( + const Style::UITheme& theme) { + const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(theme); + const auto textPalette = XCEngine::UI::Editor::ResolveUIEditorTextFieldPalette(theme); + return XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette(propertyPalette, textPalette); +} + +void DrawCard( + UIDrawList& drawList, + const UIRect& rect, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, + std::string_view title, + std::string_view subtitle = {}) { + drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius); + drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 14.0f), + std::string(title), + shellPalette.textPrimary, + shellMetrics.titleFontSize); + if (!subtitle.empty()) { + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 40.0f), + std::string(subtitle), + shellPalette.textMuted, + shellMetrics.bodyFontSize); + } +} + +void DrawButton( + UIDrawList& drawList, + const ButtonLayout& button, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, + bool hovered) { + drawList.AddFilledRect( + button.rect, + hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground, + shellMetrics.buttonRadius); + drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius); + drawList.AddText( + UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), + button.label, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); +} + +std::string DescribeHitTarget(const UIEditorTextFieldHitTarget& hitTarget) { + switch (hitTarget.kind) { + case UIEditorTextFieldHitTargetKind::ValueBox: + return "value_box"; + case UIEditorTextFieldHitTargetKind::Row: + return "row"; + case UIEditorTextFieldHitTargetKind::None: + default: + return "none"; + } +} + +UIInputEvent MakePointerEvent( + UIInputEventType type, + const UIPoint& position, + UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = position; + event.pointerButton = button; + return event; +} + +UIInputEvent MakeKeyEvent(std::int32_t keyCode) { + UIInputEvent event = {}; + event.type = UIInputEventType::KeyDown; + event.keyCode = keyCode; + return event; +} + +UIInputEvent MakeCharacterEvent(wchar_t character) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(character); + return event; +} + +UIInputEvent MakeFocusEvent(UIInputEventType type) { + UIInputEvent event = {}; + event.type = type; + return event; +} + +class ScenarioApp { +public: + int Run(HINSTANCE hInstance, int nCmdShow) { + if (!Initialize(hInstance, nCmdShow)) { + Shutdown(); + return 1; + } + + MSG message = {}; + while (message.message != WM_QUIT) { + if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + TranslateMessage(&message); + DispatchMessageW(&message); + continue; + } + + RenderFrame(); + Sleep(8); + } + + Shutdown(); + return static_cast(message.wParam); + } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->OnResize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + + case WM_MOUSEMOVE: + if (app != nullptr) { + app->HandleMouseMove( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_MOUSELEAVE: + if (app != nullptr) { + app->HandleMouseLeave(); + return 0; + } + break; + + case WM_LBUTTONDOWN: + if (app != nullptr) { + app->HandleLeftButtonDown( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandleLeftButtonUp( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr) { + if (wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + app->m_lastResult = "已请求截图,输出到 captures/latest.png"; + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + if (wParam == VK_F6) { + app->HandleFocusLost(); + return 0; + } + + const std::int32_t keyCode = MapTextFieldKey(static_cast(wParam)); + if (keyCode != static_cast(KeyCode::None)) { + app->HandleKeyDown(keyCode); + return 0; + } + } + break; + + case WM_CHAR: + if (app != nullptr) { + app->HandleCharacter(static_cast(wParam)); + return 0; + } + break; + + case WM_PAINT: + if (app != nullptr) { + PAINTSTRUCT paintStruct = {}; + BeginPaint(hwnd, &paintStruct); + app->RenderFrame(); + EndPaint(hwnd, &paintStruct); + return 0; + } + break; + + case WM_ERASEBKGND: + return 1; + + case WM_DESTROY: + if (app != nullptr) { + app->m_hwnd = nullptr; + } + PostQuitMessage(0); + return 0; + + default: + break; + } + + return DefWindowProcW(hwnd, message, wParam, lParam); + } + + bool Initialize(HINSTANCE hInstance, int nCmdShow) { + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(windowClass); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = &ScenarioApp::WndProc; + windowClass.hInstance = hInstance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.lpszClassName = kWindowClassName; + + m_windowClassAtom = RegisterClassExW(&windowClass); + if (m_windowClassAtom == 0) { + return false; + } + + m_hwnd = CreateWindowExW( + 0, + kWindowClassName, + kWindowTitle, + WS_OVERLAPPEDWINDOW | WS_VISIBLE, + CW_USEDEFAULT, + CW_USEDEFAULT, + 1480, + 920, + nullptr, + nullptr, + hInstance, + this); + if (m_hwnd == nullptr) { + return false; + } + + ShowWindow(m_hwnd, nCmdShow); + UpdateWindow(m_hwnd); + + if (!m_renderer.Initialize(m_hwnd)) { + return false; + } + + m_captureRoot = + ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/text_field_basic/captures"; + m_autoScreenshot.Initialize(m_captureRoot); + const auto themeLoad = + XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath()); + if (themeLoad.succeeded) { + m_theme = themeLoad.theme; + m_themeStatus = "loaded"; + } else { + m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error; + } + + ResetScenario(); + return true; + } + + void Shutdown() { + m_autoScreenshot.Shutdown(); + m_renderer.Shutdown(); + + if (m_hwnd != nullptr && IsWindow(m_hwnd)) { + DestroyWindow(m_hwnd); + } + m_hwnd = nullptr; + + if (m_windowClassAtom != 0) { + UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr)); + m_windowClassAtom = 0; + } + } + + ScenarioLayout GetLayout() const { + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + return BuildScenarioLayout( + width, + height, + XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme)); + } + + void ResetScenario() { + m_spec = {}; + m_spec.fieldId = "game_object_name"; + m_spec.label = "Name"; + m_spec.value = "Player"; + m_spec.readOnly = false; + m_interactionState = {}; + m_interactionState.textFieldState.focused = true; + m_mousePosition = UIPoint(-1000.0f, -1000.0f); + m_hoveredAction = ActionId::Reset; + m_hasHoveredAction = false; + m_lastResult = "已重置到默认 TextField 状态"; + RefreshFrame(); + } + + void RefreshFrame() { + if (m_hwnd == nullptr) { + return; + } + + const ScenarioLayout layout = GetLayout(); + const auto metrics = ResolveHostedTextFieldMetrics(m_theme); + m_frame = UpdateUIEditorTextFieldInteraction( + m_interactionState, + m_spec, + layout.fieldRect, + {}, + metrics); + } + + void OnResize(UINT width, UINT height) { + if (width == 0u || height == 0u) { + return; + } + + m_renderer.Resize(width, height); + RefreshFrame(); + } + + void HandleMouseMove(float x, float y) { + m_mousePosition = UIPoint(x, y); + const ScenarioLayout layout = GetLayout(); + UpdateHoveredAction(layout, x, y); + + TRACKMOUSEEVENT trackEvent = {}; + trackEvent.cbSize = sizeof(trackEvent); + trackEvent.dwFlags = TME_LEAVE; + trackEvent.hwndTrack = m_hwnd; + TrackMouseEvent(&trackEvent); + + PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) }); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleMouseLeave() { + m_mousePosition = UIPoint(-1000.0f, -1000.0f); + m_hasHoveredAction = false; + PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) }); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleLeftButtonDown(float x, float y) { + m_mousePosition = UIPoint(x, y); + const ScenarioLayout layout = GetLayout(); + if (HitTestAction(layout, x, y) != nullptr) { + UpdateHoveredAction(layout, x, y); + InvalidateRect(m_hwnd, nullptr, FALSE); + return; + } + + const UIEditorTextFieldInteractionResult result = + PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleLeftButtonUp(float x, float y) { + m_mousePosition = UIPoint(x, y); + const ScenarioLayout layout = GetLayout(); + const ButtonLayout* button = HitTestAction(layout, x, y); + if (button != nullptr) { + ExecuteAction(button->action); + UpdateHoveredAction(layout, x, y); + InvalidateRect(m_hwnd, nullptr, FALSE); + return; + } + + const UIEditorTextFieldInteractionResult result = + PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleKeyDown(std::int32_t keyCode) { + const UIEditorTextFieldInteractionResult result = PumpEvents({ MakeKeyEvent(keyCode) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleCharacter(wchar_t character) { + if (character < 32) { + return; + } + + const UIEditorTextFieldInteractionResult result = PumpEvents({ MakeCharacterEvent(character) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleFocusLost() { + const UIEditorTextFieldInteractionResult result = + PumpEvents({ MakeFocusEvent(UIInputEventType::FocusLost) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) { + const ButtonLayout* button = HitTestAction(layout, x, y); + if (button == nullptr) { + m_hasHoveredAction = false; + return; + } + + m_hoveredAction = button->action; + m_hasHoveredAction = true; + } + + const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const { + for (const ButtonLayout& button : layout.buttons) { + if (ContainsPoint(button.rect, x, y)) { + return &button; + } + } + + return nullptr; + } + + UIEditorTextFieldInteractionResult PumpEvents(std::vector events) { + const ScenarioLayout layout = GetLayout(); + const auto metrics = ResolveHostedTextFieldMetrics(m_theme); + m_frame = UpdateUIEditorTextFieldInteraction( + m_interactionState, + m_spec, + layout.fieldRect, + std::move(events), + metrics); + return m_frame.result; + } + + void UpdateResultText(const UIEditorTextFieldInteractionResult& result) { + if (result.editCommitted) { + m_lastResult = std::string("已提交文本: ") + result.committedText; + return; + } + if (result.editCanceled) { + m_lastResult = "已取消编辑"; + return; + } + if (result.editStarted) { + m_lastResult = "已进入编辑态"; + return; + } + if (result.focusChanged) { + m_lastResult = std::string("焦点变化: ") + (m_interactionState.textFieldState.focused ? "focused" : "lost"); + return; + } + if (result.consumed) { + m_lastResult = "控件已消费输入"; + return; + } + m_lastResult = "等待交互"; + } + + void ExecuteAction(ActionId action) { + switch (action) { + case ActionId::Reset: + ResetScenario(); + break; + + case ActionId::Capture: + m_autoScreenshot.RequestCapture("manual_button"); + m_lastResult = "已请求截图,输出到 captures/latest.png"; + break; + } + } + + void RenderFrame() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme); + const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme); + const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics); + RefreshFrame(); + + const UIEditorTextFieldHitTarget currentHit = + HitTestUIEditorTextField(m_frame.layout, m_mousePosition); + const auto textMetrics = ResolveHostedTextFieldMetrics(m_theme); + const auto textPalette = ResolveHostedTextFieldPalette(m_theme); + const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("EditorTextFieldBasic"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground); + + DrawCard( + drawList, + layout.introRect, + shellPalette, + shellMetrics, + "这个测试验证什么功能", + "验证 UIEditorTextField 的基础编辑交互契约,不涉及 PropertyGrid 或业务 Inspector。"); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f), + "1. 点击 value box,检查是否进入编辑态。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), + "2. 获得 focus 后按 Enter 开始编辑;直接输入字符也应开始编辑。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), + "3. 编辑态下按 Enter commit,按 Escape cancel。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), + "4. 编辑态下按 F6 模拟 FocusLost,应提交暂存文本并退出编辑态。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f), + "5. 检查 Hover / Focus / Editing / Value / Result 是否同步更新。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f), + "6. 按 F12 或点击截图按钮,确认自动截图路径正确。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + + DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作"); + for (const ButtonLayout& button : layout.buttons) { + DrawButton( + drawList, + button, + shellPalette, + shellMetrics, + m_hasHoveredAction && m_hoveredAction == button.action); + } + + DrawCard( + drawList, + layout.stateRect, + shellPalette, + shellMetrics, + "状态摘要", + "重点检查 hit / focus / editing / value / display / result。"); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f), + "Hover: " + DescribeHitTarget(currentHit), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f), + std::string("Focused: ") + (m_interactionState.textFieldState.focused ? "是" : "否"), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f), + std::string("Editing: ") + (m_interactionState.textFieldState.editing ? "是" : "否"), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f), + "Value: " + m_spec.value, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f), + "Display: " + m_interactionState.textFieldState.displayText, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f), + "Result: " + m_lastResult, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + + const std::string captureSummary = + m_autoScreenshot.HasPendingCapture() + ? "截图排队中..." + : (m_autoScreenshot.GetLastCaptureSummary().empty() + ? std::string("F12 -> tests/UI/Editor/integration/shell/text_field_basic/captures/") + : m_autoScreenshot.GetLastCaptureSummary()); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f), + captureSummary, + shellPalette.textWeak, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f), + "Theme: " + m_themeStatus, + shellPalette.textWeak, + shellMetrics.bodyFontSize); + + DrawCard( + drawList, + layout.previewRect, + shellPalette, + shellMetrics, + "TextField 预览", + "这里只放一个 Editor TextField,用来验证基础字段行为。"); + drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor); + drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f); + drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground); + drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f); + drawList.AddText( + UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f), + "Inspector", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor); + drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f); + drawList.AddText( + UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f), + "v Transform", + propertyPalette.sectionTextColor, + shellMetrics.bodyFontSize); + AppendUIEditorTextField( + drawList, + layout.fieldRect, + m_spec, + m_interactionState.textFieldState, + textPalette, + textMetrics); + + const bool framePresented = m_renderer.Render(drawData); + m_autoScreenshot.CaptureIfRequested( + m_renderer, + drawData, + static_cast(width), + static_cast(height), + framePresented); + } + + HWND m_hwnd = nullptr; + ATOM m_windowClassAtom = 0; + NativeRenderer m_renderer = {}; + AutoScreenshotController m_autoScreenshot = {}; + std::filesystem::path m_captureRoot = {}; + UIEditorTextFieldSpec m_spec = {}; + UIEditorTextFieldInteractionState m_interactionState = {}; + UIEditorTextFieldInteractionFrame m_frame = {}; + Style::UITheme m_theme = {}; + UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); + ActionId m_hoveredAction = ActionId::Reset; + bool m_hasHoveredAction = false; + std::string m_lastResult = {}; + std::string m_themeStatus = "fallback"; +}; + +} // namespace + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) { + return ScenarioApp().Run(hInstance, nCmdShow); +} diff --git a/tests/UI/Editor/integration/shell/vector2_field_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/vector2_field_basic/CMakeLists.txt new file mode 100644 index 00000000..438048f3 --- /dev/null +++ b/tests/UI/Editor/integration/shell/vector2_field_basic/CMakeLists.txt @@ -0,0 +1,31 @@ +add_executable(editor_ui_vector2_field_basic_validation WIN32 + main.cpp +) + +target_include_directories(editor_ui_vector2_field_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src + ${CMAKE_SOURCE_DIR}/new_editor/include + ${CMAKE_SOURCE_DIR}/new_editor/app + ${CMAKE_SOURCE_DIR}/engine/include +) + +target_compile_definitions(editor_ui_vector2_field_basic_validation PRIVATE + UNICODE + _UNICODE + XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}" +) + +if(MSVC) + target_compile_options(editor_ui_vector2_field_basic_validation PRIVATE /utf-8 /FS) + set_property(TARGET editor_ui_vector2_field_basic_validation PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +target_link_libraries(editor_ui_vector2_field_basic_validation PRIVATE + XCUIEditorLib + XCUIEditorHost +) + +set_target_properties(editor_ui_vector2_field_basic_validation PROPERTIES + OUTPUT_NAME "XCUIEditorVector2FieldBasicValidation" +) diff --git a/tests/UI/Editor/integration/shell/vector2_field_basic/main.cpp b/tests/UI/Editor/integration/shell/vector2_field_basic/main.cpp new file mode 100644 index 00000000..37052605 --- /dev/null +++ b/tests/UI/Editor/integration/shell/vector2_field_basic/main.cpp @@ -0,0 +1,896 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include +#include "EditorValidationTheme.h" +#include "Host/AutoScreenshot.h" +#include "Host/NativeRenderer.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT +#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "." +#endif + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Host::AutoScreenshotController; +using XCEngine::UI::Editor::Host::NativeRenderer; +using XCEngine::UI::Editor::UIEditorVector2FieldInteractionFrame; +using XCEngine::UI::Editor::UIEditorVector2FieldInteractionResult; +using XCEngine::UI::Editor::UIEditorVector2FieldInteractionState; +using XCEngine::UI::Editor::UpdateUIEditorVector2FieldInteraction; +using XCEngine::UI::Editor::Widgets::AppendUIEditorVector2Field; +using XCEngine::UI::Editor::Widgets::FormatUIEditorVector2FieldComponentValue; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector2Field; +using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTarget; +using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldInvalidComponentIndex; +using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec; +namespace Style = XCEngine::UI::Style; + +constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector2FieldBasicValidation"; +constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector2Field Basic"; + +enum class ActionId : unsigned char { + Reset = 0, + Capture +}; + +struct ButtonLayout { + ActionId action = ActionId::Reset; + const char* label = ""; + UIRect rect = {}; +}; + +struct ScenarioLayout { + UIRect introRect = {}; + UIRect controlRect = {}; + UIRect stateRect = {}; + UIRect previewRect = {}; + UIRect inspectorRect = {}; + UIRect inspectorHeaderRect = {}; + UIRect sectionRect = {}; + UIRect fieldRect = {}; + std::vector buttons = {}; +}; + +std::filesystem::path ResolveRepoRootPath() { + std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT; + if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { + root = root.substr(1u, root.size() - 2u); + } + + return std::filesystem::path(root).lexically_normal(); +} + +std::filesystem::path ResolveValidationThemePath() { + return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme") + .lexically_normal(); +} + +bool ContainsPoint(const UIRect& rect, float x, float y) { + return x >= rect.x && + x <= rect.x + rect.width && + y >= rect.y && + y <= rect.y + rect.height; +} + +std::int32_t MapVector2FieldKey(UINT keyCode) { + switch (keyCode) { + case VK_LEFT: + return static_cast(KeyCode::Left); + case VK_RIGHT: + return static_cast(KeyCode::Right); + case VK_UP: + return static_cast(KeyCode::Up); + case VK_DOWN: + return static_cast(KeyCode::Down); + case VK_HOME: + return static_cast(KeyCode::Home); + case VK_END: + return static_cast(KeyCode::End); + case VK_TAB: + return static_cast(KeyCode::Tab); + case VK_RETURN: + return static_cast(KeyCode::Enter); + case VK_ESCAPE: + return static_cast(KeyCode::Escape); + default: + return static_cast(KeyCode::None); + } +} + +ScenarioLayout BuildScenarioLayout( + float width, + float height, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) { + const float margin = shellMetrics.margin; + constexpr float leftWidth = 470.0f; + const float gap = shellMetrics.gap; + + ScenarioLayout layout = {}; + layout.introRect = UIRect(margin, margin, leftWidth, 272.0f); + layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f); + layout.stateRect = UIRect( + margin, + layout.controlRect.y + layout.controlRect.height + gap, + leftWidth, + (std::max)(240.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin)); + layout.previewRect = UIRect( + leftWidth + margin * 2.0f, + margin, + (std::max)(420.0f, width - leftWidth - margin * 3.0f), + height - margin * 2.0f); + layout.inspectorRect = UIRect( + layout.previewRect.x + 18.0f, + layout.previewRect.y + 54.0f, + (std::min)(392.0f, layout.previewRect.width - 36.0f), + 172.0f); + layout.inspectorHeaderRect = UIRect( + layout.inspectorRect.x, + layout.inspectorRect.y, + layout.inspectorRect.width, + 24.0f); + layout.sectionRect = UIRect( + layout.inspectorRect.x, + layout.inspectorRect.y + layout.inspectorHeaderRect.height, + layout.inspectorRect.width, + 24.0f); + layout.fieldRect = UIRect( + layout.inspectorRect.x, + layout.sectionRect.y + layout.sectionRect.height + 2.0f, + layout.inspectorRect.width, + 22.0f); + + const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f; + const float buttonY = layout.controlRect.y + 32.0f; + layout.buttons = { + { ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) }, + { ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) } + }; + return layout; +} + +void DrawCard( + UIDrawList& drawList, + const UIRect& rect, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, + std::string_view title, + std::string_view subtitle = {}) { + drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius); + drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 14.0f), + std::string(title), + shellPalette.textPrimary, + shellMetrics.titleFontSize); + if (!subtitle.empty()) { + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 40.0f), + std::string(subtitle), + shellPalette.textMuted, + shellMetrics.bodyFontSize); + } +} + +void DrawButton( + UIDrawList& drawList, + const ButtonLayout& button, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, + bool hovered) { + drawList.AddFilledRect( + button.rect, + hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground, + shellMetrics.buttonRadius); + drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius); + drawList.AddText( + UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), + button.label, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); +} + +std::string DescribeHitTarget(const UIEditorVector2FieldHitTarget& hitTarget) { + switch (hitTarget.kind) { + case UIEditorVector2FieldHitTargetKind::Component: + return std::string("component_") + std::to_string(hitTarget.componentIndex); + case UIEditorVector2FieldHitTargetKind::Row: + return "row"; + case UIEditorVector2FieldHitTargetKind::None: + default: + return "none"; + } +} + +std::string DescribeSelectedComponent(std::size_t componentIndex) { + if (componentIndex == UIEditorVector2FieldInvalidComponentIndex) { + return "none"; + } + return componentIndex == 0u ? "X" : "Y"; +} + +UIInputEvent MakePointerEvent( + UIInputEventType type, + const UIPoint& position, + UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = position; + event.pointerButton = button; + return event; +} + +UIInputEvent MakeKeyEvent(std::int32_t keyCode, bool shift = false) { + UIInputEvent event = {}; + event.type = UIInputEventType::KeyDown; + event.keyCode = keyCode; + event.modifiers.shift = shift; + return event; +} + +UIInputEvent MakeCharacterEvent(wchar_t character) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(character); + return event; +} + +UIInputEvent MakeFocusEvent(UIInputEventType type) { + UIInputEvent event = {}; + event.type = type; + return event; +} + +class ScenarioApp { +public: + int Run(HINSTANCE hInstance, int nCmdShow) { + if (!Initialize(hInstance, nCmdShow)) { + Shutdown(); + return 1; + } + + MSG message = {}; + while (message.message != WM_QUIT) { + if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + TranslateMessage(&message); + DispatchMessageW(&message); + continue; + } + + RenderFrame(); + Sleep(8); + } + + Shutdown(); + return static_cast(message.wParam); + } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->OnResize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + + case WM_MOUSEMOVE: + if (app != nullptr) { + app->HandleMouseMove( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_MOUSELEAVE: + if (app != nullptr) { + app->HandleMouseLeave(); + return 0; + } + break; + + case WM_LBUTTONDOWN: + if (app != nullptr) { + app->HandleLeftButtonDown( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandleLeftButtonUp( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr) { + if (wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + app->m_lastResult = "已请求截图,输出到 captures/latest.png"; + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + if (wParam == VK_F6) { + app->HandleFocusLost(); + return 0; + } + + const std::int32_t keyCode = MapVector2FieldKey(static_cast(wParam)); + if (keyCode != static_cast(KeyCode::None)) { + app->HandleKeyDown(keyCode, (GetKeyState(VK_SHIFT) & 0x8000) != 0); + return 0; + } + } + break; + + case WM_CHAR: + if (app != nullptr) { + app->HandleCharacter(static_cast(wParam)); + return 0; + } + break; + + case WM_PAINT: + if (app != nullptr) { + PAINTSTRUCT paintStruct = {}; + BeginPaint(hwnd, &paintStruct); + app->RenderFrame(); + EndPaint(hwnd, &paintStruct); + return 0; + } + break; + + case WM_ERASEBKGND: + return 1; + + case WM_DESTROY: + if (app != nullptr) { + app->m_hwnd = nullptr; + } + PostQuitMessage(0); + return 0; + + default: + break; + } + + return DefWindowProcW(hwnd, message, wParam, lParam); + } + + bool Initialize(HINSTANCE hInstance, int nCmdShow) { + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(windowClass); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = &ScenarioApp::WndProc; + windowClass.hInstance = hInstance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.lpszClassName = kWindowClassName; + + m_windowClassAtom = RegisterClassExW(&windowClass); + if (m_windowClassAtom == 0) { + return false; + } + + m_hwnd = CreateWindowExW( + 0, + kWindowClassName, + kWindowTitle, + WS_OVERLAPPEDWINDOW | WS_VISIBLE, + CW_USEDEFAULT, + CW_USEDEFAULT, + 1520, + 920, + nullptr, + nullptr, + hInstance, + this); + if (m_hwnd == nullptr) { + return false; + } + + ShowWindow(m_hwnd, nCmdShow); + UpdateWindow(m_hwnd); + + if (!m_renderer.Initialize(m_hwnd)) { + return false; + } + + m_captureRoot = + ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/vector2_field_basic/captures"; + m_autoScreenshot.Initialize(m_captureRoot); + const auto themeLoad = + XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath()); + if (themeLoad.succeeded) { + m_theme = themeLoad.theme; + m_themeStatus = "loaded"; + } else { + m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error; + } + + ResetScenario(); + return true; + } + + void Shutdown() { + m_autoScreenshot.Shutdown(); + m_renderer.Shutdown(); + + if (m_hwnd != nullptr && IsWindow(m_hwnd)) { + DestroyWindow(m_hwnd); + } + m_hwnd = nullptr; + + if (m_windowClassAtom != 0) { + UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr)); + m_windowClassAtom = 0; + } + } + + ScenarioLayout GetLayout() const { + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + return BuildScenarioLayout( + width, + height, + XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme)); + } + + void ResetScenario() { + m_spec = {}; + m_spec.fieldId = "position"; + m_spec.label = "Position"; + m_spec.values = { 1.25, -2.5 }; + m_spec.componentLabels = { std::string("X"), std::string("Y") }; + m_spec.step = 0.25; + m_spec.minValue = -10.0; + m_spec.maxValue = 10.0; + m_spec.integerMode = false; + m_spec.readOnly = false; + m_interactionState = {}; + m_interactionState.vector2FieldState.focused = true; + m_interactionState.vector2FieldState.selectedComponentIndex = 0u; + m_mousePosition = UIPoint(-1000.0f, -1000.0f); + m_hoveredAction = ActionId::Reset; + m_hasHoveredAction = false; + m_lastResult = "已重置到默认 Vector2Field 状态"; + RefreshFrame(); + } + + void RefreshFrame() { + if (m_hwnd == nullptr) { + return; + } + + const ScenarioLayout layout = GetLayout(); + const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme); + m_frame = UpdateUIEditorVector2FieldInteraction( + m_interactionState, + m_spec, + layout.fieldRect, + {}, + metrics); + } + + void OnResize(UINT width, UINT height) { + if (width == 0u || height == 0u) { + return; + } + + m_renderer.Resize(width, height); + RefreshFrame(); + } + + void HandleMouseMove(float x, float y) { + m_mousePosition = UIPoint(x, y); + const ScenarioLayout layout = GetLayout(); + UpdateHoveredAction(layout, x, y); + + TRACKMOUSEEVENT trackEvent = {}; + trackEvent.cbSize = sizeof(trackEvent); + trackEvent.dwFlags = TME_LEAVE; + trackEvent.hwndTrack = m_hwnd; + TrackMouseEvent(&trackEvent); + + PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) }); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleMouseLeave() { + m_mousePosition = UIPoint(-1000.0f, -1000.0f); + m_hasHoveredAction = false; + PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) }); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleLeftButtonDown(float x, float y) { + m_mousePosition = UIPoint(x, y); + const ScenarioLayout layout = GetLayout(); + if (HitTestAction(layout, x, y) != nullptr) { + UpdateHoveredAction(layout, x, y); + InvalidateRect(m_hwnd, nullptr, FALSE); + return; + } + + const UIEditorVector2FieldInteractionResult result = + PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleLeftButtonUp(float x, float y) { + m_mousePosition = UIPoint(x, y); + const ScenarioLayout layout = GetLayout(); + const ButtonLayout* button = HitTestAction(layout, x, y); + if (button != nullptr) { + ExecuteAction(button->action); + UpdateHoveredAction(layout, x, y); + InvalidateRect(m_hwnd, nullptr, FALSE); + return; + } + + const UIEditorVector2FieldInteractionResult result = + PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleKeyDown(std::int32_t keyCode, bool shift) { + const UIEditorVector2FieldInteractionResult result = + PumpEvents({ MakeKeyEvent(keyCode, shift) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleCharacter(wchar_t character) { + if (character < 32) { + return; + } + + const UIEditorVector2FieldInteractionResult result = + PumpEvents({ MakeCharacterEvent(character) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleFocusLost() { + const UIEditorVector2FieldInteractionResult result = + PumpEvents({ MakeFocusEvent(UIInputEventType::FocusLost) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) { + const ButtonLayout* button = HitTestAction(layout, x, y); + if (button == nullptr) { + m_hasHoveredAction = false; + return; + } + + m_hoveredAction = button->action; + m_hasHoveredAction = true; + } + + const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const { + for (const ButtonLayout& button : layout.buttons) { + if (ContainsPoint(button.rect, x, y)) { + return &button; + } + } + + return nullptr; + } + + UIEditorVector2FieldInteractionResult PumpEvents(std::vector events) { + const ScenarioLayout layout = GetLayout(); + const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme); + m_frame = UpdateUIEditorVector2FieldInteraction( + m_interactionState, + m_spec, + layout.fieldRect, + std::move(events), + metrics); + return m_frame.result; + } + + void UpdateResultText(const UIEditorVector2FieldInteractionResult& result) { + if (result.editCommitRejected) { + m_lastResult = "提交失败,当前文本不是合法数字"; + return; + } + if (result.editCommitted) { + m_lastResult = + std::string("已提交 ") + + DescribeSelectedComponent(result.changedComponentIndex) + + " = " + result.committedText; + return; + } + if (result.editCanceled) { + m_lastResult = "已取消编辑"; + return; + } + if (result.editStarted) { + m_lastResult = + std::string("开始编辑 component ") + + DescribeSelectedComponent(result.selectedComponentIndex); + return; + } + if (result.stepApplied || result.valueChanged) { + m_lastResult = + std::string("数值已更新,当前 component = ") + + DescribeSelectedComponent(result.changedComponentIndex); + return; + } + if (result.selectionChanged) { + m_lastResult = + std::string("已切换选中 component: ") + + DescribeSelectedComponent(result.selectedComponentIndex); + return; + } + if (result.focusChanged) { + m_lastResult = + std::string("焦点变化: ") + + (m_interactionState.vector2FieldState.focused ? "focused" : "lost"); + return; + } + if (result.consumed) { + m_lastResult = "控件已消费输入"; + return; + } + m_lastResult = "等待交互"; + } + + void ExecuteAction(ActionId action) { + switch (action) { + case ActionId::Reset: + ResetScenario(); + break; + + case ActionId::Capture: + m_autoScreenshot.RequestCapture("manual_button"); + m_lastResult = "已请求截图,输出到 captures/latest.png"; + break; + } + } + + void RenderFrame() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme); + const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme); + const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics); + RefreshFrame(); + + const UIEditorVector2FieldHitTarget currentHit = + HitTestUIEditorVector2Field(m_frame.layout, m_mousePosition); + const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme); + const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector2FieldPalette(m_theme); + const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector2FieldBasic"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground); + + DrawCard( + drawList, + layout.introRect, + shellPalette, + shellMetrics, + "这个测试验证什么功能", + "验证 UIEditorVector2Field 的双通道数值编辑契约,不涉及 PropertyGrid 或业务 Inspector。"); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f), + "1. 点击 X / Y 对应的 value box,检查 selected component 是否切换,并且应进入编辑态。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), + "2. 获得 focus 后按 Tab,检查 selected component 在 X / Y 之间切换。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), + "3. 非编辑态按 Up / Down / Home / End,检查当前 component 的 step / 边界行为。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), + "4. 按 Enter 开始编辑;直接输入字符也应开始编辑;Enter commit,Escape cancel。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f), + "5. 编辑态按 F6 模拟 FocusLost,应提交暂存文本并退出编辑态。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f), + "6. 检查 Hover / Selected / Editing / Values / Result 是否同步更新。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 204.0f), + "7. 按 F12 或点击截图按钮,确认自动截图路径正确。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + + DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作"); + for (const ButtonLayout& button : layout.buttons) { + DrawButton( + drawList, + button, + shellPalette, + shellMetrics, + m_hasHoveredAction && m_hoveredAction == button.action); + } + + DrawCard( + drawList, + layout.stateRect, + shellPalette, + shellMetrics, + "状态摘要", + "重点检查 hit / selected / editing / values / display / result。"); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f), + "Hover: " + DescribeHitTarget(currentHit), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f), + "Selected: " + DescribeSelectedComponent(m_interactionState.vector2FieldState.selectedComponentIndex), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f), + std::string("Focused: ") + (m_interactionState.vector2FieldState.focused ? "是" : "否"), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f), + std::string("Editing: ") + (m_interactionState.vector2FieldState.editing ? "是" : "否"), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f), + "Values: X=" + FormatUIEditorVector2FieldComponentValue(m_spec, 0u) + + " Y=" + FormatUIEditorVector2FieldComponentValue(m_spec, 1u), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f), + "Display: X=" + m_interactionState.vector2FieldState.displayTexts[0] + + " Y=" + m_interactionState.vector2FieldState.displayTexts[1], + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f), + "Result: " + m_lastResult, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + + const std::string captureSummary = + m_autoScreenshot.HasPendingCapture() + ? "截图排队中..." + : (m_autoScreenshot.GetLastCaptureSummary().empty() + ? std::string("F12 -> tests/UI/Editor/integration/shell/vector2_field_basic/captures/") + : m_autoScreenshot.GetLastCaptureSummary()); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f), + captureSummary, + shellPalette.textWeak, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f), + "Theme: " + m_themeStatus, + shellPalette.textWeak, + shellMetrics.bodyFontSize); + + DrawCard( + drawList, + layout.previewRect, + shellPalette, + shellMetrics, + "Vector2Field 预览", + "这里只放一个 Unity 风格的双通道 Vector2 字段。"); + drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor); + drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f); + drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground); + drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f); + drawList.AddText( + UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f), + "Inspector", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor); + drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f); + drawList.AddText( + UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f), + "v Transform", + propertyPalette.sectionTextColor, + shellMetrics.bodyFontSize); + AppendUIEditorVector2Field( + drawList, + layout.fieldRect, + m_spec, + m_interactionState.vector2FieldState, + vectorPalette, + vectorMetrics); + + const bool framePresented = m_renderer.Render(drawData); + m_autoScreenshot.CaptureIfRequested( + m_renderer, + drawData, + static_cast(width), + static_cast(height), + framePresented); + } + + HWND m_hwnd = nullptr; + ATOM m_windowClassAtom = 0; + NativeRenderer m_renderer = {}; + AutoScreenshotController m_autoScreenshot = {}; + std::filesystem::path m_captureRoot = {}; + UIEditorVector2FieldSpec m_spec = {}; + UIEditorVector2FieldInteractionState m_interactionState = {}; + UIEditorVector2FieldInteractionFrame m_frame = {}; + Style::UITheme m_theme = {}; + UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); + ActionId m_hoveredAction = ActionId::Reset; + bool m_hasHoveredAction = false; + std::string m_lastResult = "等待交互"; + std::string m_themeStatus = "fallback"; +}; + +} // namespace + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) { + ScenarioApp app = {}; + return app.Run(hInstance, nCmdShow); +} diff --git a/tests/UI/Editor/integration/shell/vector3_field_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/vector3_field_basic/CMakeLists.txt new file mode 100644 index 00000000..836093cb --- /dev/null +++ b/tests/UI/Editor/integration/shell/vector3_field_basic/CMakeLists.txt @@ -0,0 +1,31 @@ +add_executable(editor_ui_vector3_field_basic_validation WIN32 + main.cpp +) + +target_include_directories(editor_ui_vector3_field_basic_validation PRIVATE + ${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src + ${CMAKE_SOURCE_DIR}/new_editor/include + ${CMAKE_SOURCE_DIR}/new_editor/app + ${CMAKE_SOURCE_DIR}/engine/include +) + +target_compile_definitions(editor_ui_vector3_field_basic_validation PRIVATE + UNICODE + _UNICODE + XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}" +) + +if(MSVC) + target_compile_options(editor_ui_vector3_field_basic_validation PRIVATE /utf-8 /FS) + set_property(TARGET editor_ui_vector3_field_basic_validation PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +target_link_libraries(editor_ui_vector3_field_basic_validation PRIVATE + XCUIEditorLib + XCUIEditorHost +) + +set_target_properties(editor_ui_vector3_field_basic_validation PROPERTIES + OUTPUT_NAME "XCUIEditorVector3FieldBasicValidation" +) diff --git a/tests/UI/Editor/integration/shell/vector3_field_basic/main.cpp b/tests/UI/Editor/integration/shell/vector3_field_basic/main.cpp new file mode 100644 index 00000000..5c9fbc48 --- /dev/null +++ b/tests/UI/Editor/integration/shell/vector3_field_basic/main.cpp @@ -0,0 +1,904 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include +#include "EditorValidationTheme.h" +#include "Host/AutoScreenshot.h" +#include "Host/NativeRenderer.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT +#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "." +#endif + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIDrawData; +using XCEngine::UI::UIDrawList; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Host::AutoScreenshotController; +using XCEngine::UI::Editor::Host::NativeRenderer; +using XCEngine::UI::Editor::UIEditorVector3FieldInteractionFrame; +using XCEngine::UI::Editor::UIEditorVector3FieldInteractionResult; +using XCEngine::UI::Editor::UIEditorVector3FieldInteractionState; +using XCEngine::UI::Editor::UpdateUIEditorVector3FieldInteraction; +using XCEngine::UI::Editor::Widgets::AppendUIEditorVector3Field; +using XCEngine::UI::Editor::Widgets::FormatUIEditorVector3FieldComponentValue; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector3Field; +using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTarget; +using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldInvalidComponentIndex; +using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec; +namespace Style = XCEngine::UI::Style; + +constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector3FieldBasicValidation"; +constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector3Field Basic"; + +enum class ActionId : unsigned char { + Reset = 0, + Capture +}; + +struct ButtonLayout { + ActionId action = ActionId::Reset; + const char* label = ""; + UIRect rect = {}; +}; + +struct ScenarioLayout { + UIRect introRect = {}; + UIRect controlRect = {}; + UIRect stateRect = {}; + UIRect previewRect = {}; + UIRect inspectorRect = {}; + UIRect inspectorHeaderRect = {}; + UIRect sectionRect = {}; + UIRect fieldRect = {}; + std::vector buttons = {}; +}; + +std::filesystem::path ResolveRepoRootPath() { + std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT; + if (root.size() >= 2u && root.front() == '"' && root.back() == '"') { + root = root.substr(1u, root.size() - 2u); + } + + return std::filesystem::path(root).lexically_normal(); +} + +std::filesystem::path ResolveValidationThemePath() { + return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme") + .lexically_normal(); +} + +bool ContainsPoint(const UIRect& rect, float x, float y) { + return x >= rect.x && + x <= rect.x + rect.width && + y >= rect.y && + y <= rect.y + rect.height; +} + +std::int32_t MapVector3FieldKey(UINT keyCode) { + switch (keyCode) { + case VK_LEFT: + return static_cast(KeyCode::Left); + case VK_RIGHT: + return static_cast(KeyCode::Right); + case VK_UP: + return static_cast(KeyCode::Up); + case VK_DOWN: + return static_cast(KeyCode::Down); + case VK_HOME: + return static_cast(KeyCode::Home); + case VK_END: + return static_cast(KeyCode::End); + case VK_TAB: + return static_cast(KeyCode::Tab); + case VK_RETURN: + return static_cast(KeyCode::Enter); + case VK_ESCAPE: + return static_cast(KeyCode::Escape); + default: + return static_cast(KeyCode::None); + } +} + +ScenarioLayout BuildScenarioLayout( + float width, + float height, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) { + const float margin = shellMetrics.margin; + constexpr float leftWidth = 470.0f; + const float gap = shellMetrics.gap; + + ScenarioLayout layout = {}; + layout.introRect = UIRect(margin, margin, leftWidth, 272.0f); + layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f); + layout.stateRect = UIRect( + margin, + layout.controlRect.y + layout.controlRect.height + gap, + leftWidth, + (std::max)(240.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin)); + layout.previewRect = UIRect( + leftWidth + margin * 2.0f, + margin, + (std::max)(420.0f, width - leftWidth - margin * 3.0f), + height - margin * 2.0f); + layout.inspectorRect = UIRect( + layout.previewRect.x + 18.0f, + layout.previewRect.y + 54.0f, + (std::min)(392.0f, layout.previewRect.width - 36.0f), + 172.0f); + layout.inspectorHeaderRect = UIRect( + layout.inspectorRect.x, + layout.inspectorRect.y, + layout.inspectorRect.width, + 24.0f); + layout.sectionRect = UIRect( + layout.inspectorRect.x, + layout.inspectorRect.y + layout.inspectorHeaderRect.height, + layout.inspectorRect.width, + 24.0f); + layout.fieldRect = UIRect( + layout.inspectorRect.x, + layout.sectionRect.y + layout.sectionRect.height + 2.0f, + layout.inspectorRect.width, + 22.0f); + + const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f; + const float buttonY = layout.controlRect.y + 32.0f; + layout.buttons = { + { ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) }, + { ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) } + }; + return layout; +} + +void DrawCard( + UIDrawList& drawList, + const UIRect& rect, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, + std::string_view title, + std::string_view subtitle = {}) { + drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius); + drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius); + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 14.0f), + std::string(title), + shellPalette.textPrimary, + shellMetrics.titleFontSize); + if (!subtitle.empty()) { + drawList.AddText( + UIPoint(rect.x + 16.0f, rect.y + 40.0f), + std::string(subtitle), + shellPalette.textMuted, + shellMetrics.bodyFontSize); + } +} + +void DrawButton( + UIDrawList& drawList, + const ButtonLayout& button, + const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette, + const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics, + bool hovered) { + drawList.AddFilledRect( + button.rect, + hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground, + shellMetrics.buttonRadius); + drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius); + drawList.AddText( + UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), + button.label, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); +} + +std::string DescribeHitTarget(const UIEditorVector3FieldHitTarget& hitTarget) { + switch (hitTarget.kind) { + case UIEditorVector3FieldHitTargetKind::Component: + return std::string("component_") + std::to_string(hitTarget.componentIndex); + case UIEditorVector3FieldHitTargetKind::Row: + return "row"; + case UIEditorVector3FieldHitTargetKind::None: + default: + return "none"; + } +} + +std::string DescribeSelectedComponent(std::size_t componentIndex) { + switch (componentIndex) { + case 0u: + return "X"; + case 1u: + return "Y"; + case 2u: + return "Z"; + default: + return "none"; + } +} + +UIInputEvent MakePointerEvent( + UIInputEventType type, + const UIPoint& position, + UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = position; + event.pointerButton = button; + return event; +} + +UIInputEvent MakeKeyEvent(std::int32_t keyCode, bool shift = false) { + UIInputEvent event = {}; + event.type = UIInputEventType::KeyDown; + event.keyCode = keyCode; + event.modifiers.shift = shift; + return event; +} + +UIInputEvent MakeCharacterEvent(wchar_t character) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(character); + return event; +} + +UIInputEvent MakeFocusEvent(UIInputEventType type) { + UIInputEvent event = {}; + event.type = type; + return event; +} + +class ScenarioApp { +public: + int Run(HINSTANCE hInstance, int nCmdShow) { + if (!Initialize(hInstance, nCmdShow)) { + Shutdown(); + return 1; + } + + MSG message = {}; + while (message.message != WM_QUIT) { + if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) { + TranslateMessage(&message); + DispatchMessageW(&message); + continue; + } + + RenderFrame(); + Sleep(8); + } + + Shutdown(); + return static_cast(message.wParam); + } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->OnResize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + + case WM_MOUSEMOVE: + if (app != nullptr) { + app->HandleMouseMove( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_MOUSELEAVE: + if (app != nullptr) { + app->HandleMouseLeave(); + return 0; + } + break; + + case WM_LBUTTONDOWN: + if (app != nullptr) { + app->HandleLeftButtonDown( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandleLeftButtonUp( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr) { + if (wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + app->m_lastResult = "已请求截图,输出到 captures/latest.png"; + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + if (wParam == VK_F6) { + app->HandleFocusLost(); + return 0; + } + + const std::int32_t keyCode = MapVector3FieldKey(static_cast(wParam)); + if (keyCode != static_cast(KeyCode::None)) { + app->HandleKeyDown(keyCode, (GetKeyState(VK_SHIFT) & 0x8000) != 0); + return 0; + } + } + break; + + case WM_CHAR: + if (app != nullptr) { + app->HandleCharacter(static_cast(wParam)); + return 0; + } + break; + + case WM_PAINT: + if (app != nullptr) { + PAINTSTRUCT paintStruct = {}; + BeginPaint(hwnd, &paintStruct); + app->RenderFrame(); + EndPaint(hwnd, &paintStruct); + return 0; + } + break; + + case WM_ERASEBKGND: + return 1; + + case WM_DESTROY: + if (app != nullptr) { + app->m_hwnd = nullptr; + } + PostQuitMessage(0); + return 0; + + default: + break; + } + + return DefWindowProcW(hwnd, message, wParam, lParam); + } + + bool Initialize(HINSTANCE hInstance, int nCmdShow) { + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(windowClass); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = &ScenarioApp::WndProc; + windowClass.hInstance = hInstance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.lpszClassName = kWindowClassName; + + m_windowClassAtom = RegisterClassExW(&windowClass); + if (m_windowClassAtom == 0) { + return false; + } + + m_hwnd = CreateWindowExW( + 0, + kWindowClassName, + kWindowTitle, + WS_OVERLAPPEDWINDOW | WS_VISIBLE, + CW_USEDEFAULT, + CW_USEDEFAULT, + 1520, + 920, + nullptr, + nullptr, + hInstance, + this); + if (m_hwnd == nullptr) { + return false; + } + + ShowWindow(m_hwnd, nCmdShow); + UpdateWindow(m_hwnd); + + if (!m_renderer.Initialize(m_hwnd)) { + return false; + } + + m_captureRoot = + ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/vector3_field_basic/captures"; + m_autoScreenshot.Initialize(m_captureRoot); + const auto themeLoad = + XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath()); + if (themeLoad.succeeded) { + m_theme = themeLoad.theme; + m_themeStatus = "loaded"; + } else { + m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error; + } + + ResetScenario(); + return true; + } + + void Shutdown() { + m_autoScreenshot.Shutdown(); + m_renderer.Shutdown(); + + if (m_hwnd != nullptr && IsWindow(m_hwnd)) { + DestroyWindow(m_hwnd); + } + m_hwnd = nullptr; + + if (m_windowClassAtom != 0) { + UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr)); + m_windowClassAtom = 0; + } + } + + ScenarioLayout GetLayout() const { + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + return BuildScenarioLayout( + width, + height, + XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme)); + } + + void ResetScenario() { + m_spec = {}; + m_spec.fieldId = "position"; + m_spec.label = "Position"; + m_spec.values = { 1.25, -2.5, 4.75 }; + m_spec.componentLabels = { std::string("X"), std::string("Y"), std::string("Z") }; + m_spec.step = 0.25; + m_spec.minValue = -10.0; + m_spec.maxValue = 10.0; + m_spec.integerMode = false; + m_spec.readOnly = false; + m_interactionState = {}; + m_interactionState.vector3FieldState.focused = true; + m_interactionState.vector3FieldState.selectedComponentIndex = 0u; + m_mousePosition = UIPoint(-1000.0f, -1000.0f); + m_hoveredAction = ActionId::Reset; + m_hasHoveredAction = false; + m_lastResult = "已重置到默认 Vector3Field 状态"; + RefreshFrame(); + } + + void RefreshFrame() { + if (m_hwnd == nullptr) { + return; + } + + const ScenarioLayout layout = GetLayout(); + const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme); + m_frame = UpdateUIEditorVector3FieldInteraction( + m_interactionState, + m_spec, + layout.fieldRect, + {}, + metrics); + } + + void OnResize(UINT width, UINT height) { + if (width == 0u || height == 0u) { + return; + } + + m_renderer.Resize(width, height); + RefreshFrame(); + } + + void HandleMouseMove(float x, float y) { + m_mousePosition = UIPoint(x, y); + const ScenarioLayout layout = GetLayout(); + UpdateHoveredAction(layout, x, y); + + TRACKMOUSEEVENT trackEvent = {}; + trackEvent.cbSize = sizeof(trackEvent); + trackEvent.dwFlags = TME_LEAVE; + trackEvent.hwndTrack = m_hwnd; + TrackMouseEvent(&trackEvent); + + PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) }); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleMouseLeave() { + m_mousePosition = UIPoint(-1000.0f, -1000.0f); + m_hasHoveredAction = false; + PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) }); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleLeftButtonDown(float x, float y) { + m_mousePosition = UIPoint(x, y); + const ScenarioLayout layout = GetLayout(); + if (HitTestAction(layout, x, y) != nullptr) { + UpdateHoveredAction(layout, x, y); + InvalidateRect(m_hwnd, nullptr, FALSE); + return; + } + + const UIEditorVector3FieldInteractionResult result = + PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleLeftButtonUp(float x, float y) { + m_mousePosition = UIPoint(x, y); + const ScenarioLayout layout = GetLayout(); + const ButtonLayout* button = HitTestAction(layout, x, y); + if (button != nullptr) { + ExecuteAction(button->action); + UpdateHoveredAction(layout, x, y); + InvalidateRect(m_hwnd, nullptr, FALSE); + return; + } + + const UIEditorVector3FieldInteractionResult result = + PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleKeyDown(std::int32_t keyCode, bool shift) { + const UIEditorVector3FieldInteractionResult result = + PumpEvents({ MakeKeyEvent(keyCode, shift) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleCharacter(wchar_t character) { + if (character < 32) { + return; + } + + const UIEditorVector3FieldInteractionResult result = + PumpEvents({ MakeCharacterEvent(character) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void HandleFocusLost() { + const UIEditorVector3FieldInteractionResult result = + PumpEvents({ MakeFocusEvent(UIInputEventType::FocusLost) }); + UpdateResultText(result); + InvalidateRect(m_hwnd, nullptr, FALSE); + } + + void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) { + const ButtonLayout* button = HitTestAction(layout, x, y); + if (button == nullptr) { + m_hasHoveredAction = false; + return; + } + + m_hoveredAction = button->action; + m_hasHoveredAction = true; + } + + const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const { + for (const ButtonLayout& button : layout.buttons) { + if (ContainsPoint(button.rect, x, y)) { + return &button; + } + } + + return nullptr; + } + + UIEditorVector3FieldInteractionResult PumpEvents(std::vector events) { + const ScenarioLayout layout = GetLayout(); + const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme); + m_frame = UpdateUIEditorVector3FieldInteraction( + m_interactionState, + m_spec, + layout.fieldRect, + std::move(events), + metrics); + return m_frame.result; + } + + void UpdateResultText(const UIEditorVector3FieldInteractionResult& result) { + if (result.editCommitRejected) { + m_lastResult = "提交失败,当前文本不是合法数字"; + return; + } + if (result.editCommitted) { + m_lastResult = + std::string("已提交 ") + + DescribeSelectedComponent(result.changedComponentIndex) + + " = " + result.committedText; + return; + } + if (result.editCanceled) { + m_lastResult = "已取消编辑"; + return; + } + if (result.editStarted) { + m_lastResult = + std::string("开始编辑 component ") + + DescribeSelectedComponent(result.selectedComponentIndex); + return; + } + if (result.stepApplied || result.valueChanged) { + m_lastResult = + std::string("数值已更新,当前 component = ") + + DescribeSelectedComponent(result.changedComponentIndex); + return; + } + if (result.selectionChanged) { + m_lastResult = + std::string("已切换选中 component: ") + + DescribeSelectedComponent(result.selectedComponentIndex); + return; + } + if (result.focusChanged) { + m_lastResult = + std::string("焦点变化: ") + + (m_interactionState.vector3FieldState.focused ? "focused" : "lost"); + return; + } + if (result.consumed) { + m_lastResult = "控件已消费输入"; + return; + } + m_lastResult = "等待交互"; + } + + void ExecuteAction(ActionId action) { + switch (action) { + case ActionId::Reset: + ResetScenario(); + break; + + case ActionId::Capture: + m_autoScreenshot.RequestCapture("manual_button"); + m_lastResult = "已请求截图,输出到 captures/latest.png"; + break; + } + } + + void RenderFrame() { + if (m_hwnd == nullptr) { + return; + } + + RECT clientRect = {}; + GetClientRect(m_hwnd, &clientRect); + const float width = static_cast((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme); + const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme); + const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics); + RefreshFrame(); + + const UIEditorVector3FieldHitTarget currentHit = + HitTestUIEditorVector3Field(m_frame.layout, m_mousePosition); + const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme); + const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector3FieldPalette(m_theme); + const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme); + + UIDrawData drawData = {}; + UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector3FieldBasic"); + drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground); + + DrawCard( + drawList, + layout.introRect, + shellPalette, + shellMetrics, + "这个测试验证什么功能", + "验证 UIEditorVector3Field 的三通道数值编辑契约,不涉及 PropertyGrid 或业务 Inspector。"); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f), + "1. 点击 X / Y / Z 对应的 value box,检查 selected component 是否切换,并应进入编辑态。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f), + "2. 获得 focus 后按 Tab,检查 selected component 在 X / Y / Z 之间切换。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f), + "3. 非编辑态按 Up / Down / Home / End,检查当前 component 的 step / 边界行为。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f), + "4. 按 Enter 开始编辑;直接输入字符也应开始编辑;Enter commit,Escape cancel。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f), + "5. 编辑态按 F6 模拟 FocusLost,应提交暂存文本并退出编辑态。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f), + "6. 检查 Hover / Selected / Editing / Values / Result 是否同步更新。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 204.0f), + "7. 按 F12 或点击截图按钮,确认自动截图路径正确。", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + + DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作"); + for (const ButtonLayout& button : layout.buttons) { + DrawButton( + drawList, + button, + shellPalette, + shellMetrics, + m_hasHoveredAction && m_hoveredAction == button.action); + } + + DrawCard( + drawList, + layout.stateRect, + shellPalette, + shellMetrics, + "状态摘要", + "重点检查 hit / selected / editing / values / display / result。"); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f), + "Hover: " + DescribeHitTarget(currentHit), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f), + "Selected: " + DescribeSelectedComponent(m_interactionState.vector3FieldState.selectedComponentIndex), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f), + std::string("Focused: ") + (m_interactionState.vector3FieldState.focused ? "是" : "否"), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f), + std::string("Editing: ") + (m_interactionState.vector3FieldState.editing ? "是" : "否"), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f), + "Values: X=" + FormatUIEditorVector3FieldComponentValue(m_spec, 0u) + + " Y=" + FormatUIEditorVector3FieldComponentValue(m_spec, 1u) + + " Z=" + FormatUIEditorVector3FieldComponentValue(m_spec, 2u), + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f), + "Display: X=" + m_interactionState.vector3FieldState.displayTexts[0] + + " Y=" + m_interactionState.vector3FieldState.displayTexts[1] + + " Z=" + m_interactionState.vector3FieldState.displayTexts[2], + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f), + "Result: " + m_lastResult, + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + + const std::string captureSummary = + m_autoScreenshot.HasPendingCapture() + ? "截图排队中..." + : (m_autoScreenshot.GetLastCaptureSummary().empty() + ? std::string("F12 -> tests/UI/Editor/integration/shell/vector3_field_basic/captures/") + : m_autoScreenshot.GetLastCaptureSummary()); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f), + captureSummary, + shellPalette.textWeak, + shellMetrics.bodyFontSize); + drawList.AddText( + UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 262.0f), + "Theme: " + m_themeStatus, + shellPalette.textWeak, + shellMetrics.bodyFontSize); + + DrawCard( + drawList, + layout.previewRect, + shellPalette, + shellMetrics, + "Vector3Field 预览", + "这里只放一个 Unity 风格的三通道 Vector3 字段。"); + drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor); + drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f); + drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground); + drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f); + drawList.AddText( + UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f), + "Inspector", + shellPalette.textPrimary, + shellMetrics.bodyFontSize); + drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor); + drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f); + drawList.AddText( + UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f), + "v Transform", + propertyPalette.sectionTextColor, + shellMetrics.bodyFontSize); + AppendUIEditorVector3Field( + drawList, + layout.fieldRect, + m_spec, + m_interactionState.vector3FieldState, + vectorPalette, + vectorMetrics); + + const bool framePresented = m_renderer.Render(drawData); + m_autoScreenshot.CaptureIfRequested( + m_renderer, + drawData, + static_cast(width), + static_cast(height), + framePresented); + } + + HWND m_hwnd = nullptr; + ATOM m_windowClassAtom = 0; + NativeRenderer m_renderer = {}; + AutoScreenshotController m_autoScreenshot = {}; + std::filesystem::path m_captureRoot = {}; + UIEditorVector3FieldSpec m_spec = {}; + UIEditorVector3FieldInteractionState m_interactionState = {}; + UIEditorVector3FieldInteractionFrame m_frame = {}; + Style::UITheme m_theme = {}; + UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f); + ActionId m_hoveredAction = ActionId::Reset; + bool m_hasHoveredAction = false; + std::string m_lastResult = "等待交互"; + std::string m_themeStatus = "fallback"; +}; + +} // namespace + +int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) { + ScenarioApp app = {}; + return app.Run(hInstance, nCmdShow); +} diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index 6e6f24e0..1368446b 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -17,6 +17,8 @@ set(EDITOR_UI_UNIT_TEST_SOURCES test_ui_editor_shell_compose.cpp test_ui_editor_shell_interaction.cpp test_ui_editor_collection_primitives.cpp + test_ui_editor_field_row_layout.cpp + test_ui_editor_theme.cpp test_ui_editor_bool_field.cpp test_ui_editor_bool_field_interaction.cpp test_ui_editor_dock_host.cpp @@ -28,6 +30,12 @@ set(EDITOR_UI_UNIT_TEST_SOURCES test_ui_editor_enum_field_interaction.cpp test_ui_editor_number_field.cpp test_ui_editor_number_field_interaction.cpp + test_ui_editor_text_field.cpp + test_ui_editor_text_field_interaction.cpp + test_ui_editor_vector2_field.cpp + test_ui_editor_vector2_field_interaction.cpp + test_ui_editor_vector3_field.cpp + test_ui_editor_vector3_field_interaction.cpp test_ui_editor_scroll_view.cpp test_ui_editor_scroll_view_interaction.cpp test_ui_editor_status_bar.cpp diff --git a/tests/UI/Editor/unit/test_ui_editor_bool_field.cpp b/tests/UI/Editor/unit/test_ui_editor_bool_field.cpp index 4e02a57c..54d8125c 100644 --- a/tests/UI/Editor/unit/test_ui_editor_bool_field.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_bool_field.cpp @@ -1,9 +1,12 @@ #include +#include #include namespace { +using XCEngine::UI::UIDrawCommandType; +using XCEngine::UI::UIColor; using XCEngine::UI::UIPoint; using XCEngine::UI::UIRect; using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolFieldBackground; @@ -16,39 +19,54 @@ using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldState; TEST(UIEditorBoolFieldTest, LayoutBuildsLabelAndToggleRects) { UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false }; - const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec); + const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec); EXPECT_GT(layout.labelRect.width, 0.0f); - EXPECT_FLOAT_EQ(layout.toggleRect.width, 42.0f); - EXPECT_GT(layout.knobRect.x, layout.toggleRect.x); + EXPECT_FLOAT_EQ(layout.controlRect.x, 236.0f); + EXPECT_FLOAT_EQ(layout.checkboxRect.width, 18.0f); + EXPECT_FLOAT_EQ(layout.checkmarkRect.width, layout.checkboxRect.width); + EXPECT_FLOAT_EQ(layout.checkboxRect.y, 2.0f); } TEST(UIEditorBoolFieldTest, HitTestResolvesToggleAndRow) { UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false }; - const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec); + const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec); - const auto toggleHit = HitTestUIEditorBoolField( + const auto checkboxHit = HitTestUIEditorBoolField( layout, - UIPoint(layout.toggleRect.x + 4.0f, layout.toggleRect.y + 4.0f)); - EXPECT_EQ(toggleHit.kind, UIEditorBoolFieldHitTargetKind::Toggle); + UIPoint(layout.controlRect.x + layout.controlRect.width - 2.0f, layout.controlRect.y + 2.0f)); + EXPECT_EQ(checkboxHit.kind, UIEditorBoolFieldHitTargetKind::Checkbox); - const auto rowHit = HitTestUIEditorBoolField(layout, UIPoint(20.0f, 16.0f)); + const auto rowHit = HitTestUIEditorBoolField(layout, UIPoint(20.0f, 11.0f)); EXPECT_EQ(rowHit.kind, UIEditorBoolFieldHitTargetKind::Row); } -TEST(UIEditorBoolFieldTest, BackgroundAndForegroundEmitStableCommands) { +TEST(UIEditorBoolFieldTest, BackgroundAndForegroundEmitCheckboxOnlyChromeAndCenteredText) { UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false }; UIEditorBoolFieldState state = {}; state.focused = true; - state.hoveredTarget = UIEditorBoolFieldHitTargetKind::Toggle; + state.hoveredTarget = UIEditorBoolFieldHitTargetKind::Checkbox; XCEngine::UI::UIDrawData drawData = {}; auto& drawList = drawData.EmplaceDrawList("BoolField"); - const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec); + const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec); AppendUIEditorBoolFieldBackground(drawList, layout, spec, state); AppendUIEditorBoolFieldForeground(drawList, layout, spec); - ASSERT_GE(drawList.GetCommands().size(), 6u); + const auto& commands = drawList.GetCommands(); + ASSERT_EQ(commands.size(), 6u); + EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRect); + EXPECT_EQ(commands[0].rect.x, layout.checkboxRect.x); + EXPECT_EQ(commands[0].rect.y, layout.checkboxRect.y); + EXPECT_EQ(commands[1].type, UIDrawCommandType::RectOutline); + EXPECT_EQ(commands[1].color.r, 0.14f); + EXPECT_EQ(commands[2].type, UIDrawCommandType::PushClipRect); + EXPECT_EQ(commands[3].type, UIDrawCommandType::Text); + EXPECT_FLOAT_EQ(commands[3].position.y, 2.0f); + EXPECT_EQ(commands[4].type, UIDrawCommandType::PopClipRect); + EXPECT_EQ(commands[5].type, UIDrawCommandType::Text); + EXPECT_EQ(commands[5].text, "V"); + EXPECT_FLOAT_EQ(commands[5].position.y, 2.0f); } } // namespace diff --git a/tests/UI/Editor/unit/test_ui_editor_bool_field_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_bool_field_interaction.cpp index 99915955..e69bd573 100644 --- a/tests/UI/Editor/unit/test_ui_editor_bool_field_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_bool_field_interaction.cpp @@ -45,7 +45,7 @@ TEST(UIEditorBoolFieldInteractionTest, ClickToggleFlipsValue) { UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec, {}); - const auto toggle = frame.layout.toggleRect; + const auto checkbox = frame.layout.checkboxRect; frame = UpdateUIEditorBoolFieldInteraction( state, @@ -53,8 +53,8 @@ TEST(UIEditorBoolFieldInteractionTest, ClickToggleFlipsValue) { UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec, { - MakePointer(UIInputEventType::PointerButtonDown, toggle.x + 4.0f, toggle.y + 4.0f, UIPointerButton::Left), - MakePointer(UIInputEventType::PointerButtonUp, toggle.x + 4.0f, toggle.y + 4.0f, UIPointerButton::Left) + MakePointer(UIInputEventType::PointerButtonDown, checkbox.x + 4.0f, checkbox.y + 4.0f, UIPointerButton::Left), + MakePointer(UIInputEventType::PointerButtonUp, checkbox.x + 4.0f, checkbox.y + 4.0f, UIPointerButton::Left) }); EXPECT_TRUE(frame.result.valueChanged); @@ -97,13 +97,13 @@ TEST(UIEditorBoolFieldInteractionTest, HoverTracksToggleHitTarget) { UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec, {}); - const auto toggle = frame.layout.toggleRect; + const auto checkbox = frame.layout.checkboxRect; frame = UpdateUIEditorBoolFieldInteraction( state, value, UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec, - { MakePointer(UIInputEventType::PointerMove, toggle.x + 4.0f, toggle.y + 4.0f) }); - EXPECT_EQ(frame.result.hitTarget.kind, UIEditorBoolFieldHitTargetKind::Toggle); + { MakePointer(UIInputEventType::PointerMove, checkbox.x + 4.0f, checkbox.y + 4.0f) }); + EXPECT_EQ(frame.result.hitTarget.kind, UIEditorBoolFieldHitTargetKind::Checkbox); } diff --git a/tests/UI/Editor/unit/test_ui_editor_enum_field.cpp b/tests/UI/Editor/unit/test_ui_editor_enum_field.cpp index f4fd0746..75ea4ef6 100644 --- a/tests/UI/Editor/unit/test_ui_editor_enum_field.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_enum_field.cpp @@ -1,35 +1,77 @@ #include +#include #include namespace { +using XCEngine::UI::UIDrawCommandType; using XCEngine::UI::UIPoint; using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumFieldBackground; +using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumFieldForeground; using XCEngine::UI::Editor::Widgets::BuildUIEditorEnumFieldLayout; using XCEngine::UI::Editor::Widgets::HitTestUIEditorEnumField; using XCEngine::UI::Editor::Widgets::ResolveUIEditorEnumFieldValueText; using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind; using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec; +using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldState; TEST(UIEditorEnumFieldTest, ValueTextUsesSelectedOption) { UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false }; EXPECT_EQ(ResolveUIEditorEnumFieldValueText(spec), "Cutout"); } -TEST(UIEditorEnumFieldTest, HitTestResolvesPreviousNextAndValueBox) { +TEST(UIEditorEnumFieldTest, LayoutKeepsInspectorControlColumnAndUnityArrowWidth) { UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false }; - const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec); + const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec); + + EXPECT_FLOAT_EQ(layout.controlRect.x, 236.0f); + EXPECT_FLOAT_EQ(layout.valueRect.y, 1.0f); + EXPECT_FLOAT_EQ(layout.valueRect.height, 20.0f); + EXPECT_FLOAT_EQ(layout.arrowRect.width, 20.0f); +} + +TEST(UIEditorEnumFieldTest, HitTestResolvesArrowAndValueBox) { + UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false }; + const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec); EXPECT_EQ( - HitTestUIEditorEnumField(layout, UIPoint(layout.previousRect.x + 2.0f, layout.previousRect.y + 2.0f)).kind, - UIEditorEnumFieldHitTargetKind::PreviousButton); - EXPECT_EQ( - HitTestUIEditorEnumField(layout, UIPoint(layout.nextRect.x + 2.0f, layout.nextRect.y + 2.0f)).kind, - UIEditorEnumFieldHitTargetKind::NextButton); + HitTestUIEditorEnumField(layout, UIPoint(layout.arrowRect.x + 2.0f, layout.arrowRect.y + 2.0f)).kind, + UIEditorEnumFieldHitTargetKind::DropdownArrow); EXPECT_EQ( HitTestUIEditorEnumField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind, UIEditorEnumFieldHitTargetKind::ValueBox); } +TEST(UIEditorEnumFieldTest, BackgroundAndForegroundEmitControlOnlyChromeAndCenteredText) { + UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false }; + UIEditorEnumFieldState state = {}; + state.popupOpen = true; + state.hoveredTarget = UIEditorEnumFieldHitTargetKind::DropdownArrow; + + XCEngine::UI::UIDrawData drawData = {}; + auto& drawList = drawData.EmplaceDrawList("EnumField"); + const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec); + AppendUIEditorEnumFieldBackground(drawList, layout, spec, state); + AppendUIEditorEnumFieldForeground(drawList, layout, spec); + + const auto& commands = drawList.GetCommands(); + ASSERT_EQ(commands.size(), 9u); + EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRect); + EXPECT_EQ(commands[0].rect.x, layout.valueRect.x); + EXPECT_EQ(commands[1].type, UIDrawCommandType::RectOutline); + EXPECT_EQ(commands[2].type, UIDrawCommandType::PushClipRect); + EXPECT_EQ(commands[3].type, UIDrawCommandType::Text); + EXPECT_FLOAT_EQ(commands[3].position.y, 2.0f); + EXPECT_EQ(commands[4].type, UIDrawCommandType::PopClipRect); + EXPECT_EQ(commands[5].type, UIDrawCommandType::PushClipRect); + EXPECT_EQ(commands[6].type, UIDrawCommandType::Text); + EXPECT_FLOAT_EQ(commands[6].position.y, 1.0f); + EXPECT_EQ(commands[7].type, UIDrawCommandType::PopClipRect); + EXPECT_EQ(commands[8].type, UIDrawCommandType::Text); + EXPECT_EQ(commands[8].text, "V"); + EXPECT_FLOAT_EQ(commands[8].position.y, 2.0f); +} + } // namespace diff --git a/tests/UI/Editor/unit/test_ui_editor_enum_field_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_enum_field_interaction.cpp index 856bad14..dd4f5302 100644 --- a/tests/UI/Editor/unit/test_ui_editor_enum_field_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_enum_field_interaction.cpp @@ -33,7 +33,7 @@ UIInputEvent MakeKey(KeyCode keyCode) { } // namespace -TEST(UIEditorEnumFieldInteractionTest, ClickButtonsAdjustSelection) { +TEST(UIEditorEnumFieldInteractionTest, ClickValueBoxOpensPopupAndSelectsItem) { UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false }; UIEditorEnumFieldInteractionState state = {}; std::size_t selectedIndex = 1u; @@ -51,14 +51,29 @@ TEST(UIEditorEnumFieldInteractionTest, ClickButtonsAdjustSelection) { UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec, { - MakePointer(UIInputEventType::PointerButtonDown, frame.layout.nextRect.x + 2.0f, frame.layout.nextRect.y + 2.0f, UIPointerButton::Left), - MakePointer(UIInputEventType::PointerButtonUp, frame.layout.nextRect.x + 2.0f, frame.layout.nextRect.y + 2.0f, UIPointerButton::Left) + MakePointer(UIInputEventType::PointerButtonDown, frame.layout.valueRect.x + 2.0f, frame.layout.valueRect.y + 2.0f, UIPointerButton::Left), + MakePointer(UIInputEventType::PointerButtonUp, frame.layout.valueRect.x + 2.0f, frame.layout.valueRect.y + 2.0f, UIPointerButton::Left) + }); + EXPECT_TRUE(frame.result.popupOpened); + EXPECT_TRUE(frame.popupOpen); + + ASSERT_FALSE(frame.popupLayout.itemRects.empty()); + const auto itemRect = frame.popupLayout.itemRects[2]; + frame = UpdateUIEditorEnumFieldInteraction( + state, + selectedIndex, + UIRect(0.0f, 0.0f, 360.0f, 32.0f), + spec, + { + MakePointer(UIInputEventType::PointerButtonDown, itemRect.x + 2.0f, itemRect.y + 2.0f, UIPointerButton::Left), + MakePointer(UIInputEventType::PointerButtonUp, itemRect.x + 2.0f, itemRect.y + 2.0f, UIPointerButton::Left) }); EXPECT_TRUE(frame.result.selectionChanged); EXPECT_EQ(selectedIndex, 2u); + EXPECT_FALSE(frame.popupOpen); } -TEST(UIEditorEnumFieldInteractionTest, KeyboardControlsMoveToEnds) { +TEST(UIEditorEnumFieldInteractionTest, KeyboardCanOpenMoveAndCommitPopupSelection) { UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false }; UIEditorEnumFieldInteractionState state = {}; state.fieldState.focused = true; @@ -69,16 +84,17 @@ TEST(UIEditorEnumFieldInteractionTest, KeyboardControlsMoveToEnds) { selectedIndex, UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec, - { MakeKey(KeyCode::Home) }); - EXPECT_TRUE(frame.result.selectionChanged); - EXPECT_EQ(selectedIndex, 0u); + { MakeKey(KeyCode::Enter) }); + EXPECT_TRUE(frame.result.popupOpened); + EXPECT_TRUE(frame.popupOpen); frame = UpdateUIEditorEnumFieldInteraction( state, selectedIndex, UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec, - { MakeKey(KeyCode::End) }); + { MakeKey(KeyCode::Down), MakeKey(KeyCode::Enter) }); EXPECT_TRUE(frame.result.selectionChanged); EXPECT_EQ(selectedIndex, 2u); + EXPECT_FALSE(frame.popupOpen); } diff --git a/tests/UI/Editor/unit/test_ui_editor_field_row_layout.cpp b/tests/UI/Editor/unit/test_ui_editor_field_row_layout.cpp new file mode 100644 index 00000000..3f87001c --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_field_row_layout.cpp @@ -0,0 +1,52 @@ +#include + +#include + +namespace { + +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Widgets::BuildUIEditorFieldRowLayout; +using XCEngine::UI::Editor::Widgets::UIEditorFieldRowLayoutMetrics; + +TEST(UIEditorFieldRowLayoutTest, WideRowsKeepStableControlColumnAnchor) { + UIEditorFieldRowLayoutMetrics metrics = {}; + const auto layout = BuildUIEditorFieldRowLayout( + UIRect(10.0f, 20.0f, 392.0f, 22.0f), + 96.0f, + metrics); + + EXPECT_FLOAT_EQ(layout.bounds.height, 22.0f); + EXPECT_FLOAT_EQ(layout.labelRect.x, 22.0f); + EXPECT_FLOAT_EQ(layout.controlRect.x, 246.0f); + EXPECT_FLOAT_EQ(layout.controlRect.y, 21.0f); + EXPECT_FLOAT_EQ(layout.controlRect.height, 20.0f); +} + +TEST(UIEditorFieldRowLayoutTest, NarrowRowsCompressFromRightWithoutMagicRatioFallback) { + UIEditorFieldRowLayoutMetrics metrics = {}; + const auto layout = BuildUIEditorFieldRowLayout( + UIRect(10.0f, 20.0f, 280.0f, 22.0f), + 96.0f, + metrics); + + EXPECT_FLOAT_EQ(layout.controlRect.x, 186.0f); + EXPECT_FLOAT_EQ(layout.controlRect.width, 96.0f); + EXPECT_FLOAT_EQ(layout.labelRect.width, 144.0f); +} + +TEST(UIEditorFieldRowLayoutTest, ZeroHeightFallsBackToMetricRowHeight) { + UIEditorFieldRowLayoutMetrics metrics = {}; + metrics.rowHeight = 24.0f; + metrics.controlInsetY = 2.0f; + + const auto layout = BuildUIEditorFieldRowLayout( + UIRect(0.0f, 0.0f, 360.0f, 0.0f), + 120.0f, + metrics); + + EXPECT_FLOAT_EQ(layout.bounds.height, 24.0f); + EXPECT_FLOAT_EQ(layout.controlRect.y, 2.0f); + EXPECT_FLOAT_EQ(layout.controlRect.height, 20.0f); +} + +} // namespace diff --git a/tests/UI/Editor/unit/test_ui_editor_number_field.cpp b/tests/UI/Editor/unit/test_ui_editor_number_field.cpp index d78248d9..0c2ddadf 100644 --- a/tests/UI/Editor/unit/test_ui_editor_number_field.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_number_field.cpp @@ -20,30 +20,26 @@ TEST(UIEditorNumberFieldTest, FormatSupportsIntegerAndFloatMode) { EXPECT_EQ(FormatUIEditorNumberFieldValue(floatSpec), "1.25"); } -TEST(UIEditorNumberFieldTest, LayoutBuildsValueAndStepperRects) { +TEST(UIEditorNumberFieldTest, LayoutBuildsValueRectWithoutStepperButtons) { UIEditorNumberFieldSpec spec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false }; const auto layout = BuildUIEditorNumberFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec); EXPECT_GT(layout.labelRect.width, 0.0f); EXPECT_GT(layout.controlRect.width, 0.0f); EXPECT_GT(layout.valueRect.width, 0.0f); - EXPECT_FLOAT_EQ(layout.decrementRect.width, 22.0f); - EXPECT_FLOAT_EQ(layout.incrementRect.width, 22.0f); + EXPECT_FLOAT_EQ(layout.controlRect.width, layout.valueRect.width); } -TEST(UIEditorNumberFieldTest, HitTestResolvesButtonsAndValueBox) { +TEST(UIEditorNumberFieldTest, HitTestResolvesValueBoxAndRow) { UIEditorNumberFieldSpec spec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false }; const auto layout = BuildUIEditorNumberFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec); - EXPECT_EQ( - HitTestUIEditorNumberField(layout, UIPoint(layout.decrementRect.x + 2.0f, layout.decrementRect.y + 2.0f)).kind, - UIEditorNumberFieldHitTargetKind::DecrementButton); - EXPECT_EQ( - HitTestUIEditorNumberField(layout, UIPoint(layout.incrementRect.x + 2.0f, layout.incrementRect.y + 2.0f)).kind, - UIEditorNumberFieldHitTargetKind::IncrementButton); EXPECT_EQ( HitTestUIEditorNumberField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind, UIEditorNumberFieldHitTargetKind::ValueBox); + EXPECT_EQ( + HitTestUIEditorNumberField(layout, UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f)).kind, + UIEditorNumberFieldHitTargetKind::Row); } } // namespace diff --git a/tests/UI/Editor/unit/test_ui_editor_number_field_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_number_field_interaction.cpp index b502252f..9a93a5bb 100644 --- a/tests/UI/Editor/unit/test_ui_editor_number_field_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_number_field_interaction.cpp @@ -40,7 +40,7 @@ UIInputEvent MakeCharacter(char character) { } // namespace -TEST(UIEditorNumberFieldInteractionTest, ClickStepperButtonsAdjustValue) { +TEST(UIEditorNumberFieldInteractionTest, ClickValueBoxStartsEditing) { UIEditorNumberFieldSpec spec = { "queue", "Queue", 2.0, 1.0, 0.0, 5.0, true, false }; UIEditorNumberFieldInteractionState state = {}; @@ -57,20 +57,19 @@ TEST(UIEditorNumberFieldInteractionTest, ClickStepperButtonsAdjustValue) { { MakePointer( UIInputEventType::PointerButtonDown, - frame.layout.incrementRect.x + 2.0f, - frame.layout.incrementRect.y + 2.0f, + frame.layout.valueRect.x + 2.0f, + frame.layout.valueRect.y + 2.0f, UIPointerButton::Left), MakePointer( UIInputEventType::PointerButtonUp, - frame.layout.incrementRect.x + 2.0f, - frame.layout.incrementRect.y + 2.0f, + frame.layout.valueRect.x + 2.0f, + frame.layout.valueRect.y + 2.0f, UIPointerButton::Left) }); - EXPECT_TRUE(frame.result.stepApplied); - EXPECT_TRUE(frame.result.valueChanged); - EXPECT_DOUBLE_EQ(spec.value, 3.0); - EXPECT_DOUBLE_EQ(frame.result.valueAfter, 3.0); + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.numberFieldState.editing); + EXPECT_DOUBLE_EQ(spec.value, 2.0); } TEST(UIEditorNumberFieldInteractionTest, KeyboardStepAndBoundsWorkWhenFocused) { diff --git a/tests/UI/Editor/unit/test_ui_editor_property_grid.cpp b/tests/UI/Editor/unit/test_ui_editor_property_grid.cpp index 942e5447..50047444 100644 --- a/tests/UI/Editor/unit/test_ui_editor_property_grid.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_property_grid.cpp @@ -16,7 +16,9 @@ using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridFieldLocation; using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridSectionIndex; using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridVisibleFieldIndex; using XCEngine::UI::Editor::Widgets::HitTestUIEditorPropertyGrid; +using XCEngine::UI::Editor::Widgets::ResolveUIEditorPropertyGridFieldValueText; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField; +using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTargetKind; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridState; @@ -36,23 +38,80 @@ bool ContainsTextCommand( return false; } +UIEditorPropertyGridField MakeTextField( + std::string id, + std::string label, + std::string value, + bool readOnly = false) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.valueText = std::move(value); + field.readOnly = readOnly; + return field; +} + +UIEditorPropertyGridField MakeBoolField( + std::string id, + std::string label, + bool value) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Bool; + field.boolValue = value; + return field; +} + +UIEditorPropertyGridField MakeNumberField( + std::string id, + std::string label, + double value, + bool integerMode = true) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Number; + field.numberValue.value = value; + field.numberValue.step = 1.0; + field.numberValue.minValue = 0.0; + field.numberValue.maxValue = 5000.0; + field.numberValue.integerMode = integerMode; + return field; +} + +UIEditorPropertyGridField MakeEnumField( + std::string id, + std::string label, + std::vector options, + std::size_t selectedIndex) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Enum; + field.enumValue.options = std::move(options); + field.enumValue.selectedIndex = selectedIndex; + return field; +} + std::vector BuildSections() { return { { - "transform", - "Transform", + "inspector", + "Inspector", { - { "position", "Position", "0, 0, 0", false, 0.0f }, - { "rotation", "Rotation", "0, 45, 0", false, 0.0f } + MakeBoolField("enabled", "Enabled", true), + MakeNumberField("render_queue", "Render Queue", 2000.0), + MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u), + MakeTextField("tag", "Tag", "Player") }, 0.0f }, { - "rendering", - "Rendering", + "metadata", + "Metadata", { - { "material", "Material", "Metal", false, 0.0f }, - { "guid", "GUID", "asset-guid-001", true, 0.0f } + MakeTextField("guid", "GUID", "asset-guid-001", true) }, 0.0f } @@ -68,55 +127,64 @@ UIPoint RectCenter(const XCEngine::UI::UIRect& rect) { TEST(UIEditorPropertyGridTest, FindSectionAndFieldLocationReturnStableIndices) { const auto sections = BuildSections(); - EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "transform"), 0u); - EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "rendering"), 1u); + EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "inspector"), 0u); + EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "metadata"), 1u); EXPECT_EQ( FindUIEditorPropertyGridSectionIndex(sections, "missing"), static_cast(-1)); - const auto materialLocation = - FindUIEditorPropertyGridFieldLocation(sections, "material"); - EXPECT_TRUE(materialLocation.IsValid()); - EXPECT_EQ(materialLocation.sectionIndex, 1u); - EXPECT_EQ(materialLocation.fieldIndex, 0u); + const auto renderModeLocation = + FindUIEditorPropertyGridFieldLocation(sections, "render_mode"); + EXPECT_TRUE(renderModeLocation.IsValid()); + EXPECT_EQ(renderModeLocation.sectionIndex, 0u); + EXPECT_EQ(renderModeLocation.fieldIndex, 2u); const auto missingLocation = FindUIEditorPropertyGridFieldLocation(sections, "unknown"); EXPECT_FALSE(missingLocation.IsValid()); } -TEST(UIEditorPropertyGridTest, LayoutBuildsSectionHeadersAndVisibleFieldRects) { +TEST(UIEditorPropertyGridTest, LayoutBuildsTypedFieldRectsWithStableColumns) { const auto sections = BuildSections(); UIExpansionModel expansionModel = {}; - expansionModel.Expand("transform"); + expansionModel.Expand("inspector"); const auto layout = BuildUIEditorPropertyGridLayout( - UIRect(10.0f, 20.0f, 420.0f, 240.0f), + UIRect(10.0f, 20.0f, 420.0f, 260.0f), sections, expansionModel); ASSERT_EQ(layout.sectionHeaderRects.size(), sections.size()); - EXPECT_EQ(layout.visibleFieldIndices.size(), 2u); + ASSERT_EQ(layout.visibleFieldIndices.size(), 4u); EXPECT_EQ(layout.visibleFieldSectionIndices[0], 0u); - EXPECT_EQ(layout.visibleFieldIndices[1], 1u); EXPECT_FLOAT_EQ(layout.sectionHeaderRects[0].x, 18.0f); EXPECT_FLOAT_EQ(layout.sectionHeaderRects[0].y, 28.0f); - EXPECT_FLOAT_EQ(layout.fieldValueRects[0].x, 254.0f); - EXPECT_GT(layout.fieldValueRects[0].width, 0.0f); EXPECT_EQ( - FindUIEditorPropertyGridVisibleFieldIndex(layout, "position", sections), + FindUIEditorPropertyGridVisibleFieldIndex(layout, "enabled", sections), 0u); EXPECT_EQ( - FindUIEditorPropertyGridVisibleFieldIndex(layout, "material", sections), + FindUIEditorPropertyGridVisibleFieldIndex(layout, "guid", sections), static_cast(-1)); + + EXPECT_FLOAT_EQ(layout.fieldLabelRects[0].x, layout.fieldRowRects[0].x + 12.0f); + EXPECT_FLOAT_EQ(layout.fieldValueRects[0].x, layout.fieldRowRects[0].x + 236.0f); + EXPECT_FLOAT_EQ(layout.fieldValueRects[0].y, layout.fieldRowRects[0].y); + EXPECT_FLOAT_EQ(layout.fieldValueRects[1].x, layout.fieldRowRects[1].x + 236.0f); + EXPECT_FLOAT_EQ(layout.fieldValueRects[1].y, layout.fieldRowRects[1].y + 4.0f); + EXPECT_FLOAT_EQ(layout.fieldValueRects[2].x, layout.fieldRowRects[2].x + 236.0f); + EXPECT_FLOAT_EQ(layout.fieldValueRects[2].y, layout.fieldRowRects[2].y + 4.0f); + EXPECT_FLOAT_EQ(layout.fieldValueRects[3].x, layout.fieldRowRects[3].x + 236.0f); + EXPECT_FLOAT_EQ(layout.fieldValueRects[3].y, layout.fieldRowRects[3].y + 4.0f); + EXPECT_GT(layout.fieldValueRects[2].width, 0.0f); + EXPECT_GT(layout.fieldValueRects[3].width, 0.0f); } -TEST(UIEditorPropertyGridTest, HitTestResolvesHeaderRowAndValueBox) { +TEST(UIEditorPropertyGridTest, HitTestResolvesHeaderRowAndTypedValueHosts) { const auto sections = BuildSections(); UIExpansionModel expansionModel = {}; - expansionModel.Expand("transform"); - expansionModel.Expand("rendering"); + expansionModel.Expand("inspector"); + expansionModel.Expand("metadata"); const auto layout = BuildUIEditorPropertyGridLayout( UIRect(0.0f, 0.0f, 420.0f, 320.0f), @@ -134,32 +202,46 @@ TEST(UIEditorPropertyGridTest, HitTestResolvesHeaderRowAndValueBox) { layout.fieldRowRects[1].x + 16.0f, layout.fieldRowRects[1].y + layout.fieldRowRects[1].height * 0.5f)); EXPECT_EQ(rowHit.kind, UIEditorPropertyGridHitTargetKind::FieldRow); - EXPECT_EQ(rowHit.sectionIndex, 0u); EXPECT_EQ(rowHit.fieldIndex, 1u); - const auto valueHit = + const auto boolValueHit = + HitTestUIEditorPropertyGrid(layout, RectCenter(layout.fieldValueRects[0])); + EXPECT_EQ(boolValueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox); + EXPECT_EQ(boolValueHit.fieldIndex, 0u); + + const auto boolTrailingHit = HitTestUIEditorPropertyGrid( + layout, + UIPoint( + layout.fieldValueRects[0].x + layout.fieldValueRects[0].width - 6.0f, + layout.fieldValueRects[0].y + layout.fieldValueRects[0].height * 0.5f)); + EXPECT_EQ(boolTrailingHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox); + EXPECT_EQ(boolTrailingHit.fieldIndex, 0u); + + const auto enumValueHit = HitTestUIEditorPropertyGrid(layout, RectCenter(layout.fieldValueRects[2])); - EXPECT_EQ(valueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox); - EXPECT_EQ(valueHit.sectionIndex, 1u); - EXPECT_EQ(valueHit.fieldIndex, 0u); + EXPECT_EQ(enumValueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox); + EXPECT_EQ(enumValueHit.fieldIndex, 2u); } -TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitStableCommands) { +TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitTypedCommandsAndPopupOverlay) { const auto sections = BuildSections(); UISelectionModel selectionModel = {}; - selectionModel.SetSelection("rotation"); + selectionModel.SetSelection("render_mode"); UIExpansionModel expansionModel = {}; - expansionModel.Expand("transform"); - expansionModel.Expand("rendering"); + expansionModel.Expand("inspector"); + expansionModel.Expand("metadata"); UIPropertyEditModel propertyEditModel = {}; - propertyEditModel.BeginEdit("material", "Metal"); - propertyEditModel.UpdateStagedValue("Mat_Inst"); + propertyEditModel.BeginEdit("tag", "Player"); + propertyEditModel.UpdateStagedValue("Hero"); UIEditorPropertyGridState state = {}; state.focused = true; - state.hoveredFieldId = "position"; + state.hoveredFieldId = "enabled"; + state.hoveredHitTarget = UIEditorPropertyGridHitTargetKind::ValueBox; + state.popupFieldId = "render_mode"; + state.popupHighlightedIndex = 1u; const auto layout = BuildUIEditorPropertyGridLayout( - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, expansionModel); @@ -176,14 +258,19 @@ TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitStableCommands) { drawList, layout, sections, + state, propertyEditModel); const auto& commands = drawList.GetCommands(); - ASSERT_GE(commands.size(), 12u); + ASSERT_GE(commands.size(), 16u); EXPECT_EQ(commands[0].type, XCEngine::UI::UIDrawCommandType::FilledRect); EXPECT_EQ(commands[1].type, XCEngine::UI::UIDrawCommandType::RectOutline); - EXPECT_TRUE(ContainsTextCommand(drawData, "Transform")); - EXPECT_TRUE(ContainsTextCommand(drawData, "Position")); - EXPECT_TRUE(ContainsTextCommand(drawData, "Mat_Inst")); + EXPECT_TRUE(ContainsTextCommand(drawData, "Inspector")); + EXPECT_TRUE(ContainsTextCommand(drawData, "Enabled")); + EXPECT_TRUE(ContainsTextCommand(drawData, "Render Queue")); + EXPECT_TRUE(ContainsTextCommand(drawData, "Opaque")); + EXPECT_TRUE(ContainsTextCommand(drawData, "Hero")); EXPECT_TRUE(ContainsTextCommand(drawData, "EDIT")); + EXPECT_TRUE(ContainsTextCommand(drawData, "Cutout")); + EXPECT_EQ(ResolveUIEditorPropertyGridFieldValueText(sections[0].fields[1]), "2000"); } diff --git a/tests/UI/Editor/unit/test_ui_editor_property_grid_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_property_grid_interaction.cpp index eee58050..18c3281c 100644 --- a/tests/UI/Editor/unit/test_ui_editor_property_grid_interaction.cpp +++ b/tests/UI/Editor/unit/test_ui_editor_property_grid_interaction.cpp @@ -17,16 +17,75 @@ using XCEngine::UI::Widgets::UIPropertyEditModel; using XCEngine::UI::Widgets::UISelectionModel; using XCEngine::UI::Editor::UIEditorPropertyGridInteractionState; using XCEngine::UI::Editor::UpdateUIEditorPropertyGridInteraction; +using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField; +using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind; using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection; +UIEditorPropertyGridField MakeTextField( + std::string id, + std::string label, + std::string value, + bool readOnly = false) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.valueText = std::move(value); + field.readOnly = readOnly; + return field; +} + +UIEditorPropertyGridField MakeBoolField( + std::string id, + std::string label, + bool value) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Bool; + field.boolValue = value; + return field; +} + +UIEditorPropertyGridField MakeNumberField( + std::string id, + std::string label, + double value) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Number; + field.numberValue.value = value; + field.numberValue.step = 1.0; + field.numberValue.minValue = 0.0; + field.numberValue.maxValue = 5000.0; + field.numberValue.integerMode = true; + return field; +} + +UIEditorPropertyGridField MakeEnumField( + std::string id, + std::string label, + std::vector options, + std::size_t selectedIndex) { + UIEditorPropertyGridField field = {}; + field.fieldId = std::move(id); + field.label = std::move(label); + field.kind = UIEditorPropertyGridFieldKind::Enum; + field.enumValue.options = std::move(options); + field.enumValue.selectedIndex = selectedIndex; + return field; +} + std::vector BuildSections() { return { { - "transform", - "Transform", + "inspector", + "Inspector", { - { "position", "Position", "10", false, 0.0f }, - { "rotation", "Rotation", "0", false, 0.0f } + MakeBoolField("enabled", "Enabled", true), + MakeNumberField("render_queue", "Render Queue", 2000.0), + MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u), + MakeTextField("tag", "Tag", "Player") }, 0.0f }, @@ -34,8 +93,7 @@ std::vector BuildSections() { "metadata", "Metadata", { - { "tag", "Tag", "", false, 0.0f }, - { "guid", "GUID", "asset-guid-001", true, 0.0f } + MakeTextField("guid", "GUID", "asset-guid-001", true) }, 0.0f } @@ -84,14 +142,14 @@ UIPoint RectCenter(const XCEngine::UI::UIRect& rect) { } void ExpandAll(UIExpansionModel& expansionModel) { - expansionModel.Expand("transform"); + expansionModel.Expand("inspector"); expansionModel.Expand("metadata"); } } // namespace TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndField) { - const auto sections = BuildSections(); + auto sections = BuildSections(); UISelectionModel selectionModel = {}; UIExpansionModel expansionModel = {}; ExpandAll(expansionModel); @@ -103,7 +161,7 @@ TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndFie selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, {}); @@ -112,12 +170,12 @@ TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndFie selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { MakePointerMove( initialFrame.layout.sectionHeaderRects[0].x + 24.0f, initialFrame.layout.sectionHeaderRects[0].y + 16.0f) }); - EXPECT_EQ(state.propertyGridState.hoveredSectionId, "transform"); + EXPECT_EQ(state.propertyGridState.hoveredSectionId, "inspector"); EXPECT_TRUE(state.propertyGridState.hoveredFieldId.empty()); frame = UpdateUIEditorPropertyGridInteraction( @@ -125,20 +183,20 @@ TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndFie selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { MakePointerMove( initialFrame.layout.fieldRowRects[1].x + 16.0f, initialFrame.layout.fieldRowRects[1].y + 16.0f) }); EXPECT_EQ(frame.result.hitTarget.fieldIndex, 1u); - EXPECT_EQ(state.propertyGridState.hoveredFieldId, "rotation"); + EXPECT_EQ(state.propertyGridState.hoveredFieldId, "render_queue"); } TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion) { - const auto sections = BuildSections(); + auto sections = BuildSections(); UISelectionModel selectionModel = {}; UIExpansionModel expansionModel = {}; - expansionModel.Expand("transform"); + expansionModel.Expand("inspector"); UIPropertyEditModel propertyEditModel = {}; UIEditorPropertyGridInteractionState state = {}; @@ -147,7 +205,7 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, {}); const UIPoint metadataHeaderCenter = RectCenter(initialFrame.layout.sectionHeaderRects[1]); @@ -157,7 +215,7 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { MakePointerDown(metadataHeaderCenter.x, metadataHeaderCenter.y), @@ -170,8 +228,8 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion EXPECT_TRUE(state.propertyGridState.focused); } -TEST(UIEditorPropertyGridInteractionTest, LeftClickFieldRowSelectsFieldAndFocusesGrid) { - const auto sections = BuildSections(); +TEST(UIEditorPropertyGridInteractionTest, LeftClickBoolValueHostTogglesValueAndSelectsField) { + auto sections = BuildSections(); UISelectionModel selectionModel = {}; UIExpansionModel expansionModel = {}; ExpandAll(expansionModel); @@ -183,32 +241,34 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickFieldRowSelectsFieldAndFocuse selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, {}); - const UIPoint rowCenter = RectCenter(initialFrame.layout.fieldRowRects[1]); + const UIPoint boolValueCenter = RectCenter(initialFrame.layout.fieldValueRects[0]); const auto frame = UpdateUIEditorPropertyGridInteraction( state, selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { - MakePointerDown(rowCenter.x, rowCenter.y), - MakePointerUp(rowCenter.x, rowCenter.y) + MakePointerDown(boolValueCenter.x, boolValueCenter.y), + MakePointerUp(boolValueCenter.x, boolValueCenter.y) }); - EXPECT_TRUE(frame.result.consumed); EXPECT_TRUE(frame.result.selectionChanged); - EXPECT_EQ(frame.result.selectedFieldId, "rotation"); - EXPECT_TRUE(selectionModel.IsSelected("rotation")); - EXPECT_TRUE(state.propertyGridState.focused); + EXPECT_TRUE(frame.result.fieldValueChanged); + EXPECT_EQ(frame.result.selectedFieldId, "enabled"); + EXPECT_EQ(frame.result.changedFieldId, "enabled"); + EXPECT_EQ(frame.result.changedValue, "false"); + EXPECT_FALSE(sections[0].fields[0].boolValue); + EXPECT_TRUE(selectionModel.IsSelected("enabled")); } -TEST(UIEditorPropertyGridInteractionTest, ValueBoxEditCanCommitWithEnter) { - const auto sections = BuildSections(); +TEST(UIEditorPropertyGridInteractionTest, NumberValueBoxEditCanCommitWithEnter) { + auto sections = BuildSections(); UISelectionModel selectionModel = {}; UIExpansionModel expansionModel = {}; ExpandAll(expansionModel); @@ -220,25 +280,25 @@ TEST(UIEditorPropertyGridInteractionTest, ValueBoxEditCanCommitWithEnter) { selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, {}); - const UIPoint tagValueCenter = RectCenter(initialFrame.layout.fieldValueRects[2]); + const UIPoint numberValueCenter = RectCenter(initialFrame.layout.fieldValueRects[1]); auto frame = UpdateUIEditorPropertyGridInteraction( state, selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { - MakePointerDown(tagValueCenter.x, tagValueCenter.y), - MakePointerUp(tagValueCenter.x, tagValueCenter.y) + MakePointerDown(numberValueCenter.x, numberValueCenter.y), + MakePointerUp(numberValueCenter.x, numberValueCenter.y) }); EXPECT_TRUE(frame.result.consumed); - EXPECT_EQ(frame.result.selectedFieldId, "tag"); - EXPECT_EQ(frame.result.activeFieldId, "tag"); + EXPECT_EQ(frame.result.selectedFieldId, "render_queue"); + EXPECT_EQ(frame.result.activeFieldId, "render_queue"); EXPECT_TRUE(propertyEditModel.HasActiveEdit()); frame = UpdateUIEditorPropertyGridInteraction( @@ -246,22 +306,32 @@ TEST(UIEditorPropertyGridInteractionTest, ValueBoxEditCanCommitWithEnter) { selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { - MakeCharacter('A'), - MakeCharacter('B'), + MakeKeyDown(KeyCode::Backspace), + MakeKeyDown(KeyCode::Backspace), + MakeKeyDown(KeyCode::Backspace), + MakeKeyDown(KeyCode::Backspace), + MakeCharacter('2'), + MakeCharacter('5'), + MakeCharacter('0'), + MakeCharacter('0'), MakeKeyDown(KeyCode::Enter) }); EXPECT_TRUE(frame.result.editCommitted); - EXPECT_EQ(frame.result.committedFieldId, "tag"); - EXPECT_EQ(frame.result.committedValue, "AB"); + EXPECT_TRUE(frame.result.fieldValueChanged); + EXPECT_EQ(frame.result.committedFieldId, "render_queue"); + EXPECT_EQ(frame.result.committedValue, "2500"); + EXPECT_EQ(frame.result.changedFieldId, "render_queue"); + EXPECT_EQ(frame.result.changedValue, "2500"); EXPECT_FALSE(propertyEditModel.HasActiveEdit()); + EXPECT_DOUBLE_EQ(sections[0].fields[1].numberValue.value, 2500.0); } -TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClearsFocus) { - const auto sections = BuildSections(); +TEST(UIEditorPropertyGridInteractionTest, EnumPopupCanOpenNavigateSelectAndClose) { + auto sections = BuildSections(); UISelectionModel selectionModel = {}; UIExpansionModel expansionModel = {}; ExpandAll(expansionModel); @@ -273,17 +343,70 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, {}); - const UIPoint tagValueCenter = RectCenter(initialFrame.layout.fieldValueRects[2]); + const UIPoint enumValueCenter = RectCenter(initialFrame.layout.fieldValueRects[2]); + + auto frame = UpdateUIEditorPropertyGridInteraction( + state, + selectionModel, + expansionModel, + propertyEditModel, + UIRect(0.0f, 0.0f, 420.0f, 340.0f), + sections, + { + MakePointerDown(enumValueCenter.x, enumValueCenter.y), + MakePointerUp(enumValueCenter.x, enumValueCenter.y) + }); + EXPECT_TRUE(frame.result.popupOpened); + EXPECT_EQ(state.propertyGridState.popupFieldId, "render_mode"); + EXPECT_TRUE(selectionModel.IsSelected("render_mode")); + + frame = UpdateUIEditorPropertyGridInteraction( + state, + selectionModel, + expansionModel, + propertyEditModel, + UIRect(0.0f, 0.0f, 420.0f, 340.0f), + sections, + { + MakeKeyDown(KeyCode::Down), + MakeKeyDown(KeyCode::Enter) + }); + + EXPECT_TRUE(frame.result.popupClosed); + EXPECT_TRUE(frame.result.fieldValueChanged); + EXPECT_EQ(frame.result.changedFieldId, "render_mode"); + EXPECT_EQ(frame.result.changedValue, "Cutout"); + EXPECT_TRUE(state.propertyGridState.popupFieldId.empty()); + EXPECT_EQ(sections[0].fields[2].enumValue.selectedIndex, 1u); +} + +TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClearsFocus) { + auto sections = BuildSections(); + UISelectionModel selectionModel = {}; + UIExpansionModel expansionModel = {}; + ExpandAll(expansionModel); + UIPropertyEditModel propertyEditModel = {}; + UIEditorPropertyGridInteractionState state = {}; + + const auto initialFrame = UpdateUIEditorPropertyGridInteraction( + state, + selectionModel, + expansionModel, + propertyEditModel, + UIRect(0.0f, 0.0f, 420.0f, 340.0f), + sections, + {}); + const UIPoint tagValueCenter = RectCenter(initialFrame.layout.fieldValueRects[3]); UpdateUIEditorPropertyGridInteraction( state, selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { MakePointerDown(tagValueCenter.x, tagValueCenter.y), @@ -295,7 +418,7 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { MakeCharacter('A'), @@ -303,14 +426,14 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears }); EXPECT_TRUE(frame.result.editCanceled); EXPECT_FALSE(propertyEditModel.HasActiveEdit()); - EXPECT_TRUE(selectionModel.IsSelected("tag")); + EXPECT_EQ(sections[0].fields[3].valueText, "Player"); frame = UpdateUIEditorPropertyGridInteraction( state, selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { MakePointerDown(520.0f, 360.0f), @@ -321,9 +444,9 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears } TEST(UIEditorPropertyGridInteractionTest, ArrowAndHomeEndKeysNavigateVisibleFields) { - const auto sections = BuildSections(); + auto sections = BuildSections(); UISelectionModel selectionModel = {}; - selectionModel.SetSelection("rotation"); + selectionModel.SetSelection("render_queue"); UIExpansionModel expansionModel = {}; ExpandAll(expansionModel); UIPropertyEditModel propertyEditModel = {}; @@ -335,31 +458,31 @@ TEST(UIEditorPropertyGridInteractionTest, ArrowAndHomeEndKeysNavigateVisibleFiel selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { MakeKeyDown(KeyCode::Down) }); EXPECT_TRUE(frame.result.keyboardNavigated); - EXPECT_EQ(frame.result.selectedFieldId, "tag"); - EXPECT_TRUE(selectionModel.IsSelected("tag")); + EXPECT_EQ(frame.result.selectedFieldId, "render_mode"); + EXPECT_TRUE(selectionModel.IsSelected("render_mode")); frame = UpdateUIEditorPropertyGridInteraction( state, selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { MakeKeyDown(KeyCode::Home) }); EXPECT_TRUE(frame.result.keyboardNavigated); - EXPECT_EQ(frame.result.selectedFieldId, "position"); - EXPECT_TRUE(selectionModel.IsSelected("position")); + EXPECT_EQ(frame.result.selectedFieldId, "enabled"); + EXPECT_TRUE(selectionModel.IsSelected("enabled")); frame = UpdateUIEditorPropertyGridInteraction( state, selectionModel, expansionModel, propertyEditModel, - UIRect(0.0f, 0.0f, 420.0f, 320.0f), + UIRect(0.0f, 0.0f, 420.0f, 340.0f), sections, { MakeKeyDown(KeyCode::End) }); EXPECT_TRUE(frame.result.keyboardNavigated); diff --git a/tests/UI/Editor/unit/test_ui_editor_text_field.cpp b/tests/UI/Editor/unit/test_ui_editor_text_field.cpp new file mode 100644 index 00000000..6bf6d992 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_text_field.cpp @@ -0,0 +1,36 @@ +#include + +#include + +namespace { + +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Widgets::BuildUIEditorTextFieldLayout; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField; +using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec; + +TEST(UIEditorTextFieldTest, LayoutBuildsValueRect) { + UIEditorTextFieldSpec spec = { "name", "Name", "Player", false }; + const auto layout = BuildUIEditorTextFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec); + + EXPECT_GT(layout.labelRect.width, 0.0f); + EXPECT_GT(layout.controlRect.width, 0.0f); + EXPECT_GT(layout.valueRect.width, 0.0f); + EXPECT_FLOAT_EQ(layout.controlRect.width, layout.valueRect.width); +} + +TEST(UIEditorTextFieldTest, HitTestResolvesValueBoxAndRow) { + UIEditorTextFieldSpec spec = { "name", "Name", "Player", false }; + const auto layout = BuildUIEditorTextFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec); + + EXPECT_EQ( + HitTestUIEditorTextField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind, + UIEditorTextFieldHitTargetKind::ValueBox); + EXPECT_EQ( + HitTestUIEditorTextField(layout, UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f)).kind, + UIEditorTextFieldHitTargetKind::Row); +} + +} // namespace diff --git a/tests/UI/Editor/unit/test_ui_editor_text_field_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_text_field_interaction.cpp new file mode 100644 index 00000000..a749571f --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_text_field_interaction.cpp @@ -0,0 +1,152 @@ +#include + +#include + +#include + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::UIEditorTextFieldInteractionState; +using XCEngine::UI::Editor::UpdateUIEditorTextFieldInteraction; +using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec; + +UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = UIPoint(x, y); + event.pointerButton = button; + return event; +} + +UIInputEvent MakeKey(KeyCode keyCode) { + UIInputEvent event = {}; + event.type = UIInputEventType::KeyDown; + event.keyCode = static_cast(keyCode); + return event; +} + +UIInputEvent MakeCharacter(char character) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(character); + return event; +} + +} // namespace + +TEST(UIEditorTextFieldInteractionTest, ClickValueBoxStartsEditing) { + UIEditorTextFieldSpec spec = { "name", "Name", "Player", false }; + UIEditorTextFieldInteractionState state = {}; + + auto frame = UpdateUIEditorTextFieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 360.0f, 32.0f), + {}); + + frame = UpdateUIEditorTextFieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 360.0f, 32.0f), + { + MakePointer( + UIInputEventType::PointerButtonDown, + frame.layout.valueRect.x + 2.0f, + frame.layout.valueRect.y + 2.0f, + UIPointerButton::Left), + MakePointer( + UIInputEventType::PointerButtonUp, + frame.layout.valueRect.x + 2.0f, + frame.layout.valueRect.y + 2.0f, + UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.textFieldState.editing); + EXPECT_EQ(spec.value, "Player"); +} + +TEST(UIEditorTextFieldInteractionTest, EnterStartsEditingAndCommitUpdatesValue) { + UIEditorTextFieldSpec spec = { "name", "Name", "Player", false }; + UIEditorTextFieldInteractionState state = {}; + state.textFieldState.focused = true; + + auto frame = UpdateUIEditorTextFieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 360.0f, 32.0f), + { MakeKey(KeyCode::Enter) }); + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.textFieldState.editing); + + frame = UpdateUIEditorTextFieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 360.0f, 32.0f), + { MakeCharacter('X'), MakeKey(KeyCode::Enter) }); + + EXPECT_TRUE(frame.result.editCommitted); + EXPECT_TRUE(frame.result.valueChanged); + EXPECT_FALSE(state.textFieldState.editing); + EXPECT_EQ(spec.value, "PlayerX"); + EXPECT_EQ(frame.result.committedText, "PlayerX"); +} + +TEST(UIEditorTextFieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) { + UIEditorTextFieldSpec spec = { "name", "Name", "Player", false }; + UIEditorTextFieldInteractionState state = {}; + state.textFieldState.focused = true; + + auto frame = UpdateUIEditorTextFieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 360.0f, 32.0f), + { MakeCharacter('N') }); + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.textFieldState.editing); + EXPECT_EQ(state.textFieldState.displayText, "N"); + + frame = UpdateUIEditorTextFieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 360.0f, 32.0f), + { MakeKey(KeyCode::Escape) }); + EXPECT_TRUE(frame.result.editCanceled); + EXPECT_FALSE(state.textFieldState.editing); + EXPECT_EQ(spec.value, "Player"); + EXPECT_EQ(state.textFieldState.displayText, "Player"); +} + +TEST(UIEditorTextFieldInteractionTest, FocusLostCommitsEdit) { + UIEditorTextFieldSpec spec = { "name", "Name", "Player", false }; + UIEditorTextFieldInteractionState state = {}; + state.textFieldState.focused = true; + + auto frame = UpdateUIEditorTextFieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 360.0f, 32.0f), + { MakeKey(KeyCode::Enter), MakeCharacter('1') }); + EXPECT_TRUE(state.textFieldState.editing); + + frame = UpdateUIEditorTextFieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 360.0f, 32.0f), + { + UIInputEvent { + .type = UIInputEventType::FocusLost + } + }); + + EXPECT_TRUE(frame.result.editCommitted); + EXPECT_EQ(spec.value, "Player1"); + EXPECT_FALSE(state.textFieldState.editing); + EXPECT_FALSE(state.textFieldState.focused); +} diff --git a/tests/UI/Editor/unit/test_ui_editor_theme.cpp b/tests/UI/Editor/unit/test_ui_editor_theme.cpp new file mode 100644 index 00000000..93592156 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_theme.cpp @@ -0,0 +1,371 @@ +#include + +#include + +#include +#include +#include + +namespace { + +namespace Math = XCEngine::Math; +namespace Style = XCEngine::UI::Style; +namespace UI = XCEngine::UI; +namespace Editor = XCEngine::UI::Editor; + +Style::UITheme BuildEditorFieldTheme() { + Style::UIThemeDefinition definition = {}; + definition.SetToken("editor.size.field.row", Style::UIStyleValue(26.0f)); + definition.SetToken("editor.space.field.padding_x", Style::UIStyleValue(8.0f)); + definition.SetToken("editor.space.field.label_gap", Style::UIStyleValue(12.0f)); + definition.SetToken("editor.layout.field.control_column", Style::UIStyleValue(220.0f)); + definition.SetToken("editor.size.field.checkbox", Style::UIStyleValue(15.0f)); + definition.SetToken("editor.space.field.control_inset_y", Style::UIStyleValue(3.0f)); + definition.SetToken("editor.space.field.label_inset_y", Style::UIStyleValue(5.0f)); + definition.SetToken("editor.space.field.value_inset_x", Style::UIStyleValue(6.0f)); + definition.SetToken("editor.space.field.value_inset_y", Style::UIStyleValue(4.0f)); + definition.SetToken("editor.space.field.checkbox_glyph_inset_x", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.space.field.checkbox_glyph_inset_y", Style::UIStyleValue(-2.0f)); + definition.SetToken("editor.size.field.control_min_width", Style::UIStyleValue(88.0f)); + definition.SetToken("editor.space.field.vector_component_gap", Style::UIStyleValue(6.0f)); + definition.SetToken("editor.size.field.vector_component_min_width", Style::UIStyleValue(74.0f)); + definition.SetToken("editor.size.field.vector_prefix_width", Style::UIStyleValue(18.0f)); + definition.SetToken("editor.space.field.control_trailing_inset", Style::UIStyleValue(9.0f)); + definition.SetToken("editor.space.field.vector_prefix_gap", Style::UIStyleValue(5.0f)); + definition.SetToken("editor.space.field.vector_prefix_inset_x", Style::UIStyleValue(4.0f)); + definition.SetToken("editor.space.field.vector_prefix_inset_y", Style::UIStyleValue(5.0f)); + definition.SetToken("editor.size.field.dropdown_arrow_width", Style::UIStyleValue(14.0f)); + definition.SetToken("editor.space.field.dropdown_arrow_inset_x", Style::UIStyleValue(4.0f)); + definition.SetToken("editor.space.field.dropdown_arrow_inset_y", Style::UIStyleValue(4.0f)); + definition.SetToken("editor.radius.field.row", Style::UIStyleValue(2.0f)); + definition.SetToken("editor.radius.field.control", Style::UIStyleValue(2.0f)); + definition.SetToken("editor.border.field", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.border.field.focus", Style::UIStyleValue(2.0f)); + definition.SetToken("editor.font.field.label", Style::UIStyleValue(11.0f)); + definition.SetToken("editor.font.field.value", Style::UIStyleValue(12.0f)); + definition.SetToken("editor.font.field.glyph", Style::UIStyleValue(10.0f)); + definition.SetToken("editor.space.menu_popup.padding_x", Style::UIStyleValue(6.0f)); + definition.SetToken("editor.space.menu_popup.padding_y", Style::UIStyleValue(5.0f)); + definition.SetToken("editor.size.menu_popup.item", Style::UIStyleValue(24.0f)); + definition.SetToken("editor.size.menu_popup.separator", Style::UIStyleValue(8.0f)); + definition.SetToken("editor.size.menu_popup.check_column", Style::UIStyleValue(16.0f)); + definition.SetToken("editor.space.menu_popup.shortcut_gap", Style::UIStyleValue(18.0f)); + definition.SetToken("editor.size.menu_popup.submenu_indicator", Style::UIStyleValue(12.0f)); + definition.SetToken("editor.radius.menu_popup.row", Style::UIStyleValue(3.0f)); + definition.SetToken("editor.radius.menu_popup.surface", Style::UIStyleValue(4.0f)); + definition.SetToken("editor.space.menu_popup.label_inset_x", Style::UIStyleValue(10.0f)); + definition.SetToken("editor.space.menu_popup.label_inset_y", Style::UIStyleValue(-1.0f)); + definition.SetToken("editor.font.menu_popup.label", Style::UIStyleValue(11.0f)); + definition.SetToken("editor.space.menu_popup.shortcut_inset_right", Style::UIStyleValue(18.0f)); + definition.SetToken("editor.size.menu_popup.estimated_glyph_width", Style::UIStyleValue(6.0f)); + definition.SetToken("editor.font.menu_popup.glyph", Style::UIStyleValue(10.0f)); + definition.SetToken("editor.border.menu_popup.separator", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.border.menu_popup.surface", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.color.field.row", Style::UIStyleValue(Math::Color(0.16f, 0.16f, 0.16f, 1.0f))); + definition.SetToken("editor.color.field.row_hover", Style::UIStyleValue(Math::Color(0.20f, 0.20f, 0.20f, 1.0f))); + definition.SetToken("editor.color.field.row_active", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f))); + definition.SetToken("editor.color.field.border", Style::UIStyleValue(Math::Color(0.12f, 0.12f, 0.12f, 1.0f))); + definition.SetToken("editor.color.field.border_focus", Style::UIStyleValue(Math::Color(0.78f, 0.78f, 0.78f, 1.0f))); + definition.SetToken("editor.color.field.label", Style::UIStyleValue(Math::Color(0.84f, 0.84f, 0.84f, 1.0f))); + definition.SetToken("editor.color.field.value", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f))); + definition.SetToken("editor.color.field.value_readonly", Style::UIStyleValue(Math::Color(0.60f, 0.60f, 0.60f, 1.0f))); + definition.SetToken("editor.color.field.control", Style::UIStyleValue(Math::Color(0.18f, 0.18f, 0.18f, 1.0f))); + definition.SetToken("editor.color.field.control_hover", Style::UIStyleValue(Math::Color(0.21f, 0.21f, 0.21f, 1.0f))); + definition.SetToken("editor.color.field.control_editing", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f))); + definition.SetToken("editor.color.field.control_readonly", Style::UIStyleValue(Math::Color(0.15f, 0.15f, 0.15f, 1.0f))); + definition.SetToken("editor.color.field.control_border", Style::UIStyleValue(Math::Color(0.30f, 0.30f, 0.30f, 1.0f))); + definition.SetToken("editor.color.field.control_border_focus", Style::UIStyleValue(Math::Color(0.64f, 0.64f, 0.64f, 1.0f))); + definition.SetToken("editor.color.field.vector_prefix", Style::UIStyleValue(Math::Color(0.20f, 0.20f, 0.20f, 1.0f))); + definition.SetToken("editor.color.field.vector_prefix_border", Style::UIStyleValue(Math::Color(0.31f, 0.31f, 0.31f, 1.0f))); + definition.SetToken("editor.color.field.vector_axis_x", Style::UIStyleValue(Math::Color(0.78f, 0.42f, 0.42f, 1.0f))); + definition.SetToken("editor.color.field.vector_axis_y", Style::UIStyleValue(Math::Color(0.56f, 0.72f, 0.46f, 1.0f))); + definition.SetToken("editor.color.field.vector_axis_z", Style::UIStyleValue(Math::Color(0.45f, 0.62f, 0.82f, 1.0f))); + definition.SetToken("editor.color.field.checkbox", Style::UIStyleValue(Math::Color(0.19f, 0.19f, 0.19f, 1.0f))); + definition.SetToken("editor.color.field.checkbox_hover", Style::UIStyleValue(Math::Color(0.22f, 0.22f, 0.22f, 1.0f))); + definition.SetToken("editor.color.field.checkbox_border", Style::UIStyleValue(Math::Color(0.33f, 0.33f, 0.33f, 1.0f))); + definition.SetToken("editor.color.field.checkbox_mark", Style::UIStyleValue(Math::Color(0.90f, 0.90f, 0.90f, 1.0f))); + definition.SetToken("editor.color.field.dropdown_arrow", Style::UIStyleValue(Math::Color(0.88f, 0.88f, 0.88f, 1.0f))); + definition.SetToken("editor.color.menu_popup.surface", Style::UIStyleValue(Math::Color(0.15f, 0.15f, 0.15f, 1.0f))); + definition.SetToken("editor.color.menu_popup.border", Style::UIStyleValue(Math::Color(0.32f, 0.32f, 0.32f, 1.0f))); + definition.SetToken("editor.color.menu_popup.item_hover", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f))); + definition.SetToken("editor.color.menu_popup.item_open", Style::UIStyleValue(Math::Color(0.27f, 0.27f, 0.27f, 1.0f))); + definition.SetToken("editor.color.menu_popup.separator", Style::UIStyleValue(Math::Color(0.35f, 0.35f, 0.35f, 1.0f))); + definition.SetToken("editor.color.menu_popup.label", Style::UIStyleValue(Math::Color(0.91f, 0.91f, 0.91f, 1.0f))); + definition.SetToken("editor.color.menu_popup.shortcut", Style::UIStyleValue(Math::Color(0.76f, 0.76f, 0.76f, 1.0f))); + definition.SetToken("editor.color.menu_popup.text_disabled", Style::UIStyleValue(Math::Color(0.48f, 0.48f, 0.48f, 1.0f))); + definition.SetToken("editor.color.menu_popup.glyph", Style::UIStyleValue(Math::Color(0.86f, 0.86f, 0.86f, 1.0f))); + return Style::BuildTheme(definition); +} + +Style::UITheme BuildPropertyGridTheme() { + Style::UIThemeDefinition definition = {}; + definition.SetToken("editor.space.property.content_inset", Style::UIStyleValue(6.0f)); + definition.SetToken("editor.space.property.section_gap", Style::UIStyleValue(4.0f)); + definition.SetToken("editor.size.property.section_header", Style::UIStyleValue(24.0f)); + definition.SetToken("editor.size.property.field_row", Style::UIStyleValue(26.0f)); + definition.SetToken("editor.space.property.row_gap", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.size.property.disclosure", Style::UIStyleValue(10.0f)); + definition.SetToken("editor.space.property.disclosure_label_gap", Style::UIStyleValue(6.0f)); + definition.SetToken("editor.space.property.section_inset_y", Style::UIStyleValue(4.0f)); + definition.SetToken("editor.space.property.disclosure_glyph_inset_x", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.space.property.disclosure_glyph_inset_y", Style::UIStyleValue(-1.0f)); + definition.SetToken("editor.space.property.label_inset_y", Style::UIStyleValue(5.0f)); + definition.SetToken("editor.space.property.value_inset_y", Style::UIStyleValue(4.0f)); + definition.SetToken("editor.space.property.value_box_inset_y", Style::UIStyleValue(3.0f)); + definition.SetToken("editor.space.property.value_box_inset_x", Style::UIStyleValue(6.0f)); + definition.SetToken("editor.radius.property.panel", Style::UIStyleValue(0.0f)); + definition.SetToken("editor.radius.property.value", Style::UIStyleValue(2.0f)); + definition.SetToken("editor.border.property", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.border.property.focus", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.border.property.edit", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.font.property.section", Style::UIStyleValue(11.0f)); + definition.SetToken("editor.font.property.disclosure", Style::UIStyleValue(10.0f)); + definition.SetToken("editor.font.property.label", Style::UIStyleValue(11.0f)); + definition.SetToken("editor.font.property.value", Style::UIStyleValue(12.0f)); + definition.SetToken("editor.font.property.tag", Style::UIStyleValue(10.0f)); + definition.SetToken("editor.color.property.surface", Style::UIStyleValue(Math::Color(0.17f, 0.17f, 0.17f, 1.0f))); + definition.SetToken("editor.color.property.border", Style::UIStyleValue(Math::Color(0.10f, 0.10f, 0.10f, 1.0f))); + definition.SetToken("editor.color.property.border_focus", Style::UIStyleValue(Math::Color(0.75f, 0.75f, 0.75f, 1.0f))); + definition.SetToken("editor.color.property.section", Style::UIStyleValue(Math::Color(0.21f, 0.21f, 0.21f, 1.0f))); + definition.SetToken("editor.color.property.section_hover", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f))); + definition.SetToken("editor.color.property.field_hover", Style::UIStyleValue(Math::Color(0.22f, 0.22f, 0.22f, 1.0f))); + definition.SetToken("editor.color.property.field_selected", Style::UIStyleValue(Math::Color(0.26f, 0.26f, 0.26f, 1.0f))); + definition.SetToken("editor.color.property.field_selected_focused", Style::UIStyleValue(Math::Color(0.30f, 0.30f, 0.30f, 1.0f))); + definition.SetToken("editor.color.property.value", Style::UIStyleValue(Math::Color(0.18f, 0.18f, 0.18f, 1.0f))); + definition.SetToken("editor.color.property.value_hover", Style::UIStyleValue(Math::Color(0.21f, 0.21f, 0.21f, 1.0f))); + definition.SetToken("editor.color.property.value_editing", Style::UIStyleValue(Math::Color(0.23f, 0.23f, 0.23f, 1.0f))); + definition.SetToken("editor.color.property.value_readonly", Style::UIStyleValue(Math::Color(0.15f, 0.15f, 0.15f, 1.0f))); + definition.SetToken("editor.color.property.value_border", Style::UIStyleValue(Math::Color(0.28f, 0.28f, 0.28f, 1.0f))); + definition.SetToken("editor.color.property.value_border_editing", Style::UIStyleValue(Math::Color(0.72f, 0.72f, 0.72f, 1.0f))); + definition.SetToken("editor.color.property.disclosure", Style::UIStyleValue(Math::Color(0.84f, 0.84f, 0.84f, 1.0f))); + definition.SetToken("editor.color.property.section_text", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f))); + definition.SetToken("editor.color.property.label", Style::UIStyleValue(Math::Color(0.80f, 0.80f, 0.80f, 1.0f))); + definition.SetToken("editor.color.property.value_text", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f))); + definition.SetToken("editor.color.property.value_text_readonly", Style::UIStyleValue(Math::Color(0.60f, 0.60f, 0.60f, 1.0f))); + definition.SetToken("editor.color.property.edit_tag", Style::UIStyleValue(Math::Color(0.55f, 0.70f, 0.96f, 1.0f))); + definition.SetToken("editor.space.menu.padding_x", Style::UIStyleValue(7.0f)); + definition.SetToken("editor.space.menu.padding_y", Style::UIStyleValue(5.0f)); + definition.SetToken("editor.size.menu.item", Style::UIStyleValue(24.0f)); + definition.SetToken("editor.size.menu.separator", Style::UIStyleValue(8.0f)); + definition.SetToken("editor.size.menu.check_column", Style::UIStyleValue(16.0f)); + definition.SetToken("editor.space.menu.shortcut_gap", Style::UIStyleValue(18.0f)); + definition.SetToken("editor.size.menu.submenu_indicator", Style::UIStyleValue(12.0f)); + definition.SetToken("editor.radius.menu.row", Style::UIStyleValue(3.0f)); + definition.SetToken("editor.radius.menu.surface", Style::UIStyleValue(6.0f)); + definition.SetToken("editor.space.menu.label_inset_x", Style::UIStyleValue(11.0f)); + definition.SetToken("editor.space.menu.label_inset_y", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.font.menu.label", Style::UIStyleValue(11.0f)); + definition.SetToken("editor.space.menu.shortcut_inset_right", Style::UIStyleValue(18.0f)); + definition.SetToken("editor.size.menu.glyph_width", Style::UIStyleValue(6.0f)); + definition.SetToken("editor.font.menu.glyph", Style::UIStyleValue(10.0f)); + definition.SetToken("editor.border.menu.separator", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.border.menu.surface", Style::UIStyleValue(1.0f)); + definition.SetToken("editor.color.menu.surface", Style::UIStyleValue(Math::Color(0.16f, 0.16f, 0.16f, 1.0f))); + definition.SetToken("editor.color.menu.border", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f))); + definition.SetToken("editor.color.menu.item_hover", Style::UIStyleValue(Math::Color(0.23f, 0.23f, 0.23f, 1.0f))); + definition.SetToken("editor.color.menu.item_open", Style::UIStyleValue(Math::Color(0.28f, 0.28f, 0.28f, 1.0f))); + definition.SetToken("editor.color.menu.separator", Style::UIStyleValue(Math::Color(0.30f, 0.30f, 0.30f, 1.0f))); + definition.SetToken("editor.color.menu.text", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f))); + definition.SetToken("editor.color.menu.text_muted", Style::UIStyleValue(Math::Color(0.74f, 0.74f, 0.74f, 1.0f))); + definition.SetToken("editor.color.menu.text_disabled", Style::UIStyleValue(Math::Color(0.47f, 0.47f, 0.47f, 1.0f))); + definition.SetToken("editor.color.menu.glyph", Style::UIStyleValue(Math::Color(0.86f, 0.86f, 0.86f, 1.0f))); + return Style::BuildTheme(definition); +} + +TEST(UIEditorThemeTest, FieldResolversReadEditorThemeTokens) { + const Style::UITheme theme = BuildEditorFieldTheme(); + + const auto boolMetrics = Editor::ResolveUIEditorBoolFieldMetrics(theme); + const auto boolPalette = Editor::ResolveUIEditorBoolFieldPalette(theme); + EXPECT_FLOAT_EQ(boolMetrics.rowHeight, 26.0f); + EXPECT_FLOAT_EQ(boolMetrics.horizontalPadding, 8.0f); + EXPECT_FLOAT_EQ(boolMetrics.controlTrailingInset, 9.0f); + EXPECT_FLOAT_EQ(boolMetrics.checkboxGlyphFontSize, 10.0f); + EXPECT_FLOAT_EQ(boolPalette.rowHoverColor.r, 0.20f); + EXPECT_FLOAT_EQ(boolPalette.checkboxBorderColor.r, 0.33f); + + const auto numberMetrics = Editor::ResolveUIEditorNumberFieldMetrics(theme); + const auto numberPalette = Editor::ResolveUIEditorNumberFieldPalette(theme); + EXPECT_FLOAT_EQ(numberMetrics.controlTrailingInset, 9.0f); + EXPECT_FLOAT_EQ(numberMetrics.controlInsetY, 3.0f); + EXPECT_FLOAT_EQ(numberMetrics.valueTextInsetX, 6.0f); + EXPECT_FLOAT_EQ(numberMetrics.valueFontSize, 12.0f); + EXPECT_FLOAT_EQ(numberPalette.valueBoxEditingColor.r, 0.24f); + EXPECT_FLOAT_EQ(numberPalette.controlBorderColor.r, 0.30f); + EXPECT_FLOAT_EQ(numberPalette.controlFocusedBorderColor.r, 0.64f); + + const auto textMetrics = Editor::ResolveUIEditorTextFieldMetrics(theme); + const auto textPalette = Editor::ResolveUIEditorTextFieldPalette(theme); + EXPECT_FLOAT_EQ(textMetrics.controlTrailingInset, 9.0f); + EXPECT_FLOAT_EQ(textMetrics.valueBoxMinWidth, 88.0f); + EXPECT_FLOAT_EQ(textMetrics.valueTextInsetY, 4.0f); + EXPECT_FLOAT_EQ(textMetrics.valueFontSize, 12.0f); + EXPECT_FLOAT_EQ(textPalette.valueBoxEditingColor.r, 0.24f); + EXPECT_FLOAT_EQ(textPalette.readOnlyValueColor.r, 0.60f); + EXPECT_FLOAT_EQ(textPalette.controlFocusedBorderColor.r, 0.64f); + + const auto vector2Metrics = Editor::ResolveUIEditorVector2FieldMetrics(theme); + const auto vector2Palette = Editor::ResolveUIEditorVector2FieldPalette(theme); + EXPECT_FLOAT_EQ(vector2Metrics.componentGap, 6.0f); + EXPECT_FLOAT_EQ(vector2Metrics.componentPrefixWidth, 18.0f); + EXPECT_FLOAT_EQ(vector2Metrics.controlTrailingInset, 9.0f); + EXPECT_FLOAT_EQ(vector2Metrics.componentLabelGap, 5.0f); + EXPECT_FLOAT_EQ(vector2Metrics.prefixFontSize, 10.0f); + EXPECT_FLOAT_EQ(vector2Palette.componentFocusedBorderColor.r, 0.64f); + EXPECT_FLOAT_EQ(vector2Palette.axisXColor.r, 0.78f); + EXPECT_FLOAT_EQ(vector2Palette.axisYColor.g, 0.72f); + + const auto vector3Metrics = Editor::ResolveUIEditorVector3FieldMetrics(theme); + const auto vector3Palette = Editor::ResolveUIEditorVector3FieldPalette(theme); + EXPECT_FLOAT_EQ(vector3Metrics.componentGap, 6.0f); + EXPECT_FLOAT_EQ(vector3Metrics.componentPrefixWidth, 18.0f); + EXPECT_FLOAT_EQ(vector3Metrics.controlTrailingInset, 9.0f); + EXPECT_FLOAT_EQ(vector3Metrics.componentLabelGap, 5.0f); + EXPECT_FLOAT_EQ(vector3Metrics.prefixFontSize, 10.0f); + EXPECT_FLOAT_EQ(vector3Palette.componentFocusedBorderColor.r, 0.64f); + EXPECT_FLOAT_EQ(vector3Palette.axisZColor.b, 0.82f); + + const auto enumMetrics = Editor::ResolveUIEditorEnumFieldMetrics(theme); + const auto enumPalette = Editor::ResolveUIEditorEnumFieldPalette(theme); + EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 9.0f); + EXPECT_FLOAT_EQ(enumMetrics.valueBoxMinWidth, 88.0f); + EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowWidth, 14.0f); + EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowFontSize, 10.0f); + EXPECT_FLOAT_EQ(enumPalette.arrowColor.r, 0.88f); + + const auto popupMetrics = Editor::ResolveUIEditorMenuPopupMetrics(theme); + const auto popupPalette = Editor::ResolveUIEditorMenuPopupPalette(theme); + EXPECT_FLOAT_EQ(popupMetrics.contentPaddingX, 6.0f); + EXPECT_FLOAT_EQ(popupMetrics.itemHeight, 24.0f); + EXPECT_FLOAT_EQ(popupMetrics.glyphFontSize, 10.0f); + EXPECT_FLOAT_EQ(popupPalette.popupColor.r, 0.15f); + EXPECT_FLOAT_EQ(popupPalette.textPrimary.r, 0.91f); + EXPECT_FLOAT_EQ(popupPalette.textMuted.r, 0.76f); +} + +TEST(UIEditorThemeTest, PropertyGridResolversSupportOverridesAndFallbacks) { + const Style::UITheme theme = BuildPropertyGridTheme(); + const Style::UITheme fallbackTheme = Style::UITheme(); + + UI::Editor::Widgets::UIEditorPropertyGridMetrics fallbackMetrics = {}; + fallbackMetrics.sectionHeaderHeight = 40.0f; + fallbackMetrics.disclosureGlyphFontSize = 15.0f; + fallbackMetrics.tagFontSize = 13.0f; + const auto fallbackResolvedMetrics = + Editor::ResolveUIEditorPropertyGridMetrics(fallbackTheme, fallbackMetrics); + EXPECT_FLOAT_EQ(fallbackResolvedMetrics.sectionHeaderHeight, 40.0f); + EXPECT_FLOAT_EQ(fallbackResolvedMetrics.disclosureGlyphFontSize, 15.0f); + EXPECT_FLOAT_EQ(fallbackResolvedMetrics.tagFontSize, 13.0f); + + UI::Editor::Widgets::UIEditorPropertyGridPalette fallbackPalette = {}; + fallbackPalette.surfaceColor = UI::UIColor(0.5f, 0.4f, 0.3f, 1.0f); + const auto fallbackResolvedPalette = + Editor::ResolveUIEditorPropertyGridPalette(fallbackTheme, fallbackPalette); + EXPECT_FLOAT_EQ(fallbackResolvedPalette.surfaceColor.r, 0.5f); + + const auto themedMetrics = Editor::ResolveUIEditorPropertyGridMetrics(theme); + const auto themedPalette = Editor::ResolveUIEditorPropertyGridPalette(theme); + const auto popupMetrics = Editor::ResolveUIEditorMenuPopupMetrics(theme); + const auto popupPalette = Editor::ResolveUIEditorMenuPopupPalette(theme); + EXPECT_FLOAT_EQ(themedMetrics.contentInset, 6.0f); + EXPECT_FLOAT_EQ(themedMetrics.sectionHeaderHeight, 24.0f); + EXPECT_FLOAT_EQ(themedMetrics.disclosureGlyphFontSize, 10.0f); + EXPECT_FLOAT_EQ(themedMetrics.tagFontSize, 10.0f); + EXPECT_FLOAT_EQ(themedPalette.sectionHeaderColor.r, 0.21f); + EXPECT_FLOAT_EQ(themedPalette.valueBoxEditingBorderColor.r, 0.72f); + EXPECT_FLOAT_EQ(themedPalette.valueTextColor.r, 0.92f); + EXPECT_FLOAT_EQ(themedPalette.readOnlyValueTextColor.r, 0.60f); + EXPECT_FLOAT_EQ(popupMetrics.contentPaddingX, 7.0f); + EXPECT_FLOAT_EQ(popupMetrics.itemHeight, 24.0f); + EXPECT_FLOAT_EQ(popupMetrics.popupCornerRounding, 6.0f); + EXPECT_FLOAT_EQ(popupMetrics.glyphFontSize, 10.0f); + EXPECT_FLOAT_EQ(popupPalette.popupColor.r, 0.16f); + EXPECT_FLOAT_EQ(popupPalette.itemHoverColor.r, 0.23f); + EXPECT_FLOAT_EQ(popupPalette.glyphColor.r, 0.86f); +} + +TEST(UIEditorThemeTest, HostedFieldBuildersInheritPropertyGridMetricsAndPalette) { + UI::Editor::Widgets::UIEditorPropertyGridMetrics propertyMetrics = {}; + propertyMetrics.fieldRowHeight = 25.0f; + propertyMetrics.horizontalPadding = 7.0f; + propertyMetrics.labelControlGap = 11.0f; + propertyMetrics.controlColumnStart = 210.0f; + propertyMetrics.labelTextInsetY = 4.0f; + propertyMetrics.labelFontSize = 10.0f; + propertyMetrics.valueTextInsetY = 3.0f; + propertyMetrics.valueFontSize = 12.0f; + propertyMetrics.valueBoxInsetY = 2.0f; + propertyMetrics.valueBoxInsetX = 5.0f; + propertyMetrics.cornerRounding = 1.0f; + propertyMetrics.valueBoxRounding = 2.0f; + propertyMetrics.borderThickness = 1.0f; + propertyMetrics.focusedBorderThickness = 2.0f; + + UI::Editor::Widgets::UIEditorPropertyGridPalette propertyPalette = {}; + propertyPalette.valueBoxColor = UI::UIColor(0.2f, 0.2f, 0.2f, 1.0f); + propertyPalette.valueBoxHoverColor = UI::UIColor(0.3f, 0.3f, 0.3f, 1.0f); + propertyPalette.valueBoxEditingColor = UI::UIColor(0.4f, 0.4f, 0.4f, 1.0f); + propertyPalette.valueBoxReadOnlyColor = UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f); + propertyPalette.valueBoxBorderColor = UI::UIColor(0.5f, 0.5f, 0.5f, 1.0f); + propertyPalette.valueBoxEditingBorderColor = UI::UIColor(0.55f, 0.55f, 0.55f, 1.0f); + propertyPalette.labelTextColor = UI::UIColor(0.8f, 0.8f, 0.8f, 1.0f); + propertyPalette.valueTextColor = UI::UIColor(0.9f, 0.9f, 0.9f, 1.0f); + propertyPalette.readOnlyValueTextColor = UI::UIColor(0.6f, 0.6f, 0.6f, 1.0f); + + const auto boolMetrics = Editor::BuildUIEditorHostedBoolFieldMetrics(propertyMetrics); + const auto boolPalette = Editor::BuildUIEditorHostedBoolFieldPalette(propertyPalette); + EXPECT_FLOAT_EQ(boolMetrics.rowHeight, 25.0f); + EXPECT_FLOAT_EQ(boolMetrics.controlTrailingInset, 5.0f); + EXPECT_FLOAT_EQ(boolMetrics.labelFontSize, 10.0f); + EXPECT_FLOAT_EQ(boolMetrics.checkboxGlyphFontSize, 12.0f); + EXPECT_FLOAT_EQ(boolPalette.checkboxColor.r, 0.2f); + EXPECT_FLOAT_EQ(boolPalette.labelColor.r, 0.8f); + + const auto numberMetrics = Editor::BuildUIEditorHostedNumberFieldMetrics(propertyMetrics); + const auto numberPalette = Editor::BuildUIEditorHostedNumberFieldPalette(propertyPalette); + EXPECT_FLOAT_EQ(numberMetrics.controlTrailingInset, 5.0f); + EXPECT_FLOAT_EQ(numberMetrics.controlInsetY, 2.0f); + EXPECT_FLOAT_EQ(numberMetrics.valueTextInsetX, 5.0f); + EXPECT_FLOAT_EQ(numberMetrics.valueFontSize, 12.0f); + EXPECT_FLOAT_EQ(numberPalette.valueBoxEditingColor.r, 0.4f); + EXPECT_FLOAT_EQ(numberPalette.readOnlyValueColor.r, 0.6f); + EXPECT_FLOAT_EQ(numberPalette.controlFocusedBorderColor.r, 0.55f); + + const auto textMetrics = Editor::BuildUIEditorHostedTextFieldMetrics(propertyMetrics); + const auto textPalette = Editor::BuildUIEditorHostedTextFieldPalette(propertyPalette); + EXPECT_FLOAT_EQ(textMetrics.controlTrailingInset, 5.0f); + EXPECT_FLOAT_EQ(textMetrics.controlInsetY, 2.0f); + EXPECT_FLOAT_EQ(textMetrics.valueTextInsetX, 5.0f); + EXPECT_FLOAT_EQ(textMetrics.valueFontSize, 12.0f); + EXPECT_FLOAT_EQ(textPalette.valueBoxEditingColor.r, 0.4f); + EXPECT_FLOAT_EQ(textPalette.readOnlyValueColor.r, 0.6f); + EXPECT_FLOAT_EQ(textPalette.controlFocusedBorderColor.r, 0.55f); + + const auto vector2Metrics = Editor::BuildUIEditorHostedVector2FieldMetrics(propertyMetrics); + const auto vector2Palette = Editor::BuildUIEditorHostedVector2FieldPalette(propertyPalette); + EXPECT_FLOAT_EQ(vector2Metrics.controlInsetY, 2.0f); + EXPECT_FLOAT_EQ(vector2Metrics.controlTrailingInset, 5.0f); + EXPECT_FLOAT_EQ(vector2Metrics.valueTextInsetX, 5.0f); + EXPECT_FLOAT_EQ(vector2Metrics.componentRounding, 2.0f); + EXPECT_FLOAT_EQ(vector2Palette.componentEditingColor.r, 0.4f); + EXPECT_FLOAT_EQ(vector2Palette.componentFocusedBorderColor.r, 0.55f); + + const auto vector3Metrics = Editor::BuildUIEditorHostedVector3FieldMetrics(propertyMetrics); + const auto vector3Palette = Editor::BuildUIEditorHostedVector3FieldPalette(propertyPalette); + EXPECT_FLOAT_EQ(vector3Metrics.controlInsetY, 2.0f); + EXPECT_FLOAT_EQ(vector3Metrics.controlTrailingInset, 5.0f); + EXPECT_FLOAT_EQ(vector3Metrics.valueTextInsetX, 5.0f); + EXPECT_FLOAT_EQ(vector3Metrics.componentRounding, 2.0f); + EXPECT_FLOAT_EQ(vector3Palette.componentEditingColor.r, 0.4f); + EXPECT_FLOAT_EQ(vector3Palette.componentFocusedBorderColor.r, 0.55f); + + const auto enumMetrics = Editor::BuildUIEditorHostedEnumFieldMetrics(propertyMetrics); + const auto enumPalette = Editor::BuildUIEditorHostedEnumFieldPalette(propertyPalette); + EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 5.0f); + EXPECT_FLOAT_EQ(enumMetrics.controlInsetY, 2.0f); + EXPECT_FLOAT_EQ(enumMetrics.valueTextInsetX, 5.0f); + EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowFontSize, 12.0f); + EXPECT_FLOAT_EQ(enumPalette.arrowColor.r, 0.9f); +} + +} // namespace diff --git a/tests/UI/Editor/unit/test_ui_editor_vector2_field.cpp b/tests/UI/Editor/unit/test_ui_editor_vector2_field.cpp new file mode 100644 index 00000000..e13314fb --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_vector2_field.cpp @@ -0,0 +1,89 @@ +#include + +#include + +namespace { + +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Widgets::BuildUIEditorVector2FieldLayout; +using XCEngine::UI::Editor::Widgets::FormatUIEditorVector2FieldComponentValue; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector2Field; +using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec; + +UIRect MakeInspectorBounds() { + return UIRect(0.0f, 0.0f, 392.0f, 22.0f); +} + +TEST(UIEditorVector2FieldTest, FormatSupportsPerComponentDisplay) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.25, -3.5 }; + + EXPECT_EQ(FormatUIEditorVector2FieldComponentValue(spec, 0u), "1.25"); + EXPECT_EQ(FormatUIEditorVector2FieldComponentValue(spec, 1u), "-3.5"); +} + +TEST(UIEditorVector2FieldTest, LayoutBuildsTwoComponentRects) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec); + + EXPECT_GT(layout.labelRect.width, 0.0f); + EXPECT_GT(layout.controlRect.width, 0.0f); + EXPECT_GT(layout.componentRects[0].width, 0.0f); + EXPECT_GT(layout.componentRects[1].width, 0.0f); + EXPECT_LT(layout.componentRects[0].x + layout.componentRects[0].width, layout.componentRects[1].x); + EXPECT_GT(layout.componentPrefixRects[0].width, 0.0f); + EXPECT_GT(layout.componentValueRects[0].width, 0.0f); + EXPECT_GT(layout.componentValueRects[0].x, layout.componentPrefixRects[0].x + layout.componentPrefixRects[0].width); + EXPECT_GT(layout.componentValueRects[1].x, layout.componentPrefixRects[1].x + layout.componentPrefixRects[1].width); + EXPECT_EQ(layout.componentValueRects[0].height, layout.componentRects[0].height); + EXPECT_EQ(layout.componentValueRects[1].height, layout.componentRects[1].height); +} + +TEST(UIEditorVector2FieldTest, HitTestTreatsAxisLabelAreaAsComponentHost) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec); + + const auto prefixHit = HitTestUIEditorVector2Field( + layout, + UIPoint( + layout.componentPrefixRects[0].x + layout.componentPrefixRects[0].width * 0.5f, + layout.componentPrefixRects[0].y + layout.componentPrefixRects[0].height * 0.5f)); + EXPECT_EQ(prefixHit.kind, UIEditorVector2FieldHitTargetKind::Component); + EXPECT_EQ(prefixHit.componentIndex, 0u); + + const auto valueHit = HitTestUIEditorVector2Field( + layout, + UIPoint( + layout.componentValueRects[1].x + 4.0f, + layout.componentValueRects[1].y + 4.0f)); + EXPECT_EQ(valueHit.kind, UIEditorVector2FieldHitTargetKind::Component); + EXPECT_EQ(valueHit.componentIndex, 1u); +} + +TEST(UIEditorVector2FieldTest, HitTestResolvesComponentAndRow) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec); + + const auto firstHit = HitTestUIEditorVector2Field( + layout, + UIPoint(layout.componentRects[0].x + 4.0f, layout.componentRects[0].y + 4.0f)); + EXPECT_EQ(firstHit.kind, UIEditorVector2FieldHitTargetKind::Component); + EXPECT_EQ(firstHit.componentIndex, 0u); + + const auto rowHit = HitTestUIEditorVector2Field( + layout, + UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f)); + EXPECT_EQ(rowHit.kind, UIEditorVector2FieldHitTargetKind::Row); +} + +} // namespace diff --git a/tests/UI/Editor/unit/test_ui_editor_vector2_field_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_vector2_field_interaction.cpp new file mode 100644 index 00000000..afca66d9 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_vector2_field_interaction.cpp @@ -0,0 +1,199 @@ +#include + +#include + +#include + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::UIEditorVector2FieldInteractionState; +using XCEngine::UI::Editor::UpdateUIEditorVector2FieldInteraction; +using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec; + +UIRect MakeInspectorBounds() { + return UIRect(0.0f, 0.0f, 392.0f, 22.0f); +} + +UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = UIPoint(x, y); + event.pointerButton = button; + return event; +} + +UIInputEvent MakeKey(KeyCode keyCode) { + UIInputEvent event = {}; + event.type = UIInputEventType::KeyDown; + event.keyCode = static_cast(keyCode); + return event; +} + +UIInputEvent MakeCharacter(char character) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(character); + return event; +} + +} // namespace + +TEST(UIEditorVector2FieldInteractionTest, ClickSecondComponentStartsEditing) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.0, 2.0 }; + UIEditorVector2FieldInteractionState state = {}; + + auto frame = UpdateUIEditorVector2FieldInteraction( + state, + spec, + MakeInspectorBounds(), + {}); + + frame = UpdateUIEditorVector2FieldInteraction( + state, + spec, + MakeInspectorBounds(), + { + MakePointer( + UIInputEventType::PointerButtonDown, + frame.layout.componentRects[1].x + 4.0f, + frame.layout.componentRects[1].y + 4.0f, + UIPointerButton::Left), + MakePointer( + UIInputEventType::PointerButtonUp, + frame.layout.componentRects[1].x + 4.0f, + frame.layout.componentRects[1].y + 4.0f, + UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.vector2FieldState.editing); + EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 1u); +} + +TEST(UIEditorVector2FieldInteractionTest, ClickAxisLabelAreaAlsoStartsEditing) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.0, 2.0 }; + UIEditorVector2FieldInteractionState state = {}; + + auto frame = UpdateUIEditorVector2FieldInteraction( + state, + spec, + MakeInspectorBounds(), + {}); + + const float clickX = + frame.layout.componentPrefixRects[0].x + frame.layout.componentPrefixRects[0].width * 0.5f; + const float clickY = + frame.layout.componentPrefixRects[0].y + frame.layout.componentPrefixRects[0].height * 0.5f; + + frame = UpdateUIEditorVector2FieldInteraction( + state, + spec, + MakeInspectorBounds(), + { + MakePointer(UIInputEventType::PointerButtonDown, clickX, clickY, UIPointerButton::Left), + MakePointer(UIInputEventType::PointerButtonUp, clickX, clickY, UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.vector2FieldState.editing); + EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 0u); +} + +TEST(UIEditorVector2FieldInteractionTest, TabSelectsNextComponentAndArrowAppliesStep) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.0, 2.0 }; + spec.step = 0.5; + UIEditorVector2FieldInteractionState state = {}; + state.vector2FieldState.focused = true; + state.vector2FieldState.selectedComponentIndex = 0u; + + auto frame = UpdateUIEditorVector2FieldInteraction( + state, + spec, + MakeInspectorBounds(), + { MakeKey(KeyCode::Tab) }); + EXPECT_TRUE(frame.result.selectionChanged); + EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 1u); + + frame = UpdateUIEditorVector2FieldInteraction( + state, + spec, + MakeInspectorBounds(), + { MakeKey(KeyCode::Up) }); + EXPECT_TRUE(frame.result.stepApplied); + EXPECT_EQ(frame.result.changedComponentIndex, 1u); + EXPECT_DOUBLE_EQ(spec.values[1], 2.5); +} + +TEST(UIEditorVector2FieldInteractionTest, EnterStartsEditingAndCommitUpdatesSelectedComponent) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.0, 2.0 }; + spec.integerMode = true; + UIEditorVector2FieldInteractionState state = {}; + state.vector2FieldState.focused = true; + state.vector2FieldState.selectedComponentIndex = 0u; + + auto frame = UpdateUIEditorVector2FieldInteraction( + state, + spec, + MakeInspectorBounds(), + { MakeKey(KeyCode::Enter) }); + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.vector2FieldState.editing); + + frame = UpdateUIEditorVector2FieldInteraction( + state, + spec, + MakeInspectorBounds(), + { MakeCharacter('4'), MakeKey(KeyCode::Enter) }); + + EXPECT_TRUE(frame.result.editCommitted); + EXPECT_TRUE(frame.result.valueChanged); + EXPECT_EQ(frame.result.changedComponentIndex, 0u); + EXPECT_DOUBLE_EQ(spec.values[0], 14.0); + EXPECT_DOUBLE_EQ(spec.values[1], 2.0); +} + +TEST(UIEditorVector2FieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) { + UIEditorVector2FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.0, 2.0 }; + UIEditorVector2FieldInteractionState state = {}; + state.vector2FieldState.focused = true; + state.vector2FieldState.selectedComponentIndex = 1u; + + auto frame = UpdateUIEditorVector2FieldInteraction( + state, + spec, + MakeInspectorBounds(), + { MakeCharacter('9') }); + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.vector2FieldState.editing); + EXPECT_EQ(state.vector2FieldState.displayTexts[1], "9"); + + frame = UpdateUIEditorVector2FieldInteraction( + state, + spec, + MakeInspectorBounds(), + { MakeKey(KeyCode::Escape) }); + EXPECT_TRUE(frame.result.editCanceled); + EXPECT_FALSE(state.vector2FieldState.editing); + EXPECT_DOUBLE_EQ(spec.values[1], 2.0); +} diff --git a/tests/UI/Editor/unit/test_ui_editor_vector3_field.cpp b/tests/UI/Editor/unit/test_ui_editor_vector3_field.cpp new file mode 100644 index 00000000..49444f2c --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_vector3_field.cpp @@ -0,0 +1,58 @@ +#include + +#include + +namespace { + +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::Widgets::BuildUIEditorVector3FieldLayout; +using XCEngine::UI::Editor::Widgets::FormatUIEditorVector3FieldComponentValue; +using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector3Field; +using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTargetKind; +using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec; + +TEST(UIEditorVector3FieldTest, FormatSupportsPerComponentDisplay) { + UIEditorVector3FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.25, -3.5, 8.0 }; + + EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 0u), "1.25"); + EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 1u), "-3.5"); + EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 2u), "8"); +} + +TEST(UIEditorVector3FieldTest, LayoutBuildsThreeComponentRects) { + UIEditorVector3FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + const auto layout = BuildUIEditorVector3FieldLayout(UIRect(0.0f, 0.0f, 520.0f, 32.0f), spec); + + EXPECT_GT(layout.labelRect.width, 0.0f); + EXPECT_GT(layout.componentRects[0].width, 0.0f); + EXPECT_GT(layout.componentRects[1].width, 0.0f); + EXPECT_GT(layout.componentRects[2].width, 0.0f); + EXPECT_LT(layout.componentRects[0].x + layout.componentRects[0].width, layout.componentRects[1].x); + EXPECT_LT(layout.componentRects[1].x + layout.componentRects[1].width, layout.componentRects[2].x); +} + +TEST(UIEditorVector3FieldTest, HitTestResolvesComponentAndRow) { + UIEditorVector3FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + const auto layout = BuildUIEditorVector3FieldLayout(UIRect(0.0f, 0.0f, 520.0f, 32.0f), spec); + + const auto thirdHit = HitTestUIEditorVector3Field( + layout, + UIPoint(layout.componentRects[2].x + 4.0f, layout.componentRects[2].y + 4.0f)); + EXPECT_EQ(thirdHit.kind, UIEditorVector3FieldHitTargetKind::Component); + EXPECT_EQ(thirdHit.componentIndex, 2u); + + const auto rowHit = HitTestUIEditorVector3Field( + layout, + UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f)); + EXPECT_EQ(rowHit.kind, UIEditorVector3FieldHitTargetKind::Row); +} + +} // namespace diff --git a/tests/UI/Editor/unit/test_ui_editor_vector3_field_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_vector3_field_interaction.cpp new file mode 100644 index 00000000..0bb2818b --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_vector3_field_interaction.cpp @@ -0,0 +1,164 @@ +#include + +#include + +#include + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::UIEditorVector3FieldInteractionState; +using XCEngine::UI::Editor::UpdateUIEditorVector3FieldInteraction; +using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec; + +UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) { + UIInputEvent event = {}; + event.type = type; + event.position = UIPoint(x, y); + event.pointerButton = button; + return event; +} + +UIInputEvent MakeKey(KeyCode keyCode) { + UIInputEvent event = {}; + event.type = UIInputEventType::KeyDown; + event.keyCode = static_cast(keyCode); + return event; +} + +UIInputEvent MakeCharacter(char character) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(character); + return event; +} + +} // namespace + +TEST(UIEditorVector3FieldInteractionTest, ClickThirdComponentStartsEditing) { + UIEditorVector3FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.0, 2.0, 3.0 }; + UIEditorVector3FieldInteractionState state = {}; + + auto frame = UpdateUIEditorVector3FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 520.0f, 32.0f), + {}); + + frame = UpdateUIEditorVector3FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 520.0f, 32.0f), + { + MakePointer( + UIInputEventType::PointerButtonDown, + frame.layout.componentRects[2].x + 4.0f, + frame.layout.componentRects[2].y + 4.0f, + UIPointerButton::Left), + MakePointer( + UIInputEventType::PointerButtonUp, + frame.layout.componentRects[2].x + 4.0f, + frame.layout.componentRects[2].y + 4.0f, + UIPointerButton::Left) + }); + + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.vector3FieldState.editing); + EXPECT_EQ(state.vector3FieldState.selectedComponentIndex, 2u); +} + +TEST(UIEditorVector3FieldInteractionTest, TabSelectsNextComponentAndArrowAppliesStep) { + UIEditorVector3FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.0, 2.0, 3.0 }; + spec.step = 0.5; + UIEditorVector3FieldInteractionState state = {}; + state.vector3FieldState.focused = true; + state.vector3FieldState.selectedComponentIndex = 1u; + + auto frame = UpdateUIEditorVector3FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 520.0f, 32.0f), + { MakeKey(KeyCode::Tab) }); + EXPECT_TRUE(frame.result.selectionChanged); + EXPECT_EQ(state.vector3FieldState.selectedComponentIndex, 2u); + + frame = UpdateUIEditorVector3FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 520.0f, 32.0f), + { MakeKey(KeyCode::Up) }); + EXPECT_TRUE(frame.result.stepApplied); + EXPECT_EQ(frame.result.changedComponentIndex, 2u); + EXPECT_DOUBLE_EQ(spec.values[2], 3.5); +} + +TEST(UIEditorVector3FieldInteractionTest, EnterStartsEditingAndCommitUpdatesSelectedComponent) { + UIEditorVector3FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.0, 2.0, 3.0 }; + spec.integerMode = true; + UIEditorVector3FieldInteractionState state = {}; + state.vector3FieldState.focused = true; + state.vector3FieldState.selectedComponentIndex = 1u; + + auto frame = UpdateUIEditorVector3FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 520.0f, 32.0f), + { MakeKey(KeyCode::Enter) }); + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.vector3FieldState.editing); + + frame = UpdateUIEditorVector3FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 520.0f, 32.0f), + { MakeCharacter('4'), MakeKey(KeyCode::Enter) }); + + EXPECT_TRUE(frame.result.editCommitted); + EXPECT_TRUE(frame.result.valueChanged); + EXPECT_EQ(frame.result.changedComponentIndex, 1u); + EXPECT_DOUBLE_EQ(spec.values[0], 1.0); + EXPECT_DOUBLE_EQ(spec.values[1], 24.0); + EXPECT_DOUBLE_EQ(spec.values[2], 3.0); +} + +TEST(UIEditorVector3FieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) { + UIEditorVector3FieldSpec spec = {}; + spec.fieldId = "position"; + spec.label = "Position"; + spec.values = { 1.0, 2.0, 3.0 }; + UIEditorVector3FieldInteractionState state = {}; + state.vector3FieldState.focused = true; + state.vector3FieldState.selectedComponentIndex = 0u; + + auto frame = UpdateUIEditorVector3FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 520.0f, 32.0f), + { MakeCharacter('9') }); + EXPECT_TRUE(frame.result.editStarted); + EXPECT_TRUE(state.vector3FieldState.editing); + EXPECT_EQ(state.vector3FieldState.displayTexts[0], "9"); + + frame = UpdateUIEditorVector3FieldInteraction( + state, + spec, + UIRect(0.0f, 0.0f, 520.0f, 32.0f), + { MakeKey(KeyCode::Escape) }); + EXPECT_TRUE(frame.result.editCanceled); + EXPECT_FALSE(state.vector3FieldState.editing); + EXPECT_DOUBLE_EQ(spec.values[0], 1.0); +}