ui: add typed editor field foundations
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
|
||||
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
||||
|
||||
#include <XCEngine/UI/Types.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<Widgets::UIEditorMenuPopupItem> 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
|
||||
|
||||
@@ -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<Widgets::UIEditorPropertyGridSection>& sections,
|
||||
std::vector<Widgets::UIEditorPropertyGridSection>& sections,
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorPropertyGridMetrics& metrics = {});
|
||||
const Widgets::UIEditorPropertyGridMetrics& metrics = {},
|
||||
const Widgets::UIEditorMenuPopupMetrics& popupMetrics = {});
|
||||
|
||||
} // namespace XCEngine::UI::Editor
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorTextField.h>
|
||||
|
||||
#include <XCEngine/UI/Text/UITextInputController.h>
|
||||
#include <XCEngine/UI/Types.h>
|
||||
#include <XCEngine/UI/Widgets/UIPropertyEditModel.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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
|
||||
140
new_editor/include/XCEditor/Core/UIEditorTheme.h
Normal file
140
new_editor/include/XCEditor/Core/UIEditorTheme.h
Normal file
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorBoolField.h>
|
||||
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
||||
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
|
||||
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||
#include <XCEditor/Widgets/UIEditorPropertyGrid.h>
|
||||
#include <XCEditor/Widgets/UIEditorTextField.h>
|
||||
#include <XCEditor/Widgets/UIEditorVector2Field.h>
|
||||
#include <XCEditor/Widgets/UIEditorVector3Field.h>
|
||||
|
||||
#include <XCEngine/UI/Style/Theme.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorVector2Field.h>
|
||||
|
||||
#include <XCEngine/UI/Text/UITextInputController.h>
|
||||
#include <XCEngine/UI/Types.h>
|
||||
#include <XCEngine/UI/Widgets/UIPropertyEditModel.h>
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct 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<double, 2u> valuesBefore = { 0.0, 0.0 };
|
||||
std::array<double, 2u> 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
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorVector3Field.h>
|
||||
|
||||
#include <XCEngine/UI/Text/UITextInputController.h>
|
||||
#include <XCEngine/UI/Types.h>
|
||||
#include <XCEngine/UI/Widgets/UIPropertyEditModel.h>
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
struct 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<double, 3u> valuesBefore = { 0.0, 0.0, 0.0 };
|
||||
std::array<double, 3u> 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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
27
new_editor/include/XCEditor/Widgets/UIEditorFieldRowLayout.h
Normal file
27
new_editor/include/XCEditor/Widgets/UIEditorFieldRowLayout.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
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 = {});
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <XCEngine/UI/Widgets/UIPropertyEditModel.h>
|
||||
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
@@ -15,6 +17,13 @@ namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
inline constexpr std::size_t UIEditorPropertyGridInvalidIndex = static_cast<std::size_t>(-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<std::string> 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<UIEditorPropertyGridSection>& 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<UIEditorPropertyGridSection>& 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
|
||||
|
||||
130
new_editor/include/XCEditor/Widgets/UIEditorTextField.h
Normal file
130
new_editor/include/XCEditor/Widgets/UIEditorTextField.h
Normal file
@@ -0,0 +1,130 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
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
|
||||
35
new_editor/include/XCEditor/Widgets/UIEditorTextLayout.h
Normal file
35
new_editor/include/XCEditor/Widgets/UIEditorTextLayout.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
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
|
||||
171
new_editor/include/XCEditor/Widgets/UIEditorVector2Field.h
Normal file
171
new_editor/include/XCEditor/Widgets/UIEditorVector2Field.h
Normal file
@@ -0,0 +1,171 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
inline constexpr std::size_t UIEditorVector2FieldInvalidComponentIndex = static_cast<std::size_t>(-1);
|
||||
|
||||
enum class UIEditorVector2FieldHitTargetKind : std::uint8_t {
|
||||
None = 0,
|
||||
Row,
|
||||
Component
|
||||
};
|
||||
|
||||
struct UIEditorVector2FieldSpec {
|
||||
std::string fieldId = {};
|
||||
std::string label = {};
|
||||
std::array<double, 2u> values = { 0.0, 0.0 };
|
||||
std::array<std::string, 2u> componentLabels = { std::string("X"), std::string("Y") };
|
||||
double step = 0.1;
|
||||
double minValue = -1000000.0;
|
||||
double maxValue = 1000000.0;
|
||||
bool integerMode = false;
|
||||
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<std::string, 2u> 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
|
||||
173
new_editor/include/XCEditor/Widgets/UIEditorVector3Field.h
Normal file
173
new_editor/include/XCEditor/Widgets/UIEditorVector3Field.h
Normal file
@@ -0,0 +1,173 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
inline constexpr std::size_t UIEditorVector3FieldInvalidComponentIndex = static_cast<std::size_t>(-1);
|
||||
|
||||
enum class UIEditorVector3FieldHitTargetKind : std::uint8_t {
|
||||
None = 0,
|
||||
Row,
|
||||
Component
|
||||
};
|
||||
|
||||
struct UIEditorVector3FieldSpec {
|
||||
std::string fieldId = {};
|
||||
std::string label = {};
|
||||
std::array<double, 3u> values = { 0.0, 0.0, 0.0 };
|
||||
std::array<std::string, 3u> componentLabels = { std::string("X"), std::string("Y"), std::string("Z") };
|
||||
double step = 0.1;
|
||||
double minValue = -1000000.0;
|
||||
double maxValue = 1000000.0;
|
||||
bool integerMode = false;
|
||||
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<std::string, 3u> 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
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#include <XCEditor/Core/UIEditorEnumFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
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<UIEditorMenuPopupItem> BuildPopupItems(
|
||||
const UIEditorEnumFieldSpec& spec,
|
||||
std::size_t selectedIndex) {
|
||||
std::vector<UIEditorMenuPopupItem> 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<UIEditorMenuPopupItem>& 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<UIEditorMenuPopupItem>& popupItems) {
|
||||
if (!state.popupOpen || !state.hasPointerPosition) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return HitTestUIEditorMenuPopup(popupLayout, popupItems, state.pointerPosition);
|
||||
}
|
||||
|
||||
void SyncPopupHover(
|
||||
UIEditorEnumFieldInteractionState& state,
|
||||
const UIEditorMenuPopupLayout& popupLayout,
|
||||
const std::vector<UIEditorMenuPopupItem>& 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<UIInputEvent>& 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<UIEditorMenuPopupItem> popupItems =
|
||||
state.popupOpen ? BuildPopupItems(resolvedSpec, selectedIndex) : std::vector<UIEditorMenuPopupItem> {};
|
||||
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<KeyCode>(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<KeyCode>(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<UIEditorMenuPopupItem> {};
|
||||
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<UIEditorMenuPopupItem> {};
|
||||
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
|
||||
|
||||
@@ -53,38 +53,6 @@ bool IsPermittedCharacter(const UIEditorNumberFieldSpec& spec, std::uint32_t cha
|
||||
return !spec.integerMode && character == static_cast<std::uint32_t>('.');
|
||||
}
|
||||
|
||||
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<double>(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);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
#include <XCEditor/Core/UIEditorPropertyGridInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
|
||||
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
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<std::uint32_t>('0') &&
|
||||
character <= static_cast<std::uint32_t>('9')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (character == static_cast<std::uint32_t>('-') ||
|
||||
character == static_cast<std::uint32_t>('+')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !field.numberValue.integerMode && character == static_cast<std::uint32_t>('.');
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField* FindMutableField(
|
||||
std::vector<UIEditorPropertyGridSection>& 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<UIEditorPropertyGridSection>& 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<UIEditorMenuPopupItem> BuildPopupItems(
|
||||
const UIEditorPropertyGridField& field) {
|
||||
std::vector<UIEditorMenuPopupItem> 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<UIEditorPropertyGridSection>& sections,
|
||||
const Widgets::UIEditorMenuPopupMetrics& popupMetrics,
|
||||
UIEditorMenuPopupLayout& popupLayout,
|
||||
std::vector<UIEditorMenuPopupItem>& 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<UIEditorPropertyGridSection>& sections,
|
||||
const Widgets::UIEditorMenuPopupMetrics& popupMetrics) {
|
||||
if (!state.hasPointerPosition) {
|
||||
return {};
|
||||
}
|
||||
|
||||
UIEditorMenuPopupLayout popupLayout = {};
|
||||
std::vector<UIEditorMenuPopupItem> 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<Widgets::UIEditorPropertyGridSection>& sections) {
|
||||
const UIEditorPropertyGridLayout& layout,
|
||||
const std::vector<UIEditorPropertyGridSection>& 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<Widgets::UIEditorPropertyGridSection>& sections) {
|
||||
const UIEditorPropertyGridLayout& layout,
|
||||
const std::vector<UIEditorPropertyGridSection>& 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<UIEditorPropertyGridSection>& 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<UIEditorPropertyGridSection>& 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<Widgets::UIEditorPropertyGridSection>& 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<UIEditorPropertyGridSection>& 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<Widgets::UIEditorPropertyGridSection>& sections,
|
||||
std::vector<UIEditorPropertyGridSection>& sections,
|
||||
const std::vector<UIInputEvent>& 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<KeyCode>(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<KeyCode>(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<KeyCode>(event.keyCode) == KeyCode::Enter &&
|
||||
if ((static_cast<KeyCode>(event.keyCode) == KeyCode::Enter ||
|
||||
static_cast<KeyCode>(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<KeyCode>(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);
|
||||
|
||||
316
new_editor/src/Core/UIEditorTextFieldInteraction.cpp
Normal file
316
new_editor/src/Core/UIEditorTextFieldInteraction.cpp
Normal file
@@ -0,0 +1,316 @@
|
||||
#include <XCEditor/Core/UIEditorTextFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::Input::KeyCode;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::Text::HandleKeyDown;
|
||||
using ::XCEngine::UI::Text::InsertCharacter;
|
||||
using ::XCEngine::UI::Editor::Widgets::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<UIInputEvent>& 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<std::int32_t>(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<std::int32_t>(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
|
||||
1431
new_editor/src/Core/UIEditorTheme.cpp
Normal file
1431
new_editor/src/Core/UIEditorTheme.cpp
Normal file
File diff suppressed because it is too large
Load Diff
563
new_editor/src/Core/UIEditorVector2FieldInteraction.cpp
Normal file
563
new_editor/src/Core/UIEditorVector2FieldInteraction.cpp
Normal file
@@ -0,0 +1,563 @@
|
||||
#include <XCEditor/Core/UIEditorVector2FieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::Input::KeyCode;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::Text::HandleKeyDown;
|
||||
using ::XCEngine::UI::Text::InsertCharacter;
|
||||
using ::XCEngine::UI::Editor::Widgets::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<std::uint32_t>('0') &&
|
||||
character <= static_cast<std::uint32_t>('9')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (character == static_cast<std::uint32_t>('-') ||
|
||||
character == static_cast<std::uint32_t>('+')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !spec.integerMode && character == static_cast<std::uint32_t>('.');
|
||||
}
|
||||
|
||||
void SyncDisplayTexts(
|
||||
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<UIInputEvent>& 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<std::int32_t>(KeyCode::Escape)) {
|
||||
CancelEdit(state, spec, eventResult);
|
||||
break;
|
||||
}
|
||||
|
||||
if (event.keyCode == static_cast<std::int32_t>(KeyCode::Tab)) {
|
||||
if (CommitEdit(state, spec, eventResult)) {
|
||||
MoveSelection(
|
||||
state,
|
||||
event.modifiers.shift ? -1 : 1,
|
||||
eventResult);
|
||||
}
|
||||
eventResult.consumed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const auto textResult =
|
||||
HandleKeyDown(state.textInputState, event.keyCode, event.modifiers);
|
||||
if (textResult.handled) {
|
||||
state.editModel.UpdateStagedValue(state.textInputState.value);
|
||||
state.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<KeyCode>(event.keyCode)) {
|
||||
case KeyCode::Left:
|
||||
MoveSelection(state, -1, eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::Right:
|
||||
case KeyCode::Tab:
|
||||
MoveSelection(
|
||||
state,
|
||||
static_cast<KeyCode>(event.keyCode) == KeyCode::Tab && event.modifiers.shift ? -1 : 1,
|
||||
eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::Up:
|
||||
ApplyStep(state, spec, 1.0, false, eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::Down:
|
||||
ApplyStep(state, spec, -1.0, false, eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::Home:
|
||||
ApplyStep(state, spec, -1.0, true, eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::End:
|
||||
ApplyStep(state, spec, 1.0, true, eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::Enter:
|
||||
eventResult.selectedComponentIndex = ResolveFallbackSelectedComponentIndex(state);
|
||||
eventResult.editStarted = BeginEdit(
|
||||
state,
|
||||
spec,
|
||||
eventResult.selectedComponentIndex,
|
||||
false);
|
||||
eventResult.consumed = eventResult.editStarted;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::Character:
|
||||
if (!state.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
|
||||
563
new_editor/src/Core/UIEditorVector3FieldInteraction.cpp
Normal file
563
new_editor/src/Core/UIEditorVector3FieldInteraction.cpp
Normal file
@@ -0,0 +1,563 @@
|
||||
#include <XCEditor/Core/UIEditorVector3FieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::Input::KeyCode;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::Text::HandleKeyDown;
|
||||
using ::XCEngine::UI::Text::InsertCharacter;
|
||||
using ::XCEngine::UI::Editor::Widgets::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<std::uint32_t>('0') &&
|
||||
character <= static_cast<std::uint32_t>('9')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (character == static_cast<std::uint32_t>('-') ||
|
||||
character == static_cast<std::uint32_t>('+')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !spec.integerMode && character == static_cast<std::uint32_t>('.');
|
||||
}
|
||||
|
||||
void SyncDisplayTexts(
|
||||
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<UIInputEvent>& 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<std::int32_t>(KeyCode::Escape)) {
|
||||
CancelEdit(state, spec, eventResult);
|
||||
break;
|
||||
}
|
||||
|
||||
if (event.keyCode == static_cast<std::int32_t>(KeyCode::Tab)) {
|
||||
if (CommitEdit(state, spec, eventResult)) {
|
||||
MoveSelection(
|
||||
state,
|
||||
event.modifiers.shift ? -1 : 1,
|
||||
eventResult);
|
||||
}
|
||||
eventResult.consumed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const auto textResult =
|
||||
HandleKeyDown(state.textInputState, event.keyCode, event.modifiers);
|
||||
if (textResult.handled) {
|
||||
state.editModel.UpdateStagedValue(state.textInputState.value);
|
||||
state.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<KeyCode>(event.keyCode)) {
|
||||
case KeyCode::Left:
|
||||
MoveSelection(state, -1, eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::Right:
|
||||
case KeyCode::Tab:
|
||||
MoveSelection(
|
||||
state,
|
||||
static_cast<KeyCode>(event.keyCode) == KeyCode::Tab && event.modifiers.shift ? -1 : 1,
|
||||
eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::Up:
|
||||
ApplyStep(state, spec, 1.0, false, eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::Down:
|
||||
ApplyStep(state, spec, -1.0, false, eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::Home:
|
||||
ApplyStep(state, spec, -1.0, true, eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::End:
|
||||
ApplyStep(state, spec, 1.0, true, eventResult);
|
||||
break;
|
||||
|
||||
case KeyCode::Enter:
|
||||
eventResult.selectedComponentIndex = ResolveFallbackSelectedComponentIndex(state);
|
||||
eventResult.editStarted = BeginEdit(
|
||||
state,
|
||||
spec,
|
||||
eventResult.selectedComponentIndex,
|
||||
false);
|
||||
eventResult.consumed = eventResult.editStarted;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UIInputEventType::Character:
|
||||
if (!state.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
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <XCEditor/Widgets/UIEditorBoolField.h>
|
||||
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||||
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
||||
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||||
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -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(
|
||||
|
||||
51
new_editor/src/Widgets/UIEditorFieldRowLayout.cpp
Normal file
51
new_editor/src/Widgets/UIEditorFieldRowLayout.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||||
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
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<double>(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<double>(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
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
#include <XCEditor/Widgets/UIEditorPropertyGrid.h>
|
||||
|
||||
#include <XCEditor/Core/UIEditorTheme.h>
|
||||
#include <XCEditor/Widgets/UIEditorBoolField.h>
|
||||
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
||||
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
|
||||
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||
#include <XCEditor/Widgets/UIEditorTextField.h>
|
||||
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UIPopupOverlayModel.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
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<UIEditorMenuPopupItem> BuildEnumPopupItems(
|
||||
const UIEditorPropertyGridField& field) {
|
||||
std::vector<UIEditorMenuPopupItem> 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<UIEditorPropertyGridSection>& sections,
|
||||
const UIEditorPropertyGridState& state,
|
||||
const UIEditorMenuPopupMetrics& popupMetrics,
|
||||
UIEditorMenuPopupLayout& popupLayout,
|
||||
UIEditorMenuPopupState& popupState,
|
||||
std::vector<UIEditorMenuPopupItem>& 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<UIEditorPropertyGridSection>& 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<UIEditorPropertyGridSection>& 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<UIEditorPropertyGridSection>& 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<UIEditorMenuPopupItem> 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<UIEditorPropertyGridSection>& 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
|
||||
|
||||
165
new_editor/src/Widgets/UIEditorTextField.cpp
Normal file
165
new_editor/src/Widgets/UIEditorTextField.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
#include <XCEditor/Widgets/UIEditorTextField.h>
|
||||
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||||
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(0.0f, value);
|
||||
}
|
||||
|
||||
::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
|
||||
275
new_editor/src/Widgets/UIEditorVector2Field.cpp
Normal file
275
new_editor/src/Widgets/UIEditorVector2Field.cpp
Normal file
@@ -0,0 +1,275 @@
|
||||
#include <XCEditor/Widgets/UIEditorVector2Field.h>
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||||
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(0.0f, value);
|
||||
}
|
||||
|
||||
float ApproximateTextWidth(float fontSize, std::size_t characterCount) {
|
||||
return fontSize * 0.55f * static_cast<float>(characterCount);
|
||||
}
|
||||
|
||||
UIEditorNumberFieldSpec BuildComponentNumberSpec(
|
||||
const 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<float>(componentIndex);
|
||||
const UIRect componentRect(componentX, layout.controlRect.y, componentWidth, layout.controlRect.height);
|
||||
const float prefixWidth = (std::min)(metrics.componentPrefixWidth, componentRect.width);
|
||||
const float labelGap = ClampNonNegative(metrics.componentLabelGap);
|
||||
const float valueX = componentRect.x + prefixWidth + labelGap;
|
||||
layout.componentRects[componentIndex] = componentRect;
|
||||
layout.componentPrefixRects[componentIndex] =
|
||||
UIRect(componentRect.x, componentRect.y, prefixWidth, componentRect.height);
|
||||
layout.componentValueRects[componentIndex] =
|
||||
UIRect(
|
||||
valueX,
|
||||
componentRect.y,
|
||||
ClampNonNegative(componentRect.width - prefixWidth - labelGap),
|
||||
componentRect.height);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
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
|
||||
283
new_editor/src/Widgets/UIEditorVector3Field.cpp
Normal file
283
new_editor/src/Widgets/UIEditorVector3Field.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
#include <XCEditor/Widgets/UIEditorVector3Field.h>
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||||
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||
#include <XCEditor/Widgets/UIEditorTextLayout.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine::UI::Editor::Widgets {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
|
||||
float ClampNonNegative(float value) {
|
||||
return (std::max)(0.0f, value);
|
||||
}
|
||||
|
||||
float ApproximateTextWidth(float fontSize, std::size_t characterCount) {
|
||||
return fontSize * 0.55f * static_cast<float>(characterCount);
|
||||
}
|
||||
|
||||
UIEditorNumberFieldSpec BuildComponentNumberSpec(
|
||||
const 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<float>(componentIndex);
|
||||
const UIRect componentRect(componentX, layout.controlRect.y, componentWidth, layout.controlRect.height);
|
||||
const float prefixWidth = (std::min)(metrics.componentPrefixWidth, componentRect.width);
|
||||
const float labelGap = ClampNonNegative(metrics.componentLabelGap);
|
||||
const float valueX = componentRect.x + prefixWidth + labelGap;
|
||||
layout.componentRects[componentIndex] = componentRect;
|
||||
layout.componentPrefixRects[componentIndex] =
|
||||
UIRect(componentRect.x, componentRect.y, prefixWidth, componentRect.height);
|
||||
layout.componentValueRects[componentIndex] =
|
||||
UIRect(
|
||||
valueX,
|
||||
componentRect.y,
|
||||
ClampNonNegative(componentRect.width - prefixWidth - labelGap),
|
||||
componentRect.height);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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)
|
||||
|
||||
218
tests/UI/Editor/integration/shared/src/EditorValidationTheme.h
Normal file
218
tests/UI/Editor/integration/shared/src/EditorValidationTheme.h
Normal file
@@ -0,0 +1,218 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Style/DocumentStyleCompiler.h>
|
||||
#include <XCEngine/UI/Style/Theme.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace XCEngine::Tests::EditorUI {
|
||||
|
||||
struct EditorValidationThemeLoadResult {
|
||||
::XCEngine::UI::Style::UITheme theme = {};
|
||||
std::string error = {};
|
||||
bool succeeded = false;
|
||||
};
|
||||
|
||||
struct EditorValidationShellPalette {
|
||||
::XCEngine::UI::UIColor windowBackground = ::XCEngine::UI::UIColor(0.13f, 0.13f, 0.13f, 1.0f);
|
||||
::XCEngine::UI::UIColor cardBackground = ::XCEngine::UI::UIColor(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
::XCEngine::UI::UIColor cardBorder = ::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f);
|
||||
::XCEngine::UI::UIColor textPrimary = ::XCEngine::UI::UIColor(0.94f, 0.94f, 0.94f, 1.0f);
|
||||
::XCEngine::UI::UIColor textMuted = ::XCEngine::UI::UIColor(0.72f, 0.72f, 0.72f, 1.0f);
|
||||
::XCEngine::UI::UIColor textWeak = ::XCEngine::UI::UIColor(0.56f, 0.56f, 0.56f, 1.0f);
|
||||
::XCEngine::UI::UIColor textSuccess = ::XCEngine::UI::UIColor(0.63f, 0.76f, 0.63f, 1.0f);
|
||||
::XCEngine::UI::UIColor buttonBackground = ::XCEngine::UI::UIColor(0.25f, 0.25f, 0.25f, 1.0f);
|
||||
::XCEngine::UI::UIColor buttonHoverBackground = ::XCEngine::UI::UIColor(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
};
|
||||
|
||||
struct EditorValidationShellMetrics {
|
||||
float margin = 20.0f;
|
||||
float gap = 16.0f;
|
||||
float cardRadius = 10.0f;
|
||||
float buttonRadius = 8.0f;
|
||||
float titleFontSize = 17.0f;
|
||||
float bodyFontSize = 12.0f;
|
||||
};
|
||||
|
||||
inline ::XCEngine::UI::UIColor ToUIColor(const ::XCEngine::Math::Color& color) {
|
||||
return ::XCEngine::UI::UIColor(color.r, color.g, color.b, color.a);
|
||||
}
|
||||
|
||||
inline bool TryResolveThemeFloat(
|
||||
const ::XCEngine::UI::Style::UITheme& theme,
|
||||
std::string_view tokenName,
|
||||
float& outValue) {
|
||||
const auto resolution =
|
||||
theme.ResolveToken(std::string(tokenName), ::XCEngine::UI::Style::UIStyleValueType::Float);
|
||||
if (resolution.status != ::XCEngine::UI::Style::UITokenResolveStatus::Resolved) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float* value = resolution.value.TryGetFloat();
|
||||
if (value == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outValue = *value;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool TryResolveThemeColor(
|
||||
const ::XCEngine::UI::Style::UITheme& theme,
|
||||
std::string_view tokenName,
|
||||
::XCEngine::UI::UIColor& outColor) {
|
||||
const auto resolution =
|
||||
theme.ResolveToken(std::string(tokenName), ::XCEngine::UI::Style::UIStyleValueType::Color);
|
||||
if (resolution.status != ::XCEngine::UI::Style::UITokenResolveStatus::Resolved) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ::XCEngine::Math::Color* value = resolution.value.TryGetColor();
|
||||
if (value == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outColor = ToUIColor(*value);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline float ResolveThemeFloatAliases(
|
||||
const ::XCEngine::UI::Style::UITheme& theme,
|
||||
std::initializer_list<std::string_view> tokenNames,
|
||||
float fallbackValue) {
|
||||
float resolvedValue = fallbackValue;
|
||||
for (std::string_view tokenName : tokenNames) {
|
||||
if (TryResolveThemeFloat(theme, tokenName, resolvedValue)) {
|
||||
return resolvedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
inline ::XCEngine::UI::UIColor ResolveThemeColorAliases(
|
||||
const ::XCEngine::UI::Style::UITheme& theme,
|
||||
std::initializer_list<std::string_view> tokenNames,
|
||||
const ::XCEngine::UI::UIColor& fallbackValue) {
|
||||
::XCEngine::UI::UIColor resolvedValue = fallbackValue;
|
||||
for (std::string_view tokenName : tokenNames) {
|
||||
if (TryResolveThemeColor(theme, tokenName, resolvedValue)) {
|
||||
return resolvedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
inline EditorValidationThemeLoadResult LoadEditorValidationTheme(
|
||||
const std::filesystem::path& themePath) {
|
||||
EditorValidationThemeLoadResult result = {};
|
||||
|
||||
::XCEngine::Resources::UIDocumentCompileResult compileResult = {};
|
||||
const ::XCEngine::Containers::String pathString(themePath.generic_string().c_str());
|
||||
if (!::XCEngine::Resources::CompileUIDocument(
|
||||
::XCEngine::Resources::UIDocumentCompileRequest {
|
||||
::XCEngine::Resources::UIDocumentKind::Theme,
|
||||
pathString,
|
||||
::XCEngine::Resources::GetUIDocumentDefaultRootTag(
|
||||
::XCEngine::Resources::UIDocumentKind::Theme)
|
||||
},
|
||||
compileResult)) {
|
||||
result.error = compileResult.errorMessage.Empty()
|
||||
? std::string("Failed to compile editor validation theme document.")
|
||||
: std::string(compileResult.errorMessage.CStr());
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto styleCompileResult =
|
||||
::XCEngine::UI::Style::CompileDocumentStyle(compileResult.document);
|
||||
if (!styleCompileResult.succeeded) {
|
||||
result.error = styleCompileResult.errorMessage;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.theme = styleCompileResult.theme;
|
||||
result.succeeded = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
inline EditorValidationShellPalette ResolveEditorValidationShellPalette(
|
||||
const ::XCEngine::UI::Style::UITheme& theme) {
|
||||
EditorValidationShellPalette palette = {};
|
||||
palette.windowBackground = ResolveThemeColorAliases(
|
||||
theme,
|
||||
{ "editor.color.validation.window", "color.bg.workspace" },
|
||||
palette.windowBackground);
|
||||
palette.cardBackground = ResolveThemeColorAliases(
|
||||
theme,
|
||||
{ "editor.color.validation.card", "color.bg.panel" },
|
||||
palette.cardBackground);
|
||||
palette.cardBorder = ResolveThemeColorAliases(
|
||||
theme,
|
||||
{ "editor.color.validation.card_border", "editor.color.menu_popup.border" },
|
||||
palette.cardBorder);
|
||||
palette.textPrimary = ResolveThemeColorAliases(
|
||||
theme,
|
||||
{ "editor.color.validation.text_primary", "color.text.primary" },
|
||||
palette.textPrimary);
|
||||
palette.textMuted = ResolveThemeColorAliases(
|
||||
theme,
|
||||
{ "editor.color.validation.text_muted", "color.text.muted" },
|
||||
palette.textMuted);
|
||||
palette.textWeak = ResolveThemeColorAliases(
|
||||
theme,
|
||||
{ "editor.color.validation.text_weak", "color.text.muted" },
|
||||
palette.textWeak);
|
||||
palette.textSuccess = ResolveThemeColorAliases(
|
||||
theme,
|
||||
{ "editor.color.validation.text_success" },
|
||||
palette.textSuccess);
|
||||
palette.buttonBackground = ResolveThemeColorAliases(
|
||||
theme,
|
||||
{ "editor.color.validation.button", "color.bg.selection" },
|
||||
palette.buttonBackground);
|
||||
palette.buttonHoverBackground = ResolveThemeColorAliases(
|
||||
theme,
|
||||
{ "editor.color.validation.button_hover", "editor.color.validation.button", "color.bg.selection" },
|
||||
palette.buttonHoverBackground);
|
||||
return palette;
|
||||
}
|
||||
|
||||
inline EditorValidationShellMetrics ResolveEditorValidationShellMetrics(
|
||||
const ::XCEngine::UI::Style::UITheme& theme) {
|
||||
EditorValidationShellMetrics metrics = {};
|
||||
metrics.margin = ResolveThemeFloatAliases(
|
||||
theme,
|
||||
{ "editor.space.validation.margin", "space.shell" },
|
||||
metrics.margin);
|
||||
metrics.gap = ResolveThemeFloatAliases(
|
||||
theme,
|
||||
{ "editor.space.validation.gap" },
|
||||
metrics.gap);
|
||||
metrics.cardRadius = ResolveThemeFloatAliases(
|
||||
theme,
|
||||
{ "editor.radius.validation.card", "radius.panel" },
|
||||
metrics.cardRadius);
|
||||
metrics.buttonRadius = ResolveThemeFloatAliases(
|
||||
theme,
|
||||
{ "editor.radius.validation.button", "radius.control" },
|
||||
metrics.buttonRadius);
|
||||
metrics.titleFontSize = ResolveThemeFloatAliases(
|
||||
theme,
|
||||
{ "editor.font.validation.title" },
|
||||
metrics.titleFontSize);
|
||||
metrics.bodyFontSize = ResolveThemeFloatAliases(
|
||||
theme,
|
||||
{ "editor.font.validation.body", "editor.font.field.value" },
|
||||
metrics.bodyFontSize);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::Tests::EditorUI
|
||||
@@ -5,10 +5,156 @@
|
||||
<Color name="color.bg.selection" value="#3A3A3A" />
|
||||
<Color name="color.text.primary" value="#F2F2F2" />
|
||||
<Color name="color.text.muted" value="#B4B4B4" />
|
||||
<Color name="color.text.weak" value="#8F8F8F" />
|
||||
<Color name="color.text.success" value="#A6C3A6" />
|
||||
<Color name="color.border.panel" value="#4A4A4A" />
|
||||
<Color name="color.button.default" value="#343434" />
|
||||
<Color name="color.button.hover" value="#404040" />
|
||||
<Spacing name="space.shell" value="18" />
|
||||
<Spacing name="space.panel" value="12" />
|
||||
<Radius name="radius.panel" value="10" />
|
||||
<Radius name="radius.control" value="8" />
|
||||
<Spacing name="editor.space.validation.margin" value="20" />
|
||||
<Spacing name="editor.space.validation.gap" value="16" />
|
||||
<Radius name="editor.radius.validation.card" value="radius.panel" />
|
||||
<Radius name="editor.radius.validation.button" value="radius.control" />
|
||||
<Number name="editor.font.validation.title" value="17" />
|
||||
<Number name="editor.font.validation.body" value="12" />
|
||||
<Color name="editor.color.validation.window" value="color.bg.workspace" />
|
||||
<Color name="editor.color.validation.card" value="color.bg.panel" />
|
||||
<Color name="editor.color.validation.card_border" value="color.border.panel" />
|
||||
<Color name="editor.color.validation.text_primary" value="color.text.primary" />
|
||||
<Color name="editor.color.validation.text_muted" value="color.text.muted" />
|
||||
<Color name="editor.color.validation.text_weak" value="color.text.weak" />
|
||||
<Color name="editor.color.validation.text_success" value="color.text.success" />
|
||||
<Color name="editor.color.validation.button" value="color.button.default" />
|
||||
<Color name="editor.color.validation.button_hover" value="color.button.hover" />
|
||||
|
||||
<Number name="editor.size.field.row" value="22" />
|
||||
<Number name="editor.space.field.padding_x" value="12" />
|
||||
<Number name="editor.space.field.label_gap" value="20" />
|
||||
<Number name="editor.layout.field.control_column" value="236" />
|
||||
<Number name="editor.space.field.control_trailing_inset" value="8" />
|
||||
<Number name="editor.size.field.checkbox" value="14" />
|
||||
<Number name="editor.size.field.control_min_width" value="96" />
|
||||
<Number name="editor.space.field.vector_component_gap" value="6" />
|
||||
<Number name="editor.size.field.vector_component_min_width" value="72" />
|
||||
<Number name="editor.size.field.vector_prefix_width" value="9" />
|
||||
<Number name="editor.space.field.vector_prefix_gap" value="4" />
|
||||
<Number name="editor.space.field.vector_prefix_inset_x" value="0" />
|
||||
<Number name="editor.space.field.vector_prefix_inset_y" value="-1" />
|
||||
<Number name="editor.space.field.control_inset_y" value="1" />
|
||||
<Number name="editor.space.field.label_inset_y" value="0" />
|
||||
<Number name="editor.space.field.value_inset_x" value="5" />
|
||||
<Number name="editor.space.field.value_inset_y" value="0" />
|
||||
<Number name="editor.space.field.checkbox_glyph_inset_x" value="1" />
|
||||
<Number name="editor.space.field.checkbox_glyph_inset_y" value="-2" />
|
||||
<Number name="editor.size.field.dropdown_arrow_width" value="16" />
|
||||
<Number name="editor.space.field.dropdown_arrow_inset_x" value="4" />
|
||||
<Number name="editor.space.field.dropdown_arrow_inset_y" value="3" />
|
||||
<Number name="editor.radius.field.row" value="0" />
|
||||
<Number name="editor.radius.field.control" value="2" />
|
||||
<Number name="editor.border.field" value="1" />
|
||||
<Number name="editor.border.field.focus" value="1" />
|
||||
<Number name="editor.font.field.label" value="11" />
|
||||
<Number name="editor.font.field.value" value="12" />
|
||||
<Number name="editor.font.field.glyph" value="10" />
|
||||
<Color name="editor.color.field.row" value="#00000000" />
|
||||
<Color name="editor.color.field.row_hover" value="#2A2A2AFF" />
|
||||
<Color name="editor.color.field.row_active" value="#313131FF" />
|
||||
<Color name="editor.color.field.border" value="#00000000" />
|
||||
<Color name="editor.color.field.border_focus" value="#00000000" />
|
||||
<Color name="editor.color.field.label" value="#C9C9C9FF" />
|
||||
<Color name="editor.color.field.value" value="#EEEEEEFF" />
|
||||
<Color name="editor.color.field.value_readonly" value="#8E8E8EFF" />
|
||||
<Color name="editor.color.field.control" value="#2E2E2EFF" />
|
||||
<Color name="editor.color.field.control_hover" value="#353535FF" />
|
||||
<Color name="editor.color.field.control_editing" value="#3A3A3AFF" />
|
||||
<Color name="editor.color.field.control_readonly" value="#252525FF" />
|
||||
<Color name="editor.color.field.control_border" value="#242424FF" />
|
||||
<Color name="editor.color.field.control_border_focus" value="#2A2A2AFF" />
|
||||
<Color name="editor.color.field.vector_prefix" value="#222222FF" />
|
||||
<Color name="editor.color.field.vector_prefix_border" value="#343434FF" />
|
||||
<Color name="editor.color.field.vector_axis_x" value="#A8A8A8FF" />
|
||||
<Color name="editor.color.field.vector_axis_y" value="#A8A8A8FF" />
|
||||
<Color name="editor.color.field.vector_axis_z" value="#A8A8A8FF" />
|
||||
<Color name="editor.color.field.checkbox" value="#1C1C1CFF" />
|
||||
<Color name="editor.color.field.checkbox_hover" value="#202020FF" />
|
||||
<Color name="editor.color.field.checkbox_border" value="#343434FF" />
|
||||
<Color name="editor.color.field.checkbox_mark" value="#D8D8D8FF" />
|
||||
<Color name="editor.color.field.dropdown_arrow" value="#D0D0D0FF" />
|
||||
|
||||
<Number name="editor.space.property.content_inset" value="6" />
|
||||
<Number name="editor.space.property.section_gap" value="4" />
|
||||
<Number name="editor.size.property.section_header" value="24" />
|
||||
<Number name="editor.size.property.field_row" value="24" />
|
||||
<Number name="editor.space.property.row_gap" value="1" />
|
||||
<Number name="editor.size.property.disclosure" value="10" />
|
||||
<Number name="editor.space.property.disclosure_label_gap" value="6" />
|
||||
<Number name="editor.space.property.section_inset_y" value="5" />
|
||||
<Number name="editor.space.property.disclosure_glyph_inset_x" value="1" />
|
||||
<Number name="editor.space.property.disclosure_glyph_inset_y" value="-1" />
|
||||
<Number name="editor.space.property.label_inset_y" value="5" />
|
||||
<Number name="editor.space.property.value_inset_y" value="4" />
|
||||
<Number name="editor.space.property.value_box_inset_y" value="2" />
|
||||
<Number name="editor.space.property.value_box_inset_x" value="6" />
|
||||
<Number name="editor.radius.property.panel" value="0" />
|
||||
<Number name="editor.radius.property.value" value="2" />
|
||||
<Number name="editor.border.property" value="1" />
|
||||
<Number name="editor.border.property.focus" value="1" />
|
||||
<Number name="editor.border.property.edit" value="1" />
|
||||
<Number name="editor.font.property.section" value="11" />
|
||||
<Number name="editor.font.property.disclosure" value="10" />
|
||||
<Number name="editor.font.property.label" value="11" />
|
||||
<Number name="editor.font.property.value" value="12" />
|
||||
<Number name="editor.font.property.tag" value="10" />
|
||||
<Color name="editor.color.property.surface" value="#232323FF" />
|
||||
<Color name="editor.color.property.border" value="#171717FF" />
|
||||
<Color name="editor.color.property.border_focus" value="#717171FF" />
|
||||
<Color name="editor.color.property.section" value="#2B2B2BFF" />
|
||||
<Color name="editor.color.property.section_hover" value="#313131FF" />
|
||||
<Color name="editor.color.property.field_hover" value="#262626FF" />
|
||||
<Color name="editor.color.property.field_selected" value="#303030FF" />
|
||||
<Color name="editor.color.property.field_selected_focused" value="#393939FF" />
|
||||
<Color name="editor.color.property.value" value="#1C1C1CFF" />
|
||||
<Color name="editor.color.property.value_hover" value="#222222FF" />
|
||||
<Color name="editor.color.property.value_editing" value="#292929FF" />
|
||||
<Color name="editor.color.property.value_readonly" value="#171717FF" />
|
||||
<Color name="editor.color.property.value_border" value="#343434FF" />
|
||||
<Color name="editor.color.property.value_border_editing" value="#767676FF" />
|
||||
<Color name="editor.color.property.disclosure" value="#C9C9C9FF" />
|
||||
<Color name="editor.color.property.section_text" value="#E2E2E2FF" />
|
||||
<Color name="editor.color.property.label" value="#CDCDCDFF" />
|
||||
<Color name="editor.color.property.value_text" value="#EFEFEFFF" />
|
||||
<Color name="editor.color.property.value_text_readonly" value="#8E8E8EFF" />
|
||||
<Color name="editor.color.property.edit_tag" value="#82A7DAFF" />
|
||||
|
||||
<Number name="editor.space.menu_popup.padding_x" value="6" />
|
||||
<Number name="editor.space.menu_popup.padding_y" value="5" />
|
||||
<Number name="editor.size.menu_popup.item" value="24" />
|
||||
<Number name="editor.size.menu_popup.separator" value="8" />
|
||||
<Number name="editor.size.menu_popup.check_column" value="16" />
|
||||
<Number name="editor.space.menu_popup.shortcut_gap" value="18" />
|
||||
<Number name="editor.size.menu_popup.submenu_indicator" value="12" />
|
||||
<Number name="editor.radius.menu_popup.row" value="3" />
|
||||
<Number name="editor.radius.menu_popup.surface" value="4" />
|
||||
<Number name="editor.space.menu_popup.label_inset_x" value="10" />
|
||||
<Number name="editor.space.menu_popup.label_inset_y" value="-1" />
|
||||
<Number name="editor.font.menu_popup.label" value="11" />
|
||||
<Number name="editor.space.menu_popup.shortcut_inset_right" value="18" />
|
||||
<Number name="editor.size.menu_popup.estimated_glyph_width" value="6" />
|
||||
<Number name="editor.font.menu_popup.glyph" value="10" />
|
||||
<Number name="editor.border.menu_popup.separator" value="1" />
|
||||
<Number name="editor.border.menu_popup.surface" value="1" />
|
||||
<Color name="editor.color.menu_popup.surface" value="#242424FF" />
|
||||
<Color name="editor.color.menu_popup.border" value="#343434FF" />
|
||||
<Color name="editor.color.menu_popup.item_hover" value="#2C2C2CFF" />
|
||||
<Color name="editor.color.menu_popup.item_open" value="#313131FF" />
|
||||
<Color name="editor.color.menu_popup.separator" value="#3A3A3AFF" />
|
||||
<Color name="editor.color.menu_popup.label" value="#E8E8E8FF" />
|
||||
<Color name="editor.color.menu_popup.text_muted" value="#B9B9B9FF" />
|
||||
<Color name="editor.color.menu_popup.text_disabled" value="#757575FF" />
|
||||
<Color name="editor.color.menu_popup.glyph" value="#D0D0D0FF" />
|
||||
</Tokens>
|
||||
|
||||
<Widgets>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Core/UIEditorBoolFieldInteraction.h>
|
||||
#include <XCEditor/Core/UIEditorTheme.h>
|
||||
#include <XCEditor/Widgets/UIEditorBoolField.h>
|
||||
#include "EditorValidationTheme.h"
|
||||
#include "Host/AutoScreenshot.h"
|
||||
#include "Host/NativeRenderer.h"
|
||||
|
||||
@@ -45,19 +47,11 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorBoolField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
|
||||
namespace Style = XCEngine::UI::Style;
|
||||
|
||||
constexpr const wchar_t* kWindowClassName = L"XCUIEditorBoolFieldBasicValidation";
|
||||
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | BoolField Basic";
|
||||
|
||||
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
|
||||
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f);
|
||||
constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f);
|
||||
constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f);
|
||||
constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f);
|
||||
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
|
||||
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
|
||||
enum class ActionId : unsigned char {
|
||||
Reset = 0,
|
||||
Capture
|
||||
@@ -83,10 +77,14 @@ std::filesystem::path ResolveRepoRootPath() {
|
||||
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||
root = root.substr(1u, root.size() - 2u);
|
||||
}
|
||||
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveValidationThemePath() {
|
||||
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||
return x >= rect.x &&
|
||||
x <= rect.x + rect.width &&
|
||||
@@ -105,10 +103,13 @@ std::int32_t MapBoolFieldKey(UINT keyCode) {
|
||||
}
|
||||
}
|
||||
|
||||
ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||
constexpr float margin = 20.0f;
|
||||
ScenarioLayout BuildScenarioLayout(
|
||||
float width,
|
||||
float height,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
|
||||
const float margin = shellMetrics.margin;
|
||||
constexpr float leftWidth = 430.0f;
|
||||
constexpr float gap = 16.0f;
|
||||
const float gap = shellMetrics.gap;
|
||||
|
||||
ScenarioLayout layout = {};
|
||||
layout.introRect = UIRect(margin, margin, leftWidth, 214.0f);
|
||||
@@ -141,33 +142,48 @@ ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||
void DrawCard(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
std::string_view title,
|
||||
std::string_view subtitle = {}) {
|
||||
drawList.AddFilledRect(rect, kCardBg, 10.0f);
|
||||
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
|
||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
|
||||
drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius);
|
||||
drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
|
||||
std::string(title),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.titleFontSize);
|
||||
if (!subtitle.empty()) {
|
||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 40.0f),
|
||||
std::string(subtitle),
|
||||
shellPalette.textMuted,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawButton(
|
||||
UIDrawList& drawList,
|
||||
const ButtonLayout& button,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
bool hovered) {
|
||||
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
|
||||
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
|
||||
drawList.AddFilledRect(
|
||||
button.rect,
|
||||
hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground,
|
||||
shellMetrics.buttonRadius);
|
||||
drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
|
||||
button.label,
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
|
||||
std::string DescribeHitTarget(const UIEditorBoolFieldHitTarget& hitTarget) {
|
||||
switch (hitTarget.kind) {
|
||||
case UIEditorBoolFieldHitTargetKind::Toggle:
|
||||
return "toggle";
|
||||
case UIEditorBoolFieldHitTargetKind::Checkbox:
|
||||
return "checkbox";
|
||||
case UIEditorBoolFieldHitTargetKind::Row:
|
||||
return "row";
|
||||
case UIEditorBoolFieldHitTargetKind::None:
|
||||
@@ -355,6 +371,14 @@ private:
|
||||
m_captureRoot =
|
||||
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/bool_field_basic/captures";
|
||||
m_autoScreenshot.Initialize(m_captureRoot);
|
||||
const auto themeLoad =
|
||||
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
|
||||
if (themeLoad.succeeded) {
|
||||
m_theme = themeLoad.theme;
|
||||
m_themeStatus = "loaded";
|
||||
} else {
|
||||
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
|
||||
}
|
||||
|
||||
ResetScenario();
|
||||
return true;
|
||||
@@ -380,7 +404,10 @@ private:
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
return BuildScenarioLayout(width, height);
|
||||
return BuildScenarioLayout(
|
||||
width,
|
||||
height,
|
||||
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
|
||||
}
|
||||
|
||||
void ResetScenario() {
|
||||
@@ -403,12 +430,14 @@ private:
|
||||
}
|
||||
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme);
|
||||
m_frame = UpdateUIEditorBoolFieldInteraction(
|
||||
m_interactionState,
|
||||
m_value,
|
||||
layout.fieldRect,
|
||||
m_spec,
|
||||
{});
|
||||
{},
|
||||
metrics);
|
||||
}
|
||||
|
||||
void OnResize(UINT width, UINT height) {
|
||||
@@ -502,18 +531,20 @@ private:
|
||||
|
||||
UIEditorBoolFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme);
|
||||
m_frame = UpdateUIEditorBoolFieldInteraction(
|
||||
m_interactionState,
|
||||
m_value,
|
||||
layout.fieldRect,
|
||||
m_spec,
|
||||
std::move(events));
|
||||
std::move(events),
|
||||
metrics);
|
||||
return m_frame.result;
|
||||
}
|
||||
|
||||
void UpdateResultText(const UIEditorBoolFieldInteractionResult& result) {
|
||||
if (result.valueChanged) {
|
||||
m_lastResult = std::string("值已切换到 ") + (m_value ? "true" : "false");
|
||||
m_lastResult = std::string("值已切换到: ") + (m_value ? "true" : "false");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -547,76 +578,90 @@ private:
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
const ScenarioLayout layout = BuildScenarioLayout(width, height);
|
||||
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
|
||||
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
|
||||
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
|
||||
RefreshFrame();
|
||||
|
||||
const UIEditorBoolFieldHitTarget currentHit =
|
||||
HitTestUIEditorBoolField(m_frame.layout, m_mousePosition);
|
||||
const auto boolMetrics = XCEngine::UI::Editor::ResolveUIEditorBoolFieldMetrics(m_theme);
|
||||
const auto boolPalette = XCEngine::UI::Editor::ResolveUIEditorBoolFieldPalette(m_theme);
|
||||
|
||||
UIDrawData drawData = {};
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("EditorBoolFieldBasic");
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.introRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"这个测试在验证什么功能",
|
||||
"只验证 Editor BoolField 的基础交互契约,不涉及 PropertyGrid 或任何业务 Inspector。");
|
||||
"只验证 Editor BoolField 的基础交互契约,不涉及 PropertyGrid 或业务 Inspector。");
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
|
||||
"1. 点击 row 或 toggle,检查 true / false 是否稳定切换。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
"1. 点击 row 或 checkbox,检查 true / false 是否稳定切换。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
||||
"2. 控件获得 focus 后按 Space / Enter,也必须能切换值。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
|
||||
"3. 检查 Hover / Focus / Value / Result 是否同步更新。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
|
||||
"4. 按 F12 或点击截图按钮,确认自动截图路径正确。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
|
||||
DrawCard(drawList, layout.controlRect, "操作");
|
||||
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作");
|
||||
for (const ButtonLayout& button : layout.buttons) {
|
||||
DrawButton(
|
||||
drawList,
|
||||
button,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
m_hasHoveredAction && m_hoveredAction == button.action);
|
||||
}
|
||||
|
||||
DrawCard(drawList, layout.stateRect, "状态摘要", "重点检查 hit / focus / value / result。");
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.stateRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"状态摘要",
|
||||
"重点检查 hit / focus / value / result。");
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
|
||||
"Hover: " + DescribeHitTarget(currentHit),
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
|
||||
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "开" : "关"),
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
|
||||
std::string("Active: ") + (m_interactionState.fieldState.active ? "开" : "关"),
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
|
||||
std::string("Value: ") + (m_value ? "true" : "false"),
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
|
||||
"Result: " + m_lastResult,
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
|
||||
const std::string captureSummary =
|
||||
m_autoScreenshot.HasPendingCapture()
|
||||
@@ -627,17 +672,30 @@ private:
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
|
||||
captureSummary,
|
||||
kTextWeak,
|
||||
12.0f);
|
||||
shellPalette.textWeak,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
|
||||
"Theme: " + m_themeStatus,
|
||||
shellPalette.textWeak,
|
||||
shellMetrics.bodyFontSize);
|
||||
|
||||
DrawCard(drawList, layout.previewRect, "BoolField 预览", "这里只放一个 BoolField。");
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.previewRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"BoolField 预览",
|
||||
"这里只放一个 Unity 风格 BoolField。");
|
||||
UIEditorBoolFieldSpec previewSpec = m_spec;
|
||||
previewSpec.value = m_value;
|
||||
AppendUIEditorBoolField(
|
||||
drawList,
|
||||
layout.fieldRect,
|
||||
previewSpec,
|
||||
m_interactionState.fieldState);
|
||||
m_interactionState.fieldState,
|
||||
boolPalette,
|
||||
boolMetrics);
|
||||
|
||||
const bool framePresented = m_renderer.Render(drawData);
|
||||
m_autoScreenshot.CaptureIfRequested(
|
||||
@@ -657,10 +715,12 @@ private:
|
||||
bool m_value = false;
|
||||
UIEditorBoolFieldInteractionState m_interactionState = {};
|
||||
UIEditorBoolFieldInteractionFrame m_frame = {};
|
||||
Style::UITheme m_theme = {};
|
||||
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
ActionId m_hoveredAction = ActionId::Reset;
|
||||
bool m_hasHoveredAction = false;
|
||||
std::string m_lastResult = {};
|
||||
std::string m_themeStatus = "fallback";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Core/UIEditorEnumFieldInteraction.h>
|
||||
#include <XCEditor/Core/UIEditorTheme.h>
|
||||
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
||||
#include <XCEditor/Widgets/UIEditorMenuPopup.h>
|
||||
#include "EditorValidationTheme.h"
|
||||
#include "Host/AutoScreenshot.h"
|
||||
#include "Host/NativeRenderer.h"
|
||||
|
||||
@@ -41,24 +44,18 @@ using XCEngine::UI::Editor::UIEditorEnumFieldInteractionResult;
|
||||
using XCEngine::UI::Editor::UIEditorEnumFieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorEnumFieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumField;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorMenuPopup;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorEnumField;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorEnumFieldValueText;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorMenuPopupInvalidIndex;
|
||||
namespace Style = XCEngine::UI::Style;
|
||||
|
||||
constexpr const wchar_t* kWindowClassName = L"XCUIEditorEnumFieldBasicValidation";
|
||||
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | EnumField Basic";
|
||||
|
||||
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
|
||||
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f);
|
||||
constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f);
|
||||
constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f);
|
||||
constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f);
|
||||
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
|
||||
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
|
||||
enum class ActionId : unsigned char {
|
||||
Reset = 0,
|
||||
Capture
|
||||
@@ -87,6 +84,11 @@ std::filesystem::path ResolveRepoRootPath() {
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveValidationThemePath() {
|
||||
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||
return x >= rect.x &&
|
||||
x <= rect.x + rect.width &&
|
||||
@@ -96,34 +98,41 @@ bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||
|
||||
std::int32_t MapEnumFieldKey(UINT keyCode) {
|
||||
switch (keyCode) {
|
||||
case VK_LEFT:
|
||||
return static_cast<std::int32_t>(KeyCode::Left);
|
||||
case VK_RIGHT:
|
||||
return static_cast<std::int32_t>(KeyCode::Right);
|
||||
case VK_UP:
|
||||
return static_cast<std::int32_t>(KeyCode::Up);
|
||||
case VK_DOWN:
|
||||
return static_cast<std::int32_t>(KeyCode::Down);
|
||||
case VK_HOME:
|
||||
return static_cast<std::int32_t>(KeyCode::Home);
|
||||
case VK_END:
|
||||
return static_cast<std::int32_t>(KeyCode::End);
|
||||
case VK_RETURN:
|
||||
return static_cast<std::int32_t>(KeyCode::Enter);
|
||||
case VK_SPACE:
|
||||
return static_cast<std::int32_t>(KeyCode::Space);
|
||||
case VK_ESCAPE:
|
||||
return static_cast<std::int32_t>(KeyCode::Escape);
|
||||
default:
|
||||
return static_cast<std::int32_t>(KeyCode::None);
|
||||
}
|
||||
}
|
||||
|
||||
ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||
constexpr float margin = 20.0f;
|
||||
constexpr float leftWidth = 430.0f;
|
||||
constexpr float gap = 16.0f;
|
||||
ScenarioLayout BuildScenarioLayout(
|
||||
float width,
|
||||
float height,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
|
||||
const float margin = shellMetrics.margin;
|
||||
constexpr float leftWidth = 440.0f;
|
||||
const float gap = shellMetrics.gap;
|
||||
|
||||
ScenarioLayout layout = {};
|
||||
layout.introRect = UIRect(margin, margin, leftWidth, 220.0f);
|
||||
layout.introRect = UIRect(margin, margin, leftWidth, 240.0f);
|
||||
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
|
||||
layout.stateRect = UIRect(
|
||||
margin,
|
||||
layout.controlRect.y + layout.controlRect.height + gap,
|
||||
leftWidth,
|
||||
(std::max)(220.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
|
||||
(std::max)(250.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
|
||||
layout.previewRect = UIRect(
|
||||
leftWidth + margin * 2.0f,
|
||||
margin,
|
||||
@@ -132,7 +141,7 @@ ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||
layout.fieldRect = UIRect(
|
||||
layout.previewRect.x + 24.0f,
|
||||
layout.previewRect.y + 82.0f,
|
||||
320.0f,
|
||||
340.0f,
|
||||
32.0f);
|
||||
|
||||
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
|
||||
@@ -147,31 +156,48 @@ ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||
void DrawCard(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
std::string_view title,
|
||||
std::string_view subtitle = {}) {
|
||||
drawList.AddFilledRect(rect, kCardBg, 10.0f);
|
||||
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
|
||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
|
||||
drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius);
|
||||
drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
|
||||
std::string(title),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.titleFontSize);
|
||||
if (!subtitle.empty()) {
|
||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 40.0f),
|
||||
std::string(subtitle),
|
||||
shellPalette.textMuted,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawButton(
|
||||
UIDrawList& drawList,
|
||||
const ButtonLayout& button,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
bool hovered) {
|
||||
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
|
||||
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
|
||||
drawList.AddText(UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f);
|
||||
drawList.AddFilledRect(
|
||||
button.rect,
|
||||
hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground,
|
||||
shellMetrics.buttonRadius);
|
||||
drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
|
||||
button.label,
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
|
||||
std::string DescribeHitTarget(const UIEditorEnumFieldHitTarget& hitTarget) {
|
||||
switch (hitTarget.kind) {
|
||||
case UIEditorEnumFieldHitTargetKind::PreviousButton:
|
||||
return "previous";
|
||||
case UIEditorEnumFieldHitTargetKind::NextButton:
|
||||
return "next";
|
||||
case UIEditorEnumFieldHitTargetKind::DropdownArrow:
|
||||
return "dropdown_arrow";
|
||||
case UIEditorEnumFieldHitTargetKind::ValueBox:
|
||||
return "value_box";
|
||||
case UIEditorEnumFieldHitTargetKind::Row:
|
||||
@@ -361,6 +387,14 @@ private:
|
||||
m_captureRoot =
|
||||
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/enum_field_basic/captures";
|
||||
m_autoScreenshot.Initialize(m_captureRoot);
|
||||
const auto themeLoad =
|
||||
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
|
||||
if (themeLoad.succeeded) {
|
||||
m_theme = themeLoad.theme;
|
||||
m_themeStatus = "loaded";
|
||||
} else {
|
||||
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
|
||||
}
|
||||
|
||||
ResetScenario();
|
||||
return true;
|
||||
@@ -386,7 +420,20 @@ private:
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
return BuildScenarioLayout(width, height);
|
||||
return BuildScenarioLayout(
|
||||
width,
|
||||
height,
|
||||
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
|
||||
}
|
||||
|
||||
UIRect GetViewportRect() const {
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
return UIRect(
|
||||
0.0f,
|
||||
0.0f,
|
||||
static_cast<float>((std::max)(1L, clientRect.right - clientRect.left)),
|
||||
static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top)));
|
||||
}
|
||||
|
||||
void ResetScenario() {
|
||||
@@ -410,14 +457,19 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
m_spec.selectedIndex = m_selectedIndex;
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme);
|
||||
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
|
||||
m_spec.selectedIndex = m_selectedIndex;
|
||||
m_frame = UpdateUIEditorEnumFieldInteraction(
|
||||
m_interactionState,
|
||||
m_selectedIndex,
|
||||
layout.fieldRect,
|
||||
m_spec,
|
||||
{});
|
||||
{},
|
||||
fieldMetrics,
|
||||
popupMetrics,
|
||||
GetViewportRect());
|
||||
m_spec.selectedIndex = m_selectedIndex;
|
||||
}
|
||||
|
||||
@@ -511,13 +563,18 @@ private:
|
||||
|
||||
UIEditorEnumFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme);
|
||||
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
|
||||
m_spec.selectedIndex = m_selectedIndex;
|
||||
m_frame = UpdateUIEditorEnumFieldInteraction(
|
||||
m_interactionState,
|
||||
m_selectedIndex,
|
||||
layout.fieldRect,
|
||||
m_spec,
|
||||
std::move(events));
|
||||
std::move(events),
|
||||
fieldMetrics,
|
||||
popupMetrics,
|
||||
GetViewportRect());
|
||||
m_spec.selectedIndex = m_selectedIndex;
|
||||
return m_frame.result;
|
||||
}
|
||||
@@ -527,6 +584,14 @@ private:
|
||||
m_lastResult = std::string("已切换到: ") + m_spec.options[m_selectedIndex];
|
||||
return;
|
||||
}
|
||||
if (result.popupOpened) {
|
||||
m_lastResult = "下拉菜单已展开";
|
||||
return;
|
||||
}
|
||||
if (result.popupClosed) {
|
||||
m_lastResult = "下拉菜单已关闭";
|
||||
return;
|
||||
}
|
||||
if (result.consumed) {
|
||||
m_lastResult = "控件已消费输入";
|
||||
return;
|
||||
@@ -534,6 +599,16 @@ private:
|
||||
m_lastResult = "等待交互";
|
||||
}
|
||||
|
||||
std::string ResolveHighlightedText() const {
|
||||
if (!m_frame.popupOpen ||
|
||||
m_frame.popupState.hoveredIndex == UIEditorMenuPopupInvalidIndex ||
|
||||
m_frame.popupState.hoveredIndex >= m_spec.options.size()) {
|
||||
return "(none)";
|
||||
}
|
||||
|
||||
return m_spec.options[m_frame.popupState.hoveredIndex];
|
||||
}
|
||||
|
||||
void ExecuteAction(ActionId action) {
|
||||
switch (action) {
|
||||
case ActionId::Reset:
|
||||
@@ -551,77 +626,103 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
const ScenarioLayout layout = BuildScenarioLayout(width, height);
|
||||
const UIRect viewportRect = GetViewportRect();
|
||||
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
|
||||
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
|
||||
const ScenarioLayout layout = BuildScenarioLayout(viewportRect.width, viewportRect.height, shellMetrics);
|
||||
RefreshFrame();
|
||||
|
||||
const UIEditorEnumFieldHitTarget currentHit =
|
||||
HitTestUIEditorEnumField(m_frame.layout, m_mousePosition);
|
||||
const auto enumMetrics = XCEngine::UI::Editor::ResolveUIEditorEnumFieldMetrics(m_theme);
|
||||
const auto enumPalette = XCEngine::UI::Editor::ResolveUIEditorEnumFieldPalette(m_theme);
|
||||
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
|
||||
const auto popupPalette = XCEngine::UI::Editor::ResolveUIEditorMenuPopupPalette(m_theme);
|
||||
|
||||
UIDrawData drawData = {};
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("EditorEnumFieldBasic");
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
|
||||
drawList.AddFilledRect(viewportRect, shellPalette.windowBackground);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.introRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"这个测试在验证什么功能",
|
||||
"只验证 Editor EnumField 基础控件,不涉及 PropertyGrid 或业务 Inspector。");
|
||||
"只验证 Editor EnumField 的基础交互契约,不涉及 PropertyGrid 或业务 Inspector。");
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
|
||||
"1. 点击 < / > 按钮,检查枚举选项是否稳定切换。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
"1. 点击 value box 或 dropdown arrow,检查下拉菜单是否展开/收起。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
||||
"2. 控件获得 focus 后按 Left / Right / Home / End,检查键盘切换。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
"2. 展开后 hover 列表项,检查高亮是否稳定跟随。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
|
||||
"3. 检查 Hover / Focus / Selected / Result 是否同步更新。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
"3. 获得 focus 后按 Up / Down / Home / End,再按 Enter / Space 选中,Esc 关闭。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
|
||||
"4. 这个场景只覆盖基础交互契约,不提前承载任何业务面板。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
"4. 检查 Hover / Popup / Highlight / Selected / Result 是否同步更新。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
|
||||
"5. 按 F12 或点击截图按钮,确认自动截图路径正确。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
|
||||
DrawCard(drawList, layout.controlRect, "操作");
|
||||
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作");
|
||||
for (const ButtonLayout& button : layout.buttons) {
|
||||
DrawButton(drawList, button, m_hasHoveredAction && m_hoveredAction == button.action);
|
||||
DrawButton(
|
||||
drawList,
|
||||
button,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
m_hasHoveredAction && m_hoveredAction == button.action);
|
||||
}
|
||||
|
||||
DrawCard(drawList, layout.stateRect, "状态摘要");
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.stateRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"状态摘要",
|
||||
"重点检查 hit / popup / highlight / selected / result。");
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
|
||||
"Hover: " + DescribeHitTarget(currentHit),
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
|
||||
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "是" : "否"),
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
|
||||
std::string("Active: ") + (m_interactionState.fieldState.active ? "是" : "否"),
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
std::string("Popup: ") + (m_frame.popupOpen ? "open" : "closed"),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
|
||||
"Selected: " + ResolveUIEditorEnumFieldValueText(m_spec),
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
"Highlight: " + ResolveHighlightedText(),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
|
||||
"Selected: " + ResolveUIEditorEnumFieldValueText(m_spec),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
|
||||
"Result: " + m_lastResult,
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
|
||||
const std::string captureSummary =
|
||||
m_autoScreenshot.HasPendingCapture()
|
||||
@@ -630,24 +731,46 @@ private:
|
||||
? std::string("F12 -> tests/UI/Editor/integration/shell/enum_field_basic/captures/")
|
||||
: m_autoScreenshot.GetLastCaptureSummary());
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
|
||||
captureSummary,
|
||||
kTextWeak,
|
||||
12.0f);
|
||||
shellPalette.textWeak,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
|
||||
"Theme: " + m_themeStatus,
|
||||
shellPalette.textWeak,
|
||||
shellMetrics.bodyFontSize);
|
||||
|
||||
DrawCard(drawList, layout.previewRect, "EnumField 预览", "这里只放一个 EnumField。");
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.previewRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"EnumField 预览",
|
||||
"这里只放一个 Unity 风格 EnumField。");
|
||||
AppendUIEditorEnumField(
|
||||
drawList,
|
||||
layout.fieldRect,
|
||||
m_spec,
|
||||
m_interactionState.fieldState);
|
||||
m_interactionState.fieldState,
|
||||
enumPalette,
|
||||
enumMetrics);
|
||||
if (m_frame.popupOpen) {
|
||||
AppendUIEditorMenuPopup(
|
||||
drawList,
|
||||
m_frame.popupLayout.popupRect,
|
||||
m_frame.popupItems,
|
||||
m_frame.popupState,
|
||||
popupPalette,
|
||||
popupMetrics);
|
||||
}
|
||||
|
||||
const bool framePresented = m_renderer.Render(drawData);
|
||||
m_autoScreenshot.CaptureIfRequested(
|
||||
m_renderer,
|
||||
drawData,
|
||||
static_cast<unsigned int>(width),
|
||||
static_cast<unsigned int>(height),
|
||||
static_cast<unsigned int>(viewportRect.width),
|
||||
static_cast<unsigned int>(viewportRect.height),
|
||||
framePresented);
|
||||
}
|
||||
|
||||
@@ -660,10 +783,12 @@ private:
|
||||
std::size_t m_selectedIndex = 0u;
|
||||
UIEditorEnumFieldInteractionState m_interactionState = {};
|
||||
UIEditorEnumFieldInteractionFrame m_frame = {};
|
||||
Style::UITheme m_theme = {};
|
||||
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
ActionId m_hoveredAction = ActionId::Reset;
|
||||
bool m_hasHoveredAction = false;
|
||||
std::string m_lastResult = {};
|
||||
std::string m_themeStatus = "fallback";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Core/UIEditorNumberFieldInteraction.h>
|
||||
#include <XCEditor/Core/UIEditorTheme.h>
|
||||
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||
#include "EditorValidationTheme.h"
|
||||
#include "Host/AutoScreenshot.h"
|
||||
#include "Host/NativeRenderer.h"
|
||||
|
||||
@@ -46,19 +48,11 @@ using XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
|
||||
namespace Style = XCEngine::UI::Style;
|
||||
|
||||
constexpr const wchar_t* kWindowClassName = L"XCUIEditorNumberFieldBasicValidation";
|
||||
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | NumberField Basic";
|
||||
|
||||
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
|
||||
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f);
|
||||
constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f);
|
||||
constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f);
|
||||
constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f);
|
||||
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
|
||||
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
|
||||
enum class ActionId : unsigned char {
|
||||
Reset = 0,
|
||||
Capture
|
||||
@@ -75,6 +69,9 @@ struct ScenarioLayout {
|
||||
UIRect controlRect = {};
|
||||
UIRect stateRect = {};
|
||||
UIRect previewRect = {};
|
||||
UIRect inspectorRect = {};
|
||||
UIRect inspectorHeaderRect = {};
|
||||
UIRect sectionRect = {};
|
||||
UIRect fieldRect = {};
|
||||
std::vector<ButtonLayout> buttons = {};
|
||||
};
|
||||
@@ -87,6 +84,11 @@ std::filesystem::path ResolveRepoRootPath() {
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveValidationThemePath() {
|
||||
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||
return x >= rect.x &&
|
||||
x <= rect.x + rect.width &&
|
||||
@@ -117,13 +119,16 @@ std::int32_t MapNumberFieldKey(UINT keyCode) {
|
||||
}
|
||||
}
|
||||
|
||||
ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||
constexpr float margin = 20.0f;
|
||||
constexpr float leftWidth = 430.0f;
|
||||
constexpr float gap = 16.0f;
|
||||
ScenarioLayout BuildScenarioLayout(
|
||||
float width,
|
||||
float height,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
|
||||
const float margin = shellMetrics.margin;
|
||||
constexpr float leftWidth = 460.0f;
|
||||
const float gap = shellMetrics.gap;
|
||||
|
||||
ScenarioLayout layout = {};
|
||||
layout.introRect = UIRect(margin, margin, leftWidth, 232.0f);
|
||||
layout.introRect = UIRect(margin, margin, leftWidth, 250.0f);
|
||||
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
|
||||
layout.stateRect = UIRect(
|
||||
margin,
|
||||
@@ -135,11 +140,26 @@ ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||
margin,
|
||||
(std::max)(420.0f, width - leftWidth - margin * 3.0f),
|
||||
height - margin * 2.0f);
|
||||
layout.inspectorRect = UIRect(
|
||||
layout.previewRect.x + 18.0f,
|
||||
layout.previewRect.y + 54.0f,
|
||||
(std::min)(392.0f, layout.previewRect.width - 36.0f),
|
||||
150.0f);
|
||||
layout.inspectorHeaderRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.inspectorRect.y,
|
||||
layout.inspectorRect.width,
|
||||
24.0f);
|
||||
layout.sectionRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.inspectorRect.y + layout.inspectorHeaderRect.height,
|
||||
layout.inspectorRect.width,
|
||||
24.0f);
|
||||
layout.fieldRect = UIRect(
|
||||
layout.previewRect.x + 24.0f,
|
||||
layout.previewRect.y + 82.0f,
|
||||
320.0f,
|
||||
32.0f);
|
||||
layout.inspectorRect.x,
|
||||
layout.sectionRect.y + layout.sectionRect.height + 2.0f,
|
||||
layout.inspectorRect.width,
|
||||
22.0f);
|
||||
|
||||
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
|
||||
const float buttonY = layout.controlRect.y + 32.0f;
|
||||
@@ -150,34 +170,63 @@ ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||
return layout;
|
||||
}
|
||||
|
||||
XCEngine::UI::Editor::Widgets::UIEditorNumberFieldMetrics ResolveHostedNumberFieldMetrics(
|
||||
const Style::UITheme& theme) {
|
||||
const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(theme);
|
||||
const auto numberMetrics = XCEngine::UI::Editor::ResolveUIEditorNumberFieldMetrics(theme);
|
||||
return XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldMetrics(propertyMetrics, numberMetrics);
|
||||
}
|
||||
|
||||
XCEngine::UI::Editor::Widgets::UIEditorNumberFieldPalette ResolveHostedNumberFieldPalette(
|
||||
const Style::UITheme& theme) {
|
||||
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(theme);
|
||||
const auto numberPalette = XCEngine::UI::Editor::ResolveUIEditorNumberFieldPalette(theme);
|
||||
return XCEngine::UI::Editor::BuildUIEditorHostedNumberFieldPalette(propertyPalette, numberPalette);
|
||||
}
|
||||
|
||||
void DrawCard(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
std::string_view title,
|
||||
std::string_view subtitle = {}) {
|
||||
drawList.AddFilledRect(rect, kCardBg, 10.0f);
|
||||
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
|
||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
|
||||
drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius);
|
||||
drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
|
||||
std::string(title),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.titleFontSize);
|
||||
if (!subtitle.empty()) {
|
||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 40.0f),
|
||||
std::string(subtitle),
|
||||
shellPalette.textMuted,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawButton(
|
||||
UIDrawList& drawList,
|
||||
const ButtonLayout& button,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
bool hovered) {
|
||||
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
|
||||
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
|
||||
drawList.AddText(UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f);
|
||||
drawList.AddFilledRect(
|
||||
button.rect,
|
||||
hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground,
|
||||
shellMetrics.buttonRadius);
|
||||
drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
|
||||
button.label,
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
|
||||
std::string DescribeHitTarget(const UIEditorNumberFieldHitTarget& hitTarget) {
|
||||
switch (hitTarget.kind) {
|
||||
case UIEditorNumberFieldHitTargetKind::DecrementButton:
|
||||
return "decrement";
|
||||
case UIEditorNumberFieldHitTargetKind::IncrementButton:
|
||||
return "increment";
|
||||
case UIEditorNumberFieldHitTargetKind::ValueBox:
|
||||
return "value_box";
|
||||
case UIEditorNumberFieldHitTargetKind::Row:
|
||||
@@ -381,6 +430,14 @@ private:
|
||||
m_captureRoot =
|
||||
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/number_field_basic/captures";
|
||||
m_autoScreenshot.Initialize(m_captureRoot);
|
||||
const auto themeLoad =
|
||||
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
|
||||
if (themeLoad.succeeded) {
|
||||
m_theme = themeLoad.theme;
|
||||
m_themeStatus = "loaded";
|
||||
} else {
|
||||
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
|
||||
}
|
||||
|
||||
ResetScenario();
|
||||
return true;
|
||||
@@ -406,7 +463,10 @@ private:
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
return BuildScenarioLayout(width, height);
|
||||
return BuildScenarioLayout(
|
||||
width,
|
||||
height,
|
||||
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
|
||||
}
|
||||
|
||||
void ResetScenario() {
|
||||
@@ -434,11 +494,13 @@ private:
|
||||
}
|
||||
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = ResolveHostedNumberFieldMetrics(m_theme);
|
||||
m_frame = UpdateUIEditorNumberFieldInteraction(
|
||||
m_interactionState,
|
||||
m_spec,
|
||||
layout.fieldRect,
|
||||
{});
|
||||
{},
|
||||
metrics);
|
||||
}
|
||||
|
||||
void OnResize(UINT width, UINT height) {
|
||||
@@ -542,17 +604,19 @@ private:
|
||||
|
||||
UIEditorNumberFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = ResolveHostedNumberFieldMetrics(m_theme);
|
||||
m_frame = UpdateUIEditorNumberFieldInteraction(
|
||||
m_interactionState,
|
||||
m_spec,
|
||||
layout.fieldRect,
|
||||
std::move(events));
|
||||
std::move(events),
|
||||
metrics);
|
||||
return m_frame.result;
|
||||
}
|
||||
|
||||
void UpdateResultText(const UIEditorNumberFieldInteractionResult& result) {
|
||||
if (result.editCommitRejected) {
|
||||
m_lastResult = "提交失败,仍保留在编辑态";
|
||||
m_lastResult = "提交失败,仍保持在编辑态";
|
||||
return;
|
||||
}
|
||||
if (result.editCommitted) {
|
||||
@@ -600,83 +664,101 @@ private:
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
const ScenarioLayout layout = BuildScenarioLayout(width, height);
|
||||
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
|
||||
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
|
||||
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
|
||||
RefreshFrame();
|
||||
|
||||
const UIEditorNumberFieldHitTarget currentHit =
|
||||
HitTestUIEditorNumberField(m_frame.layout, m_mousePosition);
|
||||
const auto numberMetrics = ResolveHostedNumberFieldMetrics(m_theme);
|
||||
const auto numberPalette = ResolveHostedNumberFieldPalette(m_theme);
|
||||
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
|
||||
|
||||
UIDrawData drawData = {};
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("EditorNumberFieldBasic");
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.introRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"这个测试在验证什么功能",
|
||||
"只验证 Editor NumberField 的基础交互契约,不涉及 PropertyGrid 或任何业务 Inspector。");
|
||||
"验证 Inspector 宿主中的 NumberField 交互契约和默认宿主风格。");
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
|
||||
"1. 点击 +/- 按钮,检查步进和上下界钳制是否稳定。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
"1. 点击 value box,检查是否进入编辑态,外观应是 Unity 风格单输入框。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
||||
"2. 控件获得 focus 后,按 Left / Right / Up / Down / Home / End,检查键盘步进。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
"2. 获得 focus 后按 Left / Right / Up / Down / Home / End,检查键盘步进。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
|
||||
"3. 按 Enter 进入编辑态,直接输入字符,Enter 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Core/UIEditorPropertyGridInteraction.h>
|
||||
#include <XCEditor/Core/UIEditorTheme.h>
|
||||
#include <XCEditor/Widgets/UIEditorPropertyGrid.h>
|
||||
#include "EditorValidationTheme.h"
|
||||
#include "Host/AutoScreenshot.h"
|
||||
#include "Host/NativeRenderer.h"
|
||||
|
||||
@@ -48,24 +50,17 @@ using XCEngine::UI::Editor::UpdateUIEditorPropertyGridInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorPropertyGridBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorPropertyGridForeground;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorPropertyGrid;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorPropertyGridFieldValueText;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection;
|
||||
namespace Style = XCEngine::UI::Style;
|
||||
|
||||
constexpr const wchar_t* kWindowClassName = L"XCUIEditorPropertyGridBasicValidation";
|
||||
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | PropertyGrid Basic";
|
||||
|
||||
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
|
||||
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
|
||||
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f);
|
||||
constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f);
|
||||
constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f);
|
||||
constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f);
|
||||
constexpr UIColor kTextSuccess(0.63f, 0.76f, 0.63f, 1.0f);
|
||||
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
|
||||
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
|
||||
|
||||
enum class ActionId : unsigned char {
|
||||
Reset = 0,
|
||||
Capture
|
||||
@@ -95,6 +90,11 @@ std::filesystem::path ResolveRepoRootPath() {
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveValidationThemePath() {
|
||||
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||
return x >= rect.x &&
|
||||
x <= rect.x + rect.width &&
|
||||
@@ -129,10 +129,13 @@ std::int32_t MapEditorKey(UINT keyCode) {
|
||||
}
|
||||
}
|
||||
|
||||
ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||
constexpr float margin = 20.0f;
|
||||
ScenarioLayout BuildScenarioLayout(
|
||||
float width,
|
||||
float height,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
|
||||
const float margin = shellMetrics.margin;
|
||||
constexpr float leftWidth = 430.0f;
|
||||
constexpr float gap = 16.0f;
|
||||
const float gap = shellMetrics.gap;
|
||||
|
||||
ScenarioLayout layout = {};
|
||||
layout.introRect = UIRect(margin, margin, leftWidth, 214.0f);
|
||||
@@ -166,48 +169,109 @@ ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||
void DrawCard(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
std::string_view title,
|
||||
std::string_view subtitle = {}) {
|
||||
drawList.AddFilledRect(rect, kCardBg, 10.0f);
|
||||
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
|
||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
|
||||
drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius);
|
||||
drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
|
||||
std::string(title),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.titleFontSize);
|
||||
if (!subtitle.empty()) {
|
||||
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 40.0f),
|
||||
std::string(subtitle),
|
||||
shellPalette.textMuted,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawButton(
|
||||
UIDrawList& drawList,
|
||||
const ButtonLayout& button,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
bool hovered) {
|
||||
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
|
||||
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
|
||||
drawList.AddFilledRect(
|
||||
button.rect,
|
||||
hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground,
|
||||
shellMetrics.buttonRadius);
|
||||
drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
|
||||
button.label,
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeTextField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::string value,
|
||||
bool readOnly = false) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.valueText = std::move(value);
|
||||
field.readOnly = readOnly;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeBoolField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
bool value) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Bool;
|
||||
field.boolValue = value;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeNumberField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
double value) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Number;
|
||||
field.numberValue.value = value;
|
||||
field.numberValue.step = 1.0;
|
||||
field.numberValue.minValue = 0.0;
|
||||
field.numberValue.maxValue = 5000.0;
|
||||
field.numberValue.integerMode = true;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeEnumField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::vector<std::string> options,
|
||||
std::size_t selectedIndex) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Enum;
|
||||
field.enumValue.options = std::move(options);
|
||||
field.enumValue.selectedIndex = selectedIndex;
|
||||
return field;
|
||||
}
|
||||
|
||||
std::vector<UIEditorPropertyGridSection> BuildSections() {
|
||||
return {
|
||||
{
|
||||
"transform",
|
||||
"Transform",
|
||||
"inspector",
|
||||
"Inspector",
|
||||
{
|
||||
{ "position", "Position", "0, 0, 0", false, 0.0f },
|
||||
{ "rotation", "Rotation", "0, 45, 0", false, 0.0f },
|
||||
{ "scale", "Scale", "1, 1, 1", false, 0.0f }
|
||||
},
|
||||
0.0f
|
||||
},
|
||||
{
|
||||
"material",
|
||||
"Material",
|
||||
{
|
||||
{ "shader", "Shader", "Standard/Lit", false, 0.0f },
|
||||
{ "queue", "Render Queue", "2000", false, 0.0f },
|
||||
{ "guid", "GUID", "asset-guid-001", true, 0.0f }
|
||||
MakeBoolField("enabled", "Enabled", true),
|
||||
MakeNumberField("render_queue", "Render Queue", 2000.0),
|
||||
MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u),
|
||||
MakeTextField("tag", "Tag", "Player")
|
||||
},
|
||||
0.0f
|
||||
},
|
||||
@@ -215,8 +279,7 @@ std::vector<UIEditorPropertyGridSection> BuildSections() {
|
||||
"metadata",
|
||||
"Metadata",
|
||||
{
|
||||
{ "tag", "Tag", "", false, 0.0f },
|
||||
{ "layer", "Layer", "Default", false, 0.0f }
|
||||
MakeTextField("guid", "GUID", "asset-guid-001", true)
|
||||
},
|
||||
0.0f
|
||||
}
|
||||
@@ -246,8 +309,7 @@ std::string DescribeHitTarget(
|
||||
|
||||
std::string BuildExpandedSummary(const UIExpansionModel& expansionModel) {
|
||||
std::ostringstream stream = {};
|
||||
stream << (expansionModel.IsExpanded("transform") ? "Transform" : "-");
|
||||
stream << " / " << (expansionModel.IsExpanded("material") ? "Material" : "-");
|
||||
stream << (expansionModel.IsExpanded("inspector") ? "Inspector" : "-");
|
||||
stream << " / " << (expansionModel.IsExpanded("metadata") ? "Metadata" : "-");
|
||||
return stream.str();
|
||||
}
|
||||
@@ -467,6 +529,14 @@ private:
|
||||
m_captureRoot =
|
||||
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/property_grid_basic/captures";
|
||||
m_autoScreenshot.Initialize(m_captureRoot);
|
||||
const auto themeLoad =
|
||||
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
|
||||
if (themeLoad.succeeded) {
|
||||
m_theme = themeLoad.theme;
|
||||
m_themeStatus = "loaded";
|
||||
} else {
|
||||
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
|
||||
}
|
||||
|
||||
ResetScenario();
|
||||
return true;
|
||||
@@ -492,16 +562,18 @@ private:
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
return BuildScenarioLayout(width, height);
|
||||
return BuildScenarioLayout(
|
||||
width,
|
||||
height,
|
||||
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
|
||||
}
|
||||
|
||||
void ResetScenario() {
|
||||
m_sections = BuildSections();
|
||||
m_selectionModel = {};
|
||||
m_selectionModel.SetSelection("rotation");
|
||||
m_selectionModel.SetSelection("render_queue");
|
||||
m_expansionModel = {};
|
||||
m_expansionModel.Expand("transform");
|
||||
m_expansionModel.Expand("material");
|
||||
m_expansionModel.Expand("inspector");
|
||||
m_expansionModel.Expand("metadata");
|
||||
m_propertyEditModel = {};
|
||||
m_interactionState = {};
|
||||
@@ -520,6 +592,7 @@ private:
|
||||
}
|
||||
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme);
|
||||
m_gridFrame =
|
||||
UpdateUIEditorPropertyGridInteraction(
|
||||
m_interactionState,
|
||||
@@ -528,23 +601,16 @@ private:
|
||||
m_propertyEditModel,
|
||||
layout.gridRect,
|
||||
m_sections,
|
||||
{});
|
||||
{},
|
||||
metrics);
|
||||
}
|
||||
|
||||
void ApplyCommittedValue(const UIEditorPropertyGridInteractionResult& result) {
|
||||
if (!result.editCommitted || result.committedFieldId.empty()) {
|
||||
void UpdateLastCommit(const UIEditorPropertyGridInteractionResult& result) {
|
||||
if (!result.fieldValueChanged || result.changedFieldId.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (UIEditorPropertyGridSection& section : m_sections) {
|
||||
for (UIEditorPropertyGridField& field : section.fields) {
|
||||
if (field.fieldId == result.committedFieldId) {
|
||||
field.valueText = result.committedValue;
|
||||
m_lastCommit = result.committedFieldId + " = " + result.committedValue;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_lastCommit = result.changedFieldId + " = " + result.changedValue;
|
||||
}
|
||||
|
||||
void OnResize(UINT width, UINT height) {
|
||||
@@ -661,6 +727,7 @@ private:
|
||||
|
||||
UIEditorPropertyGridInteractionResult PumpGridEvents(std::vector<UIInputEvent> events) {
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme);
|
||||
m_gridFrame =
|
||||
UpdateUIEditorPropertyGridInteraction(
|
||||
m_interactionState,
|
||||
@@ -669,8 +736,9 @@ private:
|
||||
m_propertyEditModel,
|
||||
layout.gridRect,
|
||||
m_sections,
|
||||
std::move(events));
|
||||
ApplyCommittedValue(m_gridFrame.result);
|
||||
std::move(events),
|
||||
metrics);
|
||||
UpdateLastCommit(m_gridFrame.result);
|
||||
return m_gridFrame.result;
|
||||
}
|
||||
|
||||
@@ -678,6 +746,26 @@ private:
|
||||
const UIEditorPropertyGridInteractionResult& result,
|
||||
bool wasFocused,
|
||||
bool insideGrid) {
|
||||
if (result.popupOpened && !m_interactionState.propertyGridState.popupFieldId.empty()) {
|
||||
m_lastResult = "打开枚举下拉: " + m_interactionState.propertyGridState.popupFieldId;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.popupClosed && !result.fieldValueChanged) {
|
||||
m_lastResult = "关闭枚举下拉";
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.fieldValueChanged) {
|
||||
m_lastResult = "字段已更新: " + result.changedFieldId + " = " + result.changedValue;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.editCommitRejected && !result.activeFieldId.empty()) {
|
||||
m_lastResult = "提交被拒绝: " + result.activeFieldId;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.editCommitted) {
|
||||
m_lastResult = "提交字段: " + result.committedFieldId + " = " + result.committedValue;
|
||||
return;
|
||||
@@ -753,99 +841,124 @@ private:
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
const ScenarioLayout layout = BuildScenarioLayout(width, height);
|
||||
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
|
||||
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
|
||||
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
|
||||
RefreshGridFrame();
|
||||
|
||||
const UIEditorPropertyGridHitTarget currentHit =
|
||||
HitTestUIEditorPropertyGrid(m_gridFrame.layout, m_mousePosition);
|
||||
const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(m_theme);
|
||||
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
|
||||
const auto popupMetrics = XCEngine::UI::Editor::ResolveUIEditorMenuPopupMetrics(m_theme);
|
||||
const auto popupPalette = XCEngine::UI::Editor::ResolveUIEditorMenuPopupPalette(m_theme);
|
||||
|
||||
UIDrawData drawData = {};
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("EditorPropertyGridBasic");
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.introRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"这个测试在验证什么功能",
|
||||
"只验证 Editor PropertyGrid 基础控件,不涉及任何 Inspector 业务逻辑。");
|
||||
"只验证 Editor PropertyGrid 作为 typed 属性宿主的基础契约,不涉及任何 Inspector 业务逻辑。");
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
|
||||
"1. 点击 section header,检查展开/折叠是否稳定,字段布局不能歪。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
||||
"2. 点击 field row 只切换 selection;点击 value box 才进入编辑态。",
|
||||
kTextPrimary,
|
||||
12.0f);
|
||||
"2. 点击 value 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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
add_executable(editor_ui_text_field_basic_validation WIN32
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_include_directories(editor_ui_text_field_basic_validation PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
|
||||
${CMAKE_SOURCE_DIR}/new_editor/include
|
||||
${CMAKE_SOURCE_DIR}/new_editor/app
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
target_compile_definitions(editor_ui_text_field_basic_validation PRIVATE
|
||||
UNICODE
|
||||
_UNICODE
|
||||
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(editor_ui_text_field_basic_validation PRIVATE /utf-8 /FS)
|
||||
set_property(TARGET editor_ui_text_field_basic_validation PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
target_link_libraries(editor_ui_text_field_basic_validation PRIVATE
|
||||
XCUIEditorLib
|
||||
XCUIEditorHost
|
||||
)
|
||||
|
||||
set_target_properties(editor_ui_text_field_basic_validation PROPERTIES
|
||||
OUTPUT_NAME "XCUIEditorTextFieldBasicValidation"
|
||||
)
|
||||
842
tests/UI/Editor/integration/shell/text_field_basic/main.cpp
Normal file
842
tests/UI/Editor/integration/shell/text_field_basic/main.cpp
Normal file
@@ -0,0 +1,842 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Core/UIEditorTextFieldInteraction.h>
|
||||
#include <XCEditor/Core/UIEditorTheme.h>
|
||||
#include <XCEditor/Widgets/UIEditorTextField.h>
|
||||
#include "EditorValidationTheme.h"
|
||||
#include "Host/AutoScreenshot.h"
|
||||
#include "Host/NativeRenderer.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
|
||||
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIDrawData;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Host::AutoScreenshotController;
|
||||
using XCEngine::UI::Editor::Host::NativeRenderer;
|
||||
using XCEngine::UI::Editor::UIEditorTextFieldInteractionFrame;
|
||||
using XCEngine::UI::Editor::UIEditorTextFieldInteractionResult;
|
||||
using XCEngine::UI::Editor::UIEditorTextFieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorTextFieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorTextField;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec;
|
||||
namespace Style = XCEngine::UI::Style;
|
||||
|
||||
constexpr const wchar_t* kWindowClassName = L"XCUIEditorTextFieldBasicValidation";
|
||||
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | TextField Basic";
|
||||
|
||||
enum class ActionId : unsigned char {
|
||||
Reset = 0,
|
||||
Capture
|
||||
};
|
||||
|
||||
struct ButtonLayout {
|
||||
ActionId action = ActionId::Reset;
|
||||
const char* label = "";
|
||||
UIRect rect = {};
|
||||
};
|
||||
|
||||
struct ScenarioLayout {
|
||||
UIRect introRect = {};
|
||||
UIRect controlRect = {};
|
||||
UIRect stateRect = {};
|
||||
UIRect previewRect = {};
|
||||
UIRect inspectorRect = {};
|
||||
UIRect inspectorHeaderRect = {};
|
||||
UIRect sectionRect = {};
|
||||
UIRect fieldRect = {};
|
||||
std::vector<ButtonLayout> buttons = {};
|
||||
};
|
||||
|
||||
std::filesystem::path ResolveRepoRootPath() {
|
||||
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
|
||||
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||
root = root.substr(1u, root.size() - 2u);
|
||||
}
|
||||
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveValidationThemePath() {
|
||||
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||
return x >= rect.x &&
|
||||
x <= rect.x + rect.width &&
|
||||
y >= rect.y &&
|
||||
y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
std::int32_t MapTextFieldKey(UINT keyCode) {
|
||||
switch (keyCode) {
|
||||
case VK_RETURN:
|
||||
return static_cast<std::int32_t>(KeyCode::Enter);
|
||||
case VK_ESCAPE:
|
||||
return static_cast<std::int32_t>(KeyCode::Escape);
|
||||
default:
|
||||
return static_cast<std::int32_t>(KeyCode::None);
|
||||
}
|
||||
}
|
||||
|
||||
ScenarioLayout BuildScenarioLayout(
|
||||
float width,
|
||||
float height,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
|
||||
const float margin = shellMetrics.margin;
|
||||
constexpr float leftWidth = 460.0f;
|
||||
const float gap = shellMetrics.gap;
|
||||
|
||||
ScenarioLayout layout = {};
|
||||
layout.introRect = UIRect(margin, margin, leftWidth, 252.0f);
|
||||
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
|
||||
layout.stateRect = UIRect(
|
||||
margin,
|
||||
layout.controlRect.y + layout.controlRect.height + gap,
|
||||
leftWidth,
|
||||
(std::max)(244.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
|
||||
layout.previewRect = UIRect(
|
||||
leftWidth + margin * 2.0f,
|
||||
margin,
|
||||
(std::max)(420.0f, width - leftWidth - margin * 3.0f),
|
||||
height - margin * 2.0f);
|
||||
layout.inspectorRect = UIRect(
|
||||
layout.previewRect.x + 18.0f,
|
||||
layout.previewRect.y + 54.0f,
|
||||
(std::min)(392.0f, layout.previewRect.width - 36.0f),
|
||||
150.0f);
|
||||
layout.inspectorHeaderRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.inspectorRect.y,
|
||||
layout.inspectorRect.width,
|
||||
24.0f);
|
||||
layout.sectionRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.inspectorRect.y + layout.inspectorHeaderRect.height,
|
||||
layout.inspectorRect.width,
|
||||
24.0f);
|
||||
layout.fieldRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.sectionRect.y + layout.sectionRect.height + 2.0f,
|
||||
layout.inspectorRect.width,
|
||||
22.0f);
|
||||
|
||||
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
|
||||
const float buttonY = layout.controlRect.y + 32.0f;
|
||||
layout.buttons = {
|
||||
{ ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
|
||||
{ ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
|
||||
};
|
||||
return layout;
|
||||
}
|
||||
|
||||
XCEngine::UI::Editor::Widgets::UIEditorTextFieldMetrics ResolveHostedTextFieldMetrics(
|
||||
const Style::UITheme& theme) {
|
||||
const auto propertyMetrics = XCEngine::UI::Editor::ResolveUIEditorPropertyGridMetrics(theme);
|
||||
const auto textMetrics = XCEngine::UI::Editor::ResolveUIEditorTextFieldMetrics(theme);
|
||||
return XCEngine::UI::Editor::BuildUIEditorHostedTextFieldMetrics(propertyMetrics, textMetrics);
|
||||
}
|
||||
|
||||
XCEngine::UI::Editor::Widgets::UIEditorTextFieldPalette ResolveHostedTextFieldPalette(
|
||||
const Style::UITheme& theme) {
|
||||
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(theme);
|
||||
const auto textPalette = XCEngine::UI::Editor::ResolveUIEditorTextFieldPalette(theme);
|
||||
return XCEngine::UI::Editor::BuildUIEditorHostedTextFieldPalette(propertyPalette, textPalette);
|
||||
}
|
||||
|
||||
void DrawCard(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
std::string_view title,
|
||||
std::string_view subtitle = {}) {
|
||||
drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius);
|
||||
drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
|
||||
std::string(title),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.titleFontSize);
|
||||
if (!subtitle.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 40.0f),
|
||||
std::string(subtitle),
|
||||
shellPalette.textMuted,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawButton(
|
||||
UIDrawList& drawList,
|
||||
const ButtonLayout& button,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
bool hovered) {
|
||||
drawList.AddFilledRect(
|
||||
button.rect,
|
||||
hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground,
|
||||
shellMetrics.buttonRadius);
|
||||
drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
|
||||
button.label,
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
|
||||
std::string DescribeHitTarget(const UIEditorTextFieldHitTarget& hitTarget) {
|
||||
switch (hitTarget.kind) {
|
||||
case UIEditorTextFieldHitTargetKind::ValueBox:
|
||||
return "value_box";
|
||||
case UIEditorTextFieldHitTargetKind::Row:
|
||||
return "row";
|
||||
case UIEditorTextFieldHitTargetKind::None:
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
const UIPoint& position,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = position;
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyEvent(std::int32_t keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = keyCode;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacterEvent(wchar_t character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusEvent(UIInputEventType type) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
return event;
|
||||
}
|
||||
|
||||
class ScenarioApp {
|
||||
public:
|
||||
int Run(HINSTANCE hInstance, int nCmdShow) {
|
||||
if (!Initialize(hInstance, nCmdShow)) {
|
||||
Shutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
MSG message = {};
|
||||
while (message.message != WM_QUIT) {
|
||||
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderFrame();
|
||||
Sleep(8);
|
||||
}
|
||||
|
||||
Shutdown();
|
||||
return static_cast<int>(message.wParam);
|
||||
}
|
||||
|
||||
private:
|
||||
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
if (message == WM_NCCREATE) {
|
||||
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
switch (message) {
|
||||
case WM_SIZE:
|
||||
if (app != nullptr && wParam != SIZE_MINIMIZED) {
|
||||
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_MOUSEMOVE:
|
||||
if (app != nullptr) {
|
||||
app->HandleMouseMove(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_MOUSELEAVE:
|
||||
if (app != nullptr) {
|
||||
app->HandleMouseLeave();
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_LBUTTONDOWN:
|
||||
if (app != nullptr) {
|
||||
app->HandleLeftButtonDown(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_LBUTTONUP:
|
||||
if (app != nullptr) {
|
||||
app->HandleLeftButtonUp(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN:
|
||||
if (app != nullptr) {
|
||||
if (wParam == VK_F12) {
|
||||
app->m_autoScreenshot.RequestCapture("manual_f12");
|
||||
app->m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
if (wParam == VK_F6) {
|
||||
app->HandleFocusLost();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::int32_t keyCode = MapTextFieldKey(static_cast<UINT>(wParam));
|
||||
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
|
||||
app->HandleKeyDown(keyCode);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CHAR:
|
||||
if (app != nullptr) {
|
||||
app->HandleCharacter(static_cast<wchar_t>(wParam));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_PAINT:
|
||||
if (app != nullptr) {
|
||||
PAINTSTRUCT paintStruct = {};
|
||||
BeginPaint(hwnd, &paintStruct);
|
||||
app->RenderFrame();
|
||||
EndPaint(hwnd, &paintStruct);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
|
||||
case WM_DESTROY:
|
||||
if (app != nullptr) {
|
||||
app->m_hwnd = nullptr;
|
||||
}
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
WNDCLASSEXW windowClass = {};
|
||||
windowClass.cbSize = sizeof(windowClass);
|
||||
windowClass.style = CS_HREDRAW | CS_VREDRAW;
|
||||
windowClass.lpfnWndProc = &ScenarioApp::WndProc;
|
||||
windowClass.hInstance = hInstance;
|
||||
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
windowClass.lpszClassName = kWindowClassName;
|
||||
|
||||
m_windowClassAtom = RegisterClassExW(&windowClass);
|
||||
if (m_windowClassAtom == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_hwnd = CreateWindowExW(
|
||||
0,
|
||||
kWindowClassName,
|
||||
kWindowTitle,
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
1480,
|
||||
920,
|
||||
nullptr,
|
||||
nullptr,
|
||||
hInstance,
|
||||
this);
|
||||
if (m_hwnd == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ShowWindow(m_hwnd, nCmdShow);
|
||||
UpdateWindow(m_hwnd);
|
||||
|
||||
if (!m_renderer.Initialize(m_hwnd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_captureRoot =
|
||||
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/text_field_basic/captures";
|
||||
m_autoScreenshot.Initialize(m_captureRoot);
|
||||
const auto themeLoad =
|
||||
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
|
||||
if (themeLoad.succeeded) {
|
||||
m_theme = themeLoad.theme;
|
||||
m_themeStatus = "loaded";
|
||||
} else {
|
||||
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
|
||||
}
|
||||
|
||||
ResetScenario();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
m_autoScreenshot.Shutdown();
|
||||
m_renderer.Shutdown();
|
||||
|
||||
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
|
||||
DestroyWindow(m_hwnd);
|
||||
}
|
||||
m_hwnd = nullptr;
|
||||
|
||||
if (m_windowClassAtom != 0) {
|
||||
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
|
||||
m_windowClassAtom = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ScenarioLayout GetLayout() const {
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
return BuildScenarioLayout(
|
||||
width,
|
||||
height,
|
||||
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
|
||||
}
|
||||
|
||||
void ResetScenario() {
|
||||
m_spec = {};
|
||||
m_spec.fieldId = "game_object_name";
|
||||
m_spec.label = "Name";
|
||||
m_spec.value = "Player";
|
||||
m_spec.readOnly = false;
|
||||
m_interactionState = {};
|
||||
m_interactionState.textFieldState.focused = true;
|
||||
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
m_hoveredAction = ActionId::Reset;
|
||||
m_hasHoveredAction = false;
|
||||
m_lastResult = "已重置到默认 TextField 状态";
|
||||
RefreshFrame();
|
||||
}
|
||||
|
||||
void RefreshFrame() {
|
||||
if (m_hwnd == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = ResolveHostedTextFieldMetrics(m_theme);
|
||||
m_frame = UpdateUIEditorTextFieldInteraction(
|
||||
m_interactionState,
|
||||
m_spec,
|
||||
layout.fieldRect,
|
||||
{},
|
||||
metrics);
|
||||
}
|
||||
|
||||
void OnResize(UINT width, UINT height) {
|
||||
if (width == 0u || height == 0u) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_renderer.Resize(width, height);
|
||||
RefreshFrame();
|
||||
}
|
||||
|
||||
void HandleMouseMove(float x, float y) {
|
||||
m_mousePosition = UIPoint(x, y);
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
UpdateHoveredAction(layout, x, y);
|
||||
|
||||
TRACKMOUSEEVENT trackEvent = {};
|
||||
trackEvent.cbSize = sizeof(trackEvent);
|
||||
trackEvent.dwFlags = TME_LEAVE;
|
||||
trackEvent.hwndTrack = m_hwnd;
|
||||
TrackMouseEvent(&trackEvent);
|
||||
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleMouseLeave() {
|
||||
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
m_hasHoveredAction = false;
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleLeftButtonDown(float x, float y) {
|
||||
m_mousePosition = UIPoint(x, y);
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
if (HitTestAction(layout, x, y) != nullptr) {
|
||||
UpdateHoveredAction(layout, x, y);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
const UIEditorTextFieldInteractionResult result =
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleLeftButtonUp(float x, float y) {
|
||||
m_mousePosition = UIPoint(x, y);
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||
if (button != nullptr) {
|
||||
ExecuteAction(button->action);
|
||||
UpdateHoveredAction(layout, x, y);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
const UIEditorTextFieldInteractionResult result =
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleKeyDown(std::int32_t keyCode) {
|
||||
const UIEditorTextFieldInteractionResult result = PumpEvents({ MakeKeyEvent(keyCode) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleCharacter(wchar_t character) {
|
||||
if (character < 32) {
|
||||
return;
|
||||
}
|
||||
|
||||
const UIEditorTextFieldInteractionResult result = PumpEvents({ MakeCharacterEvent(character) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleFocusLost() {
|
||||
const UIEditorTextFieldInteractionResult result =
|
||||
PumpEvents({ MakeFocusEvent(UIInputEventType::FocusLost) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
|
||||
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||
if (button == nullptr) {
|
||||
m_hasHoveredAction = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_hoveredAction = button->action;
|
||||
m_hasHoveredAction = true;
|
||||
}
|
||||
|
||||
const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const {
|
||||
for (const ButtonLayout& button : layout.buttons) {
|
||||
if (ContainsPoint(button.rect, x, y)) {
|
||||
return &button;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorTextFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = ResolveHostedTextFieldMetrics(m_theme);
|
||||
m_frame = UpdateUIEditorTextFieldInteraction(
|
||||
m_interactionState,
|
||||
m_spec,
|
||||
layout.fieldRect,
|
||||
std::move(events),
|
||||
metrics);
|
||||
return m_frame.result;
|
||||
}
|
||||
|
||||
void UpdateResultText(const UIEditorTextFieldInteractionResult& result) {
|
||||
if (result.editCommitted) {
|
||||
m_lastResult = std::string("已提交文本: ") + result.committedText;
|
||||
return;
|
||||
}
|
||||
if (result.editCanceled) {
|
||||
m_lastResult = "已取消编辑";
|
||||
return;
|
||||
}
|
||||
if (result.editStarted) {
|
||||
m_lastResult = "已进入编辑态";
|
||||
return;
|
||||
}
|
||||
if (result.focusChanged) {
|
||||
m_lastResult = std::string("焦点变化: ") + (m_interactionState.textFieldState.focused ? "focused" : "lost");
|
||||
return;
|
||||
}
|
||||
if (result.consumed) {
|
||||
m_lastResult = "控件已消费输入";
|
||||
return;
|
||||
}
|
||||
m_lastResult = "等待交互";
|
||||
}
|
||||
|
||||
void ExecuteAction(ActionId action) {
|
||||
switch (action) {
|
||||
case ActionId::Reset:
|
||||
ResetScenario();
|
||||
break;
|
||||
|
||||
case ActionId::Capture:
|
||||
m_autoScreenshot.RequestCapture("manual_button");
|
||||
m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderFrame() {
|
||||
if (m_hwnd == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
|
||||
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
|
||||
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
|
||||
RefreshFrame();
|
||||
|
||||
const UIEditorTextFieldHitTarget currentHit =
|
||||
HitTestUIEditorTextField(m_frame.layout, m_mousePosition);
|
||||
const auto textMetrics = ResolveHostedTextFieldMetrics(m_theme);
|
||||
const auto textPalette = ResolveHostedTextFieldPalette(m_theme);
|
||||
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
|
||||
|
||||
UIDrawData drawData = {};
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("EditorTextFieldBasic");
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.introRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"这个测试验证什么功能",
|
||||
"验证 UIEditorTextField 的基础编辑交互契约,不涉及 PropertyGrid 或业务 Inspector。");
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
|
||||
"1. 点击 value box,检查是否进入编辑态。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
||||
"2. 获得 focus 后按 Enter 开始编辑;直接输入字符也应开始编辑。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
|
||||
"3. 编辑态下按 Enter commit,按 Escape cancel。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
|
||||
"4. 编辑态下按 F6 模拟 FocusLost,应提交暂存文本并退出编辑态。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
|
||||
"5. 检查 Hover / Focus / Editing / Value / Result 是否同步更新。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 182.0f),
|
||||
"6. 按 F12 或点击截图按钮,确认自动截图路径正确。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
|
||||
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作");
|
||||
for (const ButtonLayout& button : layout.buttons) {
|
||||
DrawButton(
|
||||
drawList,
|
||||
button,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
m_hasHoveredAction && m_hoveredAction == button.action);
|
||||
}
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.stateRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"状态摘要",
|
||||
"重点检查 hit / focus / editing / value / display / result。");
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
|
||||
"Hover: " + DescribeHitTarget(currentHit),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
|
||||
std::string("Focused: ") + (m_interactionState.textFieldState.focused ? "是" : "否"),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
|
||||
std::string("Editing: ") + (m_interactionState.textFieldState.editing ? "是" : "否"),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
|
||||
"Value: " + m_spec.value,
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
|
||||
"Display: " + m_interactionState.textFieldState.displayText,
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
|
||||
"Result: " + m_lastResult,
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
|
||||
const std::string captureSummary =
|
||||
m_autoScreenshot.HasPendingCapture()
|
||||
? "截图排队中..."
|
||||
: (m_autoScreenshot.GetLastCaptureSummary().empty()
|
||||
? std::string("F12 -> tests/UI/Editor/integration/shell/text_field_basic/captures/")
|
||||
: m_autoScreenshot.GetLastCaptureSummary());
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
|
||||
captureSummary,
|
||||
shellPalette.textWeak,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 238.0f),
|
||||
"Theme: " + m_themeStatus,
|
||||
shellPalette.textWeak,
|
||||
shellMetrics.bodyFontSize);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.previewRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"TextField 预览",
|
||||
"这里只放一个 Editor TextField,用来验证基础字段行为。");
|
||||
drawList.AddFilledRect(layout.inspectorRect, propertyPalette.surfaceColor);
|
||||
drawList.AddRectOutline(layout.inspectorRect, propertyPalette.borderColor, 1.0f);
|
||||
drawList.AddFilledRect(layout.inspectorHeaderRect, shellPalette.cardBackground);
|
||||
drawList.AddRectOutline(layout.inspectorHeaderRect, propertyPalette.borderColor, 1.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.inspectorHeaderRect.x + 10.0f, layout.inspectorHeaderRect.y + 5.0f),
|
||||
"Inspector",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddFilledRect(layout.sectionRect, propertyPalette.sectionHeaderColor);
|
||||
drawList.AddRectOutline(layout.sectionRect, propertyPalette.borderColor, 1.0f);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.sectionRect.x + 10.0f, layout.sectionRect.y + 5.0f),
|
||||
"v Transform",
|
||||
propertyPalette.sectionTextColor,
|
||||
shellMetrics.bodyFontSize);
|
||||
AppendUIEditorTextField(
|
||||
drawList,
|
||||
layout.fieldRect,
|
||||
m_spec,
|
||||
m_interactionState.textFieldState,
|
||||
textPalette,
|
||||
textMetrics);
|
||||
|
||||
const bool framePresented = m_renderer.Render(drawData);
|
||||
m_autoScreenshot.CaptureIfRequested(
|
||||
m_renderer,
|
||||
drawData,
|
||||
static_cast<unsigned int>(width),
|
||||
static_cast<unsigned int>(height),
|
||||
framePresented);
|
||||
}
|
||||
|
||||
HWND m_hwnd = nullptr;
|
||||
ATOM m_windowClassAtom = 0;
|
||||
NativeRenderer m_renderer = {};
|
||||
AutoScreenshotController m_autoScreenshot = {};
|
||||
std::filesystem::path m_captureRoot = {};
|
||||
UIEditorTextFieldSpec m_spec = {};
|
||||
UIEditorTextFieldInteractionState m_interactionState = {};
|
||||
UIEditorTextFieldInteractionFrame m_frame = {};
|
||||
Style::UITheme m_theme = {};
|
||||
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
ActionId m_hoveredAction = ActionId::Reset;
|
||||
bool m_hasHoveredAction = false;
|
||||
std::string m_lastResult = {};
|
||||
std::string m_themeStatus = "fallback";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||
return ScenarioApp().Run(hInstance, nCmdShow);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
add_executable(editor_ui_vector2_field_basic_validation WIN32
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_include_directories(editor_ui_vector2_field_basic_validation PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
|
||||
${CMAKE_SOURCE_DIR}/new_editor/include
|
||||
${CMAKE_SOURCE_DIR}/new_editor/app
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
target_compile_definitions(editor_ui_vector2_field_basic_validation PRIVATE
|
||||
UNICODE
|
||||
_UNICODE
|
||||
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(editor_ui_vector2_field_basic_validation PRIVATE /utf-8 /FS)
|
||||
set_property(TARGET editor_ui_vector2_field_basic_validation PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
target_link_libraries(editor_ui_vector2_field_basic_validation PRIVATE
|
||||
XCUIEditorLib
|
||||
XCUIEditorHost
|
||||
)
|
||||
|
||||
set_target_properties(editor_ui_vector2_field_basic_validation PROPERTIES
|
||||
OUTPUT_NAME "XCUIEditorVector2FieldBasicValidation"
|
||||
)
|
||||
896
tests/UI/Editor/integration/shell/vector2_field_basic/main.cpp
Normal file
896
tests/UI/Editor/integration/shell/vector2_field_basic/main.cpp
Normal file
@@ -0,0 +1,896 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Core/UIEditorTheme.h>
|
||||
#include <XCEditor/Core/UIEditorVector2FieldInteraction.h>
|
||||
#include <XCEditor/Widgets/UIEditorVector2Field.h>
|
||||
#include "EditorValidationTheme.h"
|
||||
#include "Host/AutoScreenshot.h"
|
||||
#include "Host/NativeRenderer.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
|
||||
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIDrawData;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Host::AutoScreenshotController;
|
||||
using XCEngine::UI::Editor::Host::NativeRenderer;
|
||||
using XCEngine::UI::Editor::UIEditorVector2FieldInteractionFrame;
|
||||
using XCEngine::UI::Editor::UIEditorVector2FieldInteractionResult;
|
||||
using XCEngine::UI::Editor::UIEditorVector2FieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorVector2FieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorVector2Field;
|
||||
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector2FieldComponentValue;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector2Field;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldInvalidComponentIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec;
|
||||
namespace Style = XCEngine::UI::Style;
|
||||
|
||||
constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector2FieldBasicValidation";
|
||||
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector2Field Basic";
|
||||
|
||||
enum class ActionId : unsigned char {
|
||||
Reset = 0,
|
||||
Capture
|
||||
};
|
||||
|
||||
struct ButtonLayout {
|
||||
ActionId action = ActionId::Reset;
|
||||
const char* label = "";
|
||||
UIRect rect = {};
|
||||
};
|
||||
|
||||
struct ScenarioLayout {
|
||||
UIRect introRect = {};
|
||||
UIRect controlRect = {};
|
||||
UIRect stateRect = {};
|
||||
UIRect previewRect = {};
|
||||
UIRect inspectorRect = {};
|
||||
UIRect inspectorHeaderRect = {};
|
||||
UIRect sectionRect = {};
|
||||
UIRect fieldRect = {};
|
||||
std::vector<ButtonLayout> buttons = {};
|
||||
};
|
||||
|
||||
std::filesystem::path ResolveRepoRootPath() {
|
||||
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
|
||||
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||
root = root.substr(1u, root.size() - 2u);
|
||||
}
|
||||
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveValidationThemePath() {
|
||||
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||
return x >= rect.x &&
|
||||
x <= rect.x + rect.width &&
|
||||
y >= rect.y &&
|
||||
y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
std::int32_t MapVector2FieldKey(UINT keyCode) {
|
||||
switch (keyCode) {
|
||||
case VK_LEFT:
|
||||
return static_cast<std::int32_t>(KeyCode::Left);
|
||||
case VK_RIGHT:
|
||||
return static_cast<std::int32_t>(KeyCode::Right);
|
||||
case VK_UP:
|
||||
return static_cast<std::int32_t>(KeyCode::Up);
|
||||
case VK_DOWN:
|
||||
return static_cast<std::int32_t>(KeyCode::Down);
|
||||
case VK_HOME:
|
||||
return static_cast<std::int32_t>(KeyCode::Home);
|
||||
case VK_END:
|
||||
return static_cast<std::int32_t>(KeyCode::End);
|
||||
case VK_TAB:
|
||||
return static_cast<std::int32_t>(KeyCode::Tab);
|
||||
case VK_RETURN:
|
||||
return static_cast<std::int32_t>(KeyCode::Enter);
|
||||
case VK_ESCAPE:
|
||||
return static_cast<std::int32_t>(KeyCode::Escape);
|
||||
default:
|
||||
return static_cast<std::int32_t>(KeyCode::None);
|
||||
}
|
||||
}
|
||||
|
||||
ScenarioLayout BuildScenarioLayout(
|
||||
float width,
|
||||
float height,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
|
||||
const float margin = shellMetrics.margin;
|
||||
constexpr float leftWidth = 470.0f;
|
||||
const float gap = shellMetrics.gap;
|
||||
|
||||
ScenarioLayout layout = {};
|
||||
layout.introRect = UIRect(margin, margin, leftWidth, 272.0f);
|
||||
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
|
||||
layout.stateRect = UIRect(
|
||||
margin,
|
||||
layout.controlRect.y + layout.controlRect.height + gap,
|
||||
leftWidth,
|
||||
(std::max)(240.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
|
||||
layout.previewRect = UIRect(
|
||||
leftWidth + margin * 2.0f,
|
||||
margin,
|
||||
(std::max)(420.0f, width - leftWidth - margin * 3.0f),
|
||||
height - margin * 2.0f);
|
||||
layout.inspectorRect = UIRect(
|
||||
layout.previewRect.x + 18.0f,
|
||||
layout.previewRect.y + 54.0f,
|
||||
(std::min)(392.0f, layout.previewRect.width - 36.0f),
|
||||
172.0f);
|
||||
layout.inspectorHeaderRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.inspectorRect.y,
|
||||
layout.inspectorRect.width,
|
||||
24.0f);
|
||||
layout.sectionRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.inspectorRect.y + layout.inspectorHeaderRect.height,
|
||||
layout.inspectorRect.width,
|
||||
24.0f);
|
||||
layout.fieldRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.sectionRect.y + layout.sectionRect.height + 2.0f,
|
||||
layout.inspectorRect.width,
|
||||
22.0f);
|
||||
|
||||
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
|
||||
const float buttonY = layout.controlRect.y + 32.0f;
|
||||
layout.buttons = {
|
||||
{ ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
|
||||
{ ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
|
||||
};
|
||||
return layout;
|
||||
}
|
||||
|
||||
void DrawCard(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
std::string_view title,
|
||||
std::string_view subtitle = {}) {
|
||||
drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius);
|
||||
drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
|
||||
std::string(title),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.titleFontSize);
|
||||
if (!subtitle.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 40.0f),
|
||||
std::string(subtitle),
|
||||
shellPalette.textMuted,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawButton(
|
||||
UIDrawList& drawList,
|
||||
const ButtonLayout& button,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
bool hovered) {
|
||||
drawList.AddFilledRect(
|
||||
button.rect,
|
||||
hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground,
|
||||
shellMetrics.buttonRadius);
|
||||
drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
|
||||
button.label,
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
|
||||
std::string DescribeHitTarget(const UIEditorVector2FieldHitTarget& hitTarget) {
|
||||
switch (hitTarget.kind) {
|
||||
case UIEditorVector2FieldHitTargetKind::Component:
|
||||
return std::string("component_") + std::to_string(hitTarget.componentIndex);
|
||||
case UIEditorVector2FieldHitTargetKind::Row:
|
||||
return "row";
|
||||
case UIEditorVector2FieldHitTargetKind::None:
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
std::string DescribeSelectedComponent(std::size_t componentIndex) {
|
||||
if (componentIndex == UIEditorVector2FieldInvalidComponentIndex) {
|
||||
return "none";
|
||||
}
|
||||
return componentIndex == 0u ? "X" : "Y";
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
const UIPoint& position,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = position;
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyEvent(std::int32_t keyCode, bool shift = false) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = keyCode;
|
||||
event.modifiers.shift = shift;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacterEvent(wchar_t character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusEvent(UIInputEventType type) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
return event;
|
||||
}
|
||||
|
||||
class ScenarioApp {
|
||||
public:
|
||||
int Run(HINSTANCE hInstance, int nCmdShow) {
|
||||
if (!Initialize(hInstance, nCmdShow)) {
|
||||
Shutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
MSG message = {};
|
||||
while (message.message != WM_QUIT) {
|
||||
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderFrame();
|
||||
Sleep(8);
|
||||
}
|
||||
|
||||
Shutdown();
|
||||
return static_cast<int>(message.wParam);
|
||||
}
|
||||
|
||||
private:
|
||||
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
if (message == WM_NCCREATE) {
|
||||
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
switch (message) {
|
||||
case WM_SIZE:
|
||||
if (app != nullptr && wParam != SIZE_MINIMIZED) {
|
||||
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_MOUSEMOVE:
|
||||
if (app != nullptr) {
|
||||
app->HandleMouseMove(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_MOUSELEAVE:
|
||||
if (app != nullptr) {
|
||||
app->HandleMouseLeave();
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_LBUTTONDOWN:
|
||||
if (app != nullptr) {
|
||||
app->HandleLeftButtonDown(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_LBUTTONUP:
|
||||
if (app != nullptr) {
|
||||
app->HandleLeftButtonUp(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN:
|
||||
if (app != nullptr) {
|
||||
if (wParam == VK_F12) {
|
||||
app->m_autoScreenshot.RequestCapture("manual_f12");
|
||||
app->m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
if (wParam == VK_F6) {
|
||||
app->HandleFocusLost();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::int32_t keyCode = MapVector2FieldKey(static_cast<UINT>(wParam));
|
||||
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
|
||||
app->HandleKeyDown(keyCode, (GetKeyState(VK_SHIFT) & 0x8000) != 0);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CHAR:
|
||||
if (app != nullptr) {
|
||||
app->HandleCharacter(static_cast<wchar_t>(wParam));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_PAINT:
|
||||
if (app != nullptr) {
|
||||
PAINTSTRUCT paintStruct = {};
|
||||
BeginPaint(hwnd, &paintStruct);
|
||||
app->RenderFrame();
|
||||
EndPaint(hwnd, &paintStruct);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
|
||||
case WM_DESTROY:
|
||||
if (app != nullptr) {
|
||||
app->m_hwnd = nullptr;
|
||||
}
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
WNDCLASSEXW windowClass = {};
|
||||
windowClass.cbSize = sizeof(windowClass);
|
||||
windowClass.style = CS_HREDRAW | CS_VREDRAW;
|
||||
windowClass.lpfnWndProc = &ScenarioApp::WndProc;
|
||||
windowClass.hInstance = hInstance;
|
||||
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
windowClass.lpszClassName = kWindowClassName;
|
||||
|
||||
m_windowClassAtom = RegisterClassExW(&windowClass);
|
||||
if (m_windowClassAtom == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_hwnd = CreateWindowExW(
|
||||
0,
|
||||
kWindowClassName,
|
||||
kWindowTitle,
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
1520,
|
||||
920,
|
||||
nullptr,
|
||||
nullptr,
|
||||
hInstance,
|
||||
this);
|
||||
if (m_hwnd == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ShowWindow(m_hwnd, nCmdShow);
|
||||
UpdateWindow(m_hwnd);
|
||||
|
||||
if (!m_renderer.Initialize(m_hwnd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_captureRoot =
|
||||
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/vector2_field_basic/captures";
|
||||
m_autoScreenshot.Initialize(m_captureRoot);
|
||||
const auto themeLoad =
|
||||
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
|
||||
if (themeLoad.succeeded) {
|
||||
m_theme = themeLoad.theme;
|
||||
m_themeStatus = "loaded";
|
||||
} else {
|
||||
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
|
||||
}
|
||||
|
||||
ResetScenario();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
m_autoScreenshot.Shutdown();
|
||||
m_renderer.Shutdown();
|
||||
|
||||
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
|
||||
DestroyWindow(m_hwnd);
|
||||
}
|
||||
m_hwnd = nullptr;
|
||||
|
||||
if (m_windowClassAtom != 0) {
|
||||
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
|
||||
m_windowClassAtom = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ScenarioLayout GetLayout() const {
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
return BuildScenarioLayout(
|
||||
width,
|
||||
height,
|
||||
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
|
||||
}
|
||||
|
||||
void ResetScenario() {
|
||||
m_spec = {};
|
||||
m_spec.fieldId = "position";
|
||||
m_spec.label = "Position";
|
||||
m_spec.values = { 1.25, -2.5 };
|
||||
m_spec.componentLabels = { std::string("X"), std::string("Y") };
|
||||
m_spec.step = 0.25;
|
||||
m_spec.minValue = -10.0;
|
||||
m_spec.maxValue = 10.0;
|
||||
m_spec.integerMode = false;
|
||||
m_spec.readOnly = false;
|
||||
m_interactionState = {};
|
||||
m_interactionState.vector2FieldState.focused = true;
|
||||
m_interactionState.vector2FieldState.selectedComponentIndex = 0u;
|
||||
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
m_hoveredAction = ActionId::Reset;
|
||||
m_hasHoveredAction = false;
|
||||
m_lastResult = "已重置到默认 Vector2Field 状态";
|
||||
RefreshFrame();
|
||||
}
|
||||
|
||||
void RefreshFrame() {
|
||||
if (m_hwnd == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme);
|
||||
m_frame = UpdateUIEditorVector2FieldInteraction(
|
||||
m_interactionState,
|
||||
m_spec,
|
||||
layout.fieldRect,
|
||||
{},
|
||||
metrics);
|
||||
}
|
||||
|
||||
void OnResize(UINT width, UINT height) {
|
||||
if (width == 0u || height == 0u) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_renderer.Resize(width, height);
|
||||
RefreshFrame();
|
||||
}
|
||||
|
||||
void HandleMouseMove(float x, float y) {
|
||||
m_mousePosition = UIPoint(x, y);
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
UpdateHoveredAction(layout, x, y);
|
||||
|
||||
TRACKMOUSEEVENT trackEvent = {};
|
||||
trackEvent.cbSize = sizeof(trackEvent);
|
||||
trackEvent.dwFlags = TME_LEAVE;
|
||||
trackEvent.hwndTrack = m_hwnd;
|
||||
TrackMouseEvent(&trackEvent);
|
||||
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleMouseLeave() {
|
||||
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
m_hasHoveredAction = false;
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleLeftButtonDown(float x, float y) {
|
||||
m_mousePosition = UIPoint(x, y);
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
if (HitTestAction(layout, x, y) != nullptr) {
|
||||
UpdateHoveredAction(layout, x, y);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
const UIEditorVector2FieldInteractionResult result =
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleLeftButtonUp(float x, float y) {
|
||||
m_mousePosition = UIPoint(x, y);
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||
if (button != nullptr) {
|
||||
ExecuteAction(button->action);
|
||||
UpdateHoveredAction(layout, x, y);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
const UIEditorVector2FieldInteractionResult result =
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleKeyDown(std::int32_t keyCode, bool shift) {
|
||||
const UIEditorVector2FieldInteractionResult result =
|
||||
PumpEvents({ MakeKeyEvent(keyCode, shift) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleCharacter(wchar_t character) {
|
||||
if (character < 32) {
|
||||
return;
|
||||
}
|
||||
|
||||
const UIEditorVector2FieldInteractionResult result =
|
||||
PumpEvents({ MakeCharacterEvent(character) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleFocusLost() {
|
||||
const UIEditorVector2FieldInteractionResult result =
|
||||
PumpEvents({ MakeFocusEvent(UIInputEventType::FocusLost) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
|
||||
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||
if (button == nullptr) {
|
||||
m_hasHoveredAction = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_hoveredAction = button->action;
|
||||
m_hasHoveredAction = true;
|
||||
}
|
||||
|
||||
const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const {
|
||||
for (const ButtonLayout& button : layout.buttons) {
|
||||
if (ContainsPoint(button.rect, x, y)) {
|
||||
return &button;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorVector2FieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme);
|
||||
m_frame = UpdateUIEditorVector2FieldInteraction(
|
||||
m_interactionState,
|
||||
m_spec,
|
||||
layout.fieldRect,
|
||||
std::move(events),
|
||||
metrics);
|
||||
return m_frame.result;
|
||||
}
|
||||
|
||||
void UpdateResultText(const UIEditorVector2FieldInteractionResult& result) {
|
||||
if (result.editCommitRejected) {
|
||||
m_lastResult = "提交失败,当前文本不是合法数字";
|
||||
return;
|
||||
}
|
||||
if (result.editCommitted) {
|
||||
m_lastResult =
|
||||
std::string("已提交 ") +
|
||||
DescribeSelectedComponent(result.changedComponentIndex) +
|
||||
" = " + result.committedText;
|
||||
return;
|
||||
}
|
||||
if (result.editCanceled) {
|
||||
m_lastResult = "已取消编辑";
|
||||
return;
|
||||
}
|
||||
if (result.editStarted) {
|
||||
m_lastResult =
|
||||
std::string("开始编辑 component ") +
|
||||
DescribeSelectedComponent(result.selectedComponentIndex);
|
||||
return;
|
||||
}
|
||||
if (result.stepApplied || result.valueChanged) {
|
||||
m_lastResult =
|
||||
std::string("数值已更新,当前 component = ") +
|
||||
DescribeSelectedComponent(result.changedComponentIndex);
|
||||
return;
|
||||
}
|
||||
if (result.selectionChanged) {
|
||||
m_lastResult =
|
||||
std::string("已切换选中 component: ") +
|
||||
DescribeSelectedComponent(result.selectedComponentIndex);
|
||||
return;
|
||||
}
|
||||
if (result.focusChanged) {
|
||||
m_lastResult =
|
||||
std::string("焦点变化: ") +
|
||||
(m_interactionState.vector2FieldState.focused ? "focused" : "lost");
|
||||
return;
|
||||
}
|
||||
if (result.consumed) {
|
||||
m_lastResult = "控件已消费输入";
|
||||
return;
|
||||
}
|
||||
m_lastResult = "等待交互";
|
||||
}
|
||||
|
||||
void ExecuteAction(ActionId action) {
|
||||
switch (action) {
|
||||
case ActionId::Reset:
|
||||
ResetScenario();
|
||||
break;
|
||||
|
||||
case ActionId::Capture:
|
||||
m_autoScreenshot.RequestCapture("manual_button");
|
||||
m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderFrame() {
|
||||
if (m_hwnd == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
|
||||
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
|
||||
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
|
||||
RefreshFrame();
|
||||
|
||||
const UIEditorVector2FieldHitTarget currentHit =
|
||||
HitTestUIEditorVector2Field(m_frame.layout, m_mousePosition);
|
||||
const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector2FieldMetrics(m_theme);
|
||||
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector2FieldPalette(m_theme);
|
||||
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
|
||||
|
||||
UIDrawData drawData = {};
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector2FieldBasic");
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.introRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"这个测试验证什么功能",
|
||||
"验证 UIEditorVector2Field 的双通道数值编辑契约,不涉及 PropertyGrid 或业务 Inspector。");
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
|
||||
"1. 点击 X / Y 对应的 value box,检查 selected component 是否切换,并且应进入编辑态。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
||||
"2. 获得 focus 后按 Tab,检查 selected component 在 X / Y 之间切换。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
|
||||
"3. 非编辑态按 Up / Down / Home / End,检查当前 component 的 step / 边界行为。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
|
||||
"4. 按 Enter 开始编辑;直接输入字符也应开始编辑;Enter 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<unsigned int>(width),
|
||||
static_cast<unsigned int>(height),
|
||||
framePresented);
|
||||
}
|
||||
|
||||
HWND m_hwnd = nullptr;
|
||||
ATOM m_windowClassAtom = 0;
|
||||
NativeRenderer m_renderer = {};
|
||||
AutoScreenshotController m_autoScreenshot = {};
|
||||
std::filesystem::path m_captureRoot = {};
|
||||
UIEditorVector2FieldSpec m_spec = {};
|
||||
UIEditorVector2FieldInteractionState m_interactionState = {};
|
||||
UIEditorVector2FieldInteractionFrame m_frame = {};
|
||||
Style::UITheme m_theme = {};
|
||||
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
ActionId m_hoveredAction = ActionId::Reset;
|
||||
bool m_hasHoveredAction = false;
|
||||
std::string m_lastResult = "等待交互";
|
||||
std::string m_themeStatus = "fallback";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
|
||||
ScenarioApp app = {};
|
||||
return app.Run(hInstance, nCmdShow);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
add_executable(editor_ui_vector3_field_basic_validation WIN32
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_include_directories(editor_ui_vector3_field_basic_validation PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
|
||||
${CMAKE_SOURCE_DIR}/new_editor/include
|
||||
${CMAKE_SOURCE_DIR}/new_editor/app
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
target_compile_definitions(editor_ui_vector3_field_basic_validation PRIVATE
|
||||
UNICODE
|
||||
_UNICODE
|
||||
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(editor_ui_vector3_field_basic_validation PRIVATE /utf-8 /FS)
|
||||
set_property(TARGET editor_ui_vector3_field_basic_validation PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
target_link_libraries(editor_ui_vector3_field_basic_validation PRIVATE
|
||||
XCUIEditorLib
|
||||
XCUIEditorHost
|
||||
)
|
||||
|
||||
set_target_properties(editor_ui_vector3_field_basic_validation PROPERTIES
|
||||
OUTPUT_NAME "XCUIEditorVector3FieldBasicValidation"
|
||||
)
|
||||
904
tests/UI/Editor/integration/shell/vector3_field_basic/main.cpp
Normal file
904
tests/UI/Editor/integration/shell/vector3_field_basic/main.cpp
Normal file
@@ -0,0 +1,904 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <XCEditor/Core/UIEditorTheme.h>
|
||||
#include <XCEditor/Core/UIEditorVector3FieldInteraction.h>
|
||||
#include <XCEditor/Widgets/UIEditorVector3Field.h>
|
||||
#include "EditorValidationTheme.h"
|
||||
#include "Host/AutoScreenshot.h"
|
||||
#include "Host/NativeRenderer.h"
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
|
||||
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIDrawData;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Host::AutoScreenshotController;
|
||||
using XCEngine::UI::Editor::Host::NativeRenderer;
|
||||
using XCEngine::UI::Editor::UIEditorVector3FieldInteractionFrame;
|
||||
using XCEngine::UI::Editor::UIEditorVector3FieldInteractionResult;
|
||||
using XCEngine::UI::Editor::UIEditorVector3FieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorVector3FieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorVector3Field;
|
||||
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector3FieldComponentValue;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector3Field;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTarget;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldInvalidComponentIndex;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec;
|
||||
namespace Style = XCEngine::UI::Style;
|
||||
|
||||
constexpr const wchar_t* kWindowClassName = L"XCUIEditorVector3FieldBasicValidation";
|
||||
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | Vector3Field Basic";
|
||||
|
||||
enum class ActionId : unsigned char {
|
||||
Reset = 0,
|
||||
Capture
|
||||
};
|
||||
|
||||
struct ButtonLayout {
|
||||
ActionId action = ActionId::Reset;
|
||||
const char* label = "";
|
||||
UIRect rect = {};
|
||||
};
|
||||
|
||||
struct ScenarioLayout {
|
||||
UIRect introRect = {};
|
||||
UIRect controlRect = {};
|
||||
UIRect stateRect = {};
|
||||
UIRect previewRect = {};
|
||||
UIRect inspectorRect = {};
|
||||
UIRect inspectorHeaderRect = {};
|
||||
UIRect sectionRect = {};
|
||||
UIRect fieldRect = {};
|
||||
std::vector<ButtonLayout> buttons = {};
|
||||
};
|
||||
|
||||
std::filesystem::path ResolveRepoRootPath() {
|
||||
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
|
||||
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||
root = root.substr(1u, root.size() - 2u);
|
||||
}
|
||||
|
||||
return std::filesystem::path(root).lexically_normal();
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveValidationThemePath() {
|
||||
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
|
||||
.lexically_normal();
|
||||
}
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||
return x >= rect.x &&
|
||||
x <= rect.x + rect.width &&
|
||||
y >= rect.y &&
|
||||
y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
std::int32_t MapVector3FieldKey(UINT keyCode) {
|
||||
switch (keyCode) {
|
||||
case VK_LEFT:
|
||||
return static_cast<std::int32_t>(KeyCode::Left);
|
||||
case VK_RIGHT:
|
||||
return static_cast<std::int32_t>(KeyCode::Right);
|
||||
case VK_UP:
|
||||
return static_cast<std::int32_t>(KeyCode::Up);
|
||||
case VK_DOWN:
|
||||
return static_cast<std::int32_t>(KeyCode::Down);
|
||||
case VK_HOME:
|
||||
return static_cast<std::int32_t>(KeyCode::Home);
|
||||
case VK_END:
|
||||
return static_cast<std::int32_t>(KeyCode::End);
|
||||
case VK_TAB:
|
||||
return static_cast<std::int32_t>(KeyCode::Tab);
|
||||
case VK_RETURN:
|
||||
return static_cast<std::int32_t>(KeyCode::Enter);
|
||||
case VK_ESCAPE:
|
||||
return static_cast<std::int32_t>(KeyCode::Escape);
|
||||
default:
|
||||
return static_cast<std::int32_t>(KeyCode::None);
|
||||
}
|
||||
}
|
||||
|
||||
ScenarioLayout BuildScenarioLayout(
|
||||
float width,
|
||||
float height,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
|
||||
const float margin = shellMetrics.margin;
|
||||
constexpr float leftWidth = 470.0f;
|
||||
const float gap = shellMetrics.gap;
|
||||
|
||||
ScenarioLayout layout = {};
|
||||
layout.introRect = UIRect(margin, margin, leftWidth, 272.0f);
|
||||
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
|
||||
layout.stateRect = UIRect(
|
||||
margin,
|
||||
layout.controlRect.y + layout.controlRect.height + gap,
|
||||
leftWidth,
|
||||
(std::max)(240.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
|
||||
layout.previewRect = UIRect(
|
||||
leftWidth + margin * 2.0f,
|
||||
margin,
|
||||
(std::max)(420.0f, width - leftWidth - margin * 3.0f),
|
||||
height - margin * 2.0f);
|
||||
layout.inspectorRect = UIRect(
|
||||
layout.previewRect.x + 18.0f,
|
||||
layout.previewRect.y + 54.0f,
|
||||
(std::min)(392.0f, layout.previewRect.width - 36.0f),
|
||||
172.0f);
|
||||
layout.inspectorHeaderRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.inspectorRect.y,
|
||||
layout.inspectorRect.width,
|
||||
24.0f);
|
||||
layout.sectionRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.inspectorRect.y + layout.inspectorHeaderRect.height,
|
||||
layout.inspectorRect.width,
|
||||
24.0f);
|
||||
layout.fieldRect = UIRect(
|
||||
layout.inspectorRect.x,
|
||||
layout.sectionRect.y + layout.sectionRect.height + 2.0f,
|
||||
layout.inspectorRect.width,
|
||||
22.0f);
|
||||
|
||||
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
|
||||
const float buttonY = layout.controlRect.y + 32.0f;
|
||||
layout.buttons = {
|
||||
{ ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
|
||||
{ ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
|
||||
};
|
||||
return layout;
|
||||
}
|
||||
|
||||
void DrawCard(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& rect,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
std::string_view title,
|
||||
std::string_view subtitle = {}) {
|
||||
drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius);
|
||||
drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
|
||||
std::string(title),
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.titleFontSize);
|
||||
if (!subtitle.empty()) {
|
||||
drawList.AddText(
|
||||
UIPoint(rect.x + 16.0f, rect.y + 40.0f),
|
||||
std::string(subtitle),
|
||||
shellPalette.textMuted,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawButton(
|
||||
UIDrawList& drawList,
|
||||
const ButtonLayout& button,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
|
||||
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
|
||||
bool hovered) {
|
||||
drawList.AddFilledRect(
|
||||
button.rect,
|
||||
hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground,
|
||||
shellMetrics.buttonRadius);
|
||||
drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius);
|
||||
drawList.AddText(
|
||||
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
|
||||
button.label,
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
}
|
||||
|
||||
std::string DescribeHitTarget(const UIEditorVector3FieldHitTarget& hitTarget) {
|
||||
switch (hitTarget.kind) {
|
||||
case UIEditorVector3FieldHitTargetKind::Component:
|
||||
return std::string("component_") + std::to_string(hitTarget.componentIndex);
|
||||
case UIEditorVector3FieldHitTargetKind::Row:
|
||||
return "row";
|
||||
case UIEditorVector3FieldHitTargetKind::None:
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
std::string DescribeSelectedComponent(std::size_t componentIndex) {
|
||||
switch (componentIndex) {
|
||||
case 0u:
|
||||
return "X";
|
||||
case 1u:
|
||||
return "Y";
|
||||
case 2u:
|
||||
return "Z";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
const UIPoint& position,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = position;
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKeyEvent(std::int32_t keyCode, bool shift = false) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = keyCode;
|
||||
event.modifiers.shift = shift;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacterEvent(wchar_t character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeFocusEvent(UIInputEventType type) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
return event;
|
||||
}
|
||||
|
||||
class ScenarioApp {
|
||||
public:
|
||||
int Run(HINSTANCE hInstance, int nCmdShow) {
|
||||
if (!Initialize(hInstance, nCmdShow)) {
|
||||
Shutdown();
|
||||
return 1;
|
||||
}
|
||||
|
||||
MSG message = {};
|
||||
while (message.message != WM_QUIT) {
|
||||
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderFrame();
|
||||
Sleep(8);
|
||||
}
|
||||
|
||||
Shutdown();
|
||||
return static_cast<int>(message.wParam);
|
||||
}
|
||||
|
||||
private:
|
||||
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
if (message == WM_NCCREATE) {
|
||||
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
switch (message) {
|
||||
case WM_SIZE:
|
||||
if (app != nullptr && wParam != SIZE_MINIMIZED) {
|
||||
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_MOUSEMOVE:
|
||||
if (app != nullptr) {
|
||||
app->HandleMouseMove(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_MOUSELEAVE:
|
||||
if (app != nullptr) {
|
||||
app->HandleMouseLeave();
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_LBUTTONDOWN:
|
||||
if (app != nullptr) {
|
||||
app->HandleLeftButtonDown(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_LBUTTONUP:
|
||||
if (app != nullptr) {
|
||||
app->HandleLeftButtonUp(
|
||||
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_KEYDOWN:
|
||||
case WM_SYSKEYDOWN:
|
||||
if (app != nullptr) {
|
||||
if (wParam == VK_F12) {
|
||||
app->m_autoScreenshot.RequestCapture("manual_f12");
|
||||
app->m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||
InvalidateRect(hwnd, nullptr, FALSE);
|
||||
return 0;
|
||||
}
|
||||
if (wParam == VK_F6) {
|
||||
app->HandleFocusLost();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::int32_t keyCode = MapVector3FieldKey(static_cast<UINT>(wParam));
|
||||
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
|
||||
app->HandleKeyDown(keyCode, (GetKeyState(VK_SHIFT) & 0x8000) != 0);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_CHAR:
|
||||
if (app != nullptr) {
|
||||
app->HandleCharacter(static_cast<wchar_t>(wParam));
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_PAINT:
|
||||
if (app != nullptr) {
|
||||
PAINTSTRUCT paintStruct = {};
|
||||
BeginPaint(hwnd, &paintStruct);
|
||||
app->RenderFrame();
|
||||
EndPaint(hwnd, &paintStruct);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
|
||||
case WM_DESTROY:
|
||||
if (app != nullptr) {
|
||||
app->m_hwnd = nullptr;
|
||||
}
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||
WNDCLASSEXW windowClass = {};
|
||||
windowClass.cbSize = sizeof(windowClass);
|
||||
windowClass.style = CS_HREDRAW | CS_VREDRAW;
|
||||
windowClass.lpfnWndProc = &ScenarioApp::WndProc;
|
||||
windowClass.hInstance = hInstance;
|
||||
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
windowClass.lpszClassName = kWindowClassName;
|
||||
|
||||
m_windowClassAtom = RegisterClassExW(&windowClass);
|
||||
if (m_windowClassAtom == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_hwnd = CreateWindowExW(
|
||||
0,
|
||||
kWindowClassName,
|
||||
kWindowTitle,
|
||||
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
1520,
|
||||
920,
|
||||
nullptr,
|
||||
nullptr,
|
||||
hInstance,
|
||||
this);
|
||||
if (m_hwnd == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ShowWindow(m_hwnd, nCmdShow);
|
||||
UpdateWindow(m_hwnd);
|
||||
|
||||
if (!m_renderer.Initialize(m_hwnd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_captureRoot =
|
||||
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/vector3_field_basic/captures";
|
||||
m_autoScreenshot.Initialize(m_captureRoot);
|
||||
const auto themeLoad =
|
||||
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
|
||||
if (themeLoad.succeeded) {
|
||||
m_theme = themeLoad.theme;
|
||||
m_themeStatus = "loaded";
|
||||
} else {
|
||||
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
|
||||
}
|
||||
|
||||
ResetScenario();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
m_autoScreenshot.Shutdown();
|
||||
m_renderer.Shutdown();
|
||||
|
||||
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
|
||||
DestroyWindow(m_hwnd);
|
||||
}
|
||||
m_hwnd = nullptr;
|
||||
|
||||
if (m_windowClassAtom != 0) {
|
||||
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
|
||||
m_windowClassAtom = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ScenarioLayout GetLayout() const {
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
return BuildScenarioLayout(
|
||||
width,
|
||||
height,
|
||||
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
|
||||
}
|
||||
|
||||
void ResetScenario() {
|
||||
m_spec = {};
|
||||
m_spec.fieldId = "position";
|
||||
m_spec.label = "Position";
|
||||
m_spec.values = { 1.25, -2.5, 4.75 };
|
||||
m_spec.componentLabels = { std::string("X"), std::string("Y"), std::string("Z") };
|
||||
m_spec.step = 0.25;
|
||||
m_spec.minValue = -10.0;
|
||||
m_spec.maxValue = 10.0;
|
||||
m_spec.integerMode = false;
|
||||
m_spec.readOnly = false;
|
||||
m_interactionState = {};
|
||||
m_interactionState.vector3FieldState.focused = true;
|
||||
m_interactionState.vector3FieldState.selectedComponentIndex = 0u;
|
||||
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
m_hoveredAction = ActionId::Reset;
|
||||
m_hasHoveredAction = false;
|
||||
m_lastResult = "已重置到默认 Vector3Field 状态";
|
||||
RefreshFrame();
|
||||
}
|
||||
|
||||
void RefreshFrame() {
|
||||
if (m_hwnd == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme);
|
||||
m_frame = UpdateUIEditorVector3FieldInteraction(
|
||||
m_interactionState,
|
||||
m_spec,
|
||||
layout.fieldRect,
|
||||
{},
|
||||
metrics);
|
||||
}
|
||||
|
||||
void OnResize(UINT width, UINT height) {
|
||||
if (width == 0u || height == 0u) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_renderer.Resize(width, height);
|
||||
RefreshFrame();
|
||||
}
|
||||
|
||||
void HandleMouseMove(float x, float y) {
|
||||
m_mousePosition = UIPoint(x, y);
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
UpdateHoveredAction(layout, x, y);
|
||||
|
||||
TRACKMOUSEEVENT trackEvent = {};
|
||||
trackEvent.cbSize = sizeof(trackEvent);
|
||||
trackEvent.dwFlags = TME_LEAVE;
|
||||
trackEvent.hwndTrack = m_hwnd;
|
||||
TrackMouseEvent(&trackEvent);
|
||||
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleMouseLeave() {
|
||||
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
m_hasHoveredAction = false;
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleLeftButtonDown(float x, float y) {
|
||||
m_mousePosition = UIPoint(x, y);
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
if (HitTestAction(layout, x, y) != nullptr) {
|
||||
UpdateHoveredAction(layout, x, y);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
const UIEditorVector3FieldInteractionResult result =
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleLeftButtonUp(float x, float y) {
|
||||
m_mousePosition = UIPoint(x, y);
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||
if (button != nullptr) {
|
||||
ExecuteAction(button->action);
|
||||
UpdateHoveredAction(layout, x, y);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
const UIEditorVector3FieldInteractionResult result =
|
||||
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleKeyDown(std::int32_t keyCode, bool shift) {
|
||||
const UIEditorVector3FieldInteractionResult result =
|
||||
PumpEvents({ MakeKeyEvent(keyCode, shift) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleCharacter(wchar_t character) {
|
||||
if (character < 32) {
|
||||
return;
|
||||
}
|
||||
|
||||
const UIEditorVector3FieldInteractionResult result =
|
||||
PumpEvents({ MakeCharacterEvent(character) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void HandleFocusLost() {
|
||||
const UIEditorVector3FieldInteractionResult result =
|
||||
PumpEvents({ MakeFocusEvent(UIInputEventType::FocusLost) });
|
||||
UpdateResultText(result);
|
||||
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
|
||||
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||
if (button == nullptr) {
|
||||
m_hasHoveredAction = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_hoveredAction = button->action;
|
||||
m_hasHoveredAction = true;
|
||||
}
|
||||
|
||||
const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const {
|
||||
for (const ButtonLayout& button : layout.buttons) {
|
||||
if (ContainsPoint(button.rect, x, y)) {
|
||||
return &button;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UIEditorVector3FieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
|
||||
const ScenarioLayout layout = GetLayout();
|
||||
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme);
|
||||
m_frame = UpdateUIEditorVector3FieldInteraction(
|
||||
m_interactionState,
|
||||
m_spec,
|
||||
layout.fieldRect,
|
||||
std::move(events),
|
||||
metrics);
|
||||
return m_frame.result;
|
||||
}
|
||||
|
||||
void UpdateResultText(const UIEditorVector3FieldInteractionResult& result) {
|
||||
if (result.editCommitRejected) {
|
||||
m_lastResult = "提交失败,当前文本不是合法数字";
|
||||
return;
|
||||
}
|
||||
if (result.editCommitted) {
|
||||
m_lastResult =
|
||||
std::string("已提交 ") +
|
||||
DescribeSelectedComponent(result.changedComponentIndex) +
|
||||
" = " + result.committedText;
|
||||
return;
|
||||
}
|
||||
if (result.editCanceled) {
|
||||
m_lastResult = "已取消编辑";
|
||||
return;
|
||||
}
|
||||
if (result.editStarted) {
|
||||
m_lastResult =
|
||||
std::string("开始编辑 component ") +
|
||||
DescribeSelectedComponent(result.selectedComponentIndex);
|
||||
return;
|
||||
}
|
||||
if (result.stepApplied || result.valueChanged) {
|
||||
m_lastResult =
|
||||
std::string("数值已更新,当前 component = ") +
|
||||
DescribeSelectedComponent(result.changedComponentIndex);
|
||||
return;
|
||||
}
|
||||
if (result.selectionChanged) {
|
||||
m_lastResult =
|
||||
std::string("已切换选中 component: ") +
|
||||
DescribeSelectedComponent(result.selectedComponentIndex);
|
||||
return;
|
||||
}
|
||||
if (result.focusChanged) {
|
||||
m_lastResult =
|
||||
std::string("焦点变化: ") +
|
||||
(m_interactionState.vector3FieldState.focused ? "focused" : "lost");
|
||||
return;
|
||||
}
|
||||
if (result.consumed) {
|
||||
m_lastResult = "控件已消费输入";
|
||||
return;
|
||||
}
|
||||
m_lastResult = "等待交互";
|
||||
}
|
||||
|
||||
void ExecuteAction(ActionId action) {
|
||||
switch (action) {
|
||||
case ActionId::Reset:
|
||||
ResetScenario();
|
||||
break;
|
||||
|
||||
case ActionId::Capture:
|
||||
m_autoScreenshot.RequestCapture("manual_button");
|
||||
m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderFrame() {
|
||||
if (m_hwnd == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
RECT clientRect = {};
|
||||
GetClientRect(m_hwnd, &clientRect);
|
||||
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
|
||||
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
|
||||
const ScenarioLayout layout = BuildScenarioLayout(width, height, shellMetrics);
|
||||
RefreshFrame();
|
||||
|
||||
const UIEditorVector3FieldHitTarget currentHit =
|
||||
HitTestUIEditorVector3Field(m_frame.layout, m_mousePosition);
|
||||
const auto vectorMetrics = XCEngine::UI::Editor::ResolveUIEditorVector3FieldMetrics(m_theme);
|
||||
const auto vectorPalette = XCEngine::UI::Editor::ResolveUIEditorVector3FieldPalette(m_theme);
|
||||
const auto propertyPalette = XCEngine::UI::Editor::ResolveUIEditorPropertyGridPalette(m_theme);
|
||||
|
||||
UIDrawData drawData = {};
|
||||
UIDrawList& drawList = drawData.EmplaceDrawList("EditorVector3FieldBasic");
|
||||
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), shellPalette.windowBackground);
|
||||
|
||||
DrawCard(
|
||||
drawList,
|
||||
layout.introRect,
|
||||
shellPalette,
|
||||
shellMetrics,
|
||||
"这个测试验证什么功能",
|
||||
"验证 UIEditorVector3Field 的三通道数值编辑契约,不涉及 PropertyGrid 或业务 Inspector。");
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
|
||||
"1. 点击 X / Y / Z 对应的 value box,检查 selected component 是否切换,并应进入编辑态。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
||||
"2. 获得 focus 后按 Tab,检查 selected component 在 X / Y / Z 之间切换。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
|
||||
"3. 非编辑态按 Up / Down / Home / End,检查当前 component 的 step / 边界行为。",
|
||||
shellPalette.textPrimary,
|
||||
shellMetrics.bodyFontSize);
|
||||
drawList.AddText(
|
||||
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
|
||||
"4. 按 Enter 开始编辑;直接输入字符也应开始编辑;Enter 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<unsigned int>(width),
|
||||
static_cast<unsigned int>(height),
|
||||
framePresented);
|
||||
}
|
||||
|
||||
HWND m_hwnd = nullptr;
|
||||
ATOM m_windowClassAtom = 0;
|
||||
NativeRenderer m_renderer = {};
|
||||
AutoScreenshotController m_autoScreenshot = {};
|
||||
std::filesystem::path m_captureRoot = {};
|
||||
UIEditorVector3FieldSpec m_spec = {};
|
||||
UIEditorVector3FieldInteractionState m_interactionState = {};
|
||||
UIEditorVector3FieldInteractionFrame m_frame = {};
|
||||
Style::UITheme m_theme = {};
|
||||
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||
ActionId m_hoveredAction = ActionId::Reset;
|
||||
bool m_hasHoveredAction = false;
|
||||
std::string m_lastResult = "等待交互";
|
||||
std::string m_themeStatus = "fallback";
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
|
||||
ScenarioApp app = {};
|
||||
return app.Run(hInstance, nCmdShow);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Widgets/UIEditorBoolField.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIColor;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolFieldBackground;
|
||||
@@ -16,39 +19,54 @@ using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldState;
|
||||
|
||||
TEST(UIEditorBoolFieldTest, LayoutBuildsLabelAndToggleRects) {
|
||||
UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false };
|
||||
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
|
||||
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.toggleRect.width, 42.0f);
|
||||
EXPECT_GT(layout.knobRect.x, layout.toggleRect.x);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.x, 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.checkboxRect.width, 18.0f);
|
||||
EXPECT_FLOAT_EQ(layout.checkmarkRect.width, layout.checkboxRect.width);
|
||||
EXPECT_FLOAT_EQ(layout.checkboxRect.y, 2.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorBoolFieldTest, HitTestResolvesToggleAndRow) {
|
||||
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
|
||||
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
|
||||
const auto toggleHit = HitTestUIEditorBoolField(
|
||||
const auto checkboxHit = HitTestUIEditorBoolField(
|
||||
layout,
|
||||
UIPoint(layout.toggleRect.x + 4.0f, layout.toggleRect.y + 4.0f));
|
||||
EXPECT_EQ(toggleHit.kind, UIEditorBoolFieldHitTargetKind::Toggle);
|
||||
UIPoint(layout.controlRect.x + layout.controlRect.width - 2.0f, layout.controlRect.y + 2.0f));
|
||||
EXPECT_EQ(checkboxHit.kind, UIEditorBoolFieldHitTargetKind::Checkbox);
|
||||
|
||||
const auto rowHit = HitTestUIEditorBoolField(layout, UIPoint(20.0f, 16.0f));
|
||||
const auto rowHit = HitTestUIEditorBoolField(layout, UIPoint(20.0f, 11.0f));
|
||||
EXPECT_EQ(rowHit.kind, UIEditorBoolFieldHitTargetKind::Row);
|
||||
}
|
||||
|
||||
TEST(UIEditorBoolFieldTest, BackgroundAndForegroundEmitStableCommands) {
|
||||
TEST(UIEditorBoolFieldTest, BackgroundAndForegroundEmitCheckboxOnlyChromeAndCenteredText) {
|
||||
UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false };
|
||||
UIEditorBoolFieldState state = {};
|
||||
state.focused = true;
|
||||
state.hoveredTarget = UIEditorBoolFieldHitTargetKind::Toggle;
|
||||
state.hoveredTarget = UIEditorBoolFieldHitTargetKind::Checkbox;
|
||||
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("BoolField");
|
||||
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
AppendUIEditorBoolFieldBackground(drawList, layout, spec, state);
|
||||
AppendUIEditorBoolFieldForeground(drawList, layout, spec);
|
||||
|
||||
ASSERT_GE(drawList.GetCommands().size(), 6u);
|
||||
const auto& commands = drawList.GetCommands();
|
||||
ASSERT_EQ(commands.size(), 6u);
|
||||
EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(commands[0].rect.x, layout.checkboxRect.x);
|
||||
EXPECT_EQ(commands[0].rect.y, layout.checkboxRect.y);
|
||||
EXPECT_EQ(commands[1].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(commands[1].color.r, 0.14f);
|
||||
EXPECT_EQ(commands[2].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(commands[3].type, UIDrawCommandType::Text);
|
||||
EXPECT_FLOAT_EQ(commands[3].position.y, 2.0f);
|
||||
EXPECT_EQ(commands[4].type, UIDrawCommandType::PopClipRect);
|
||||
EXPECT_EQ(commands[5].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(commands[5].text, "V");
|
||||
EXPECT_FLOAT_EQ(commands[5].position.y, 2.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,35 +1,77 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumFieldBackground;
|
||||
using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumFieldForeground;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorEnumFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorEnumField;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorEnumFieldValueText;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldState;
|
||||
|
||||
TEST(UIEditorEnumFieldTest, ValueTextUsesSelectedOption) {
|
||||
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||
EXPECT_EQ(ResolveUIEditorEnumFieldValueText(spec), "Cutout");
|
||||
}
|
||||
|
||||
TEST(UIEditorEnumFieldTest, HitTestResolvesPreviousNextAndValueBox) {
|
||||
TEST(UIEditorEnumFieldTest, LayoutKeepsInspectorControlColumnAndUnityArrowWidth) {
|
||||
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.x, 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.valueRect.y, 1.0f);
|
||||
EXPECT_FLOAT_EQ(layout.valueRect.height, 20.0f);
|
||||
EXPECT_FLOAT_EQ(layout.arrowRect.width, 20.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorEnumFieldTest, HitTestResolvesArrowAndValueBox) {
|
||||
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorEnumField(layout, UIPoint(layout.previousRect.x + 2.0f, layout.previousRect.y + 2.0f)).kind,
|
||||
UIEditorEnumFieldHitTargetKind::PreviousButton);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorEnumField(layout, UIPoint(layout.nextRect.x + 2.0f, layout.nextRect.y + 2.0f)).kind,
|
||||
UIEditorEnumFieldHitTargetKind::NextButton);
|
||||
HitTestUIEditorEnumField(layout, UIPoint(layout.arrowRect.x + 2.0f, layout.arrowRect.y + 2.0f)).kind,
|
||||
UIEditorEnumFieldHitTargetKind::DropdownArrow);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorEnumField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
|
||||
UIEditorEnumFieldHitTargetKind::ValueBox);
|
||||
}
|
||||
|
||||
TEST(UIEditorEnumFieldTest, BackgroundAndForegroundEmitControlOnlyChromeAndCenteredText) {
|
||||
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||
UIEditorEnumFieldState state = {};
|
||||
state.popupOpen = true;
|
||||
state.hoveredTarget = UIEditorEnumFieldHitTargetKind::DropdownArrow;
|
||||
|
||||
XCEngine::UI::UIDrawData drawData = {};
|
||||
auto& drawList = drawData.EmplaceDrawList("EnumField");
|
||||
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 22.0f), spec);
|
||||
AppendUIEditorEnumFieldBackground(drawList, layout, spec, state);
|
||||
AppendUIEditorEnumFieldForeground(drawList, layout, spec);
|
||||
|
||||
const auto& commands = drawList.GetCommands();
|
||||
ASSERT_EQ(commands.size(), 9u);
|
||||
EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(commands[0].rect.x, layout.valueRect.x);
|
||||
EXPECT_EQ(commands[1].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(commands[2].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(commands[3].type, UIDrawCommandType::Text);
|
||||
EXPECT_FLOAT_EQ(commands[3].position.y, 2.0f);
|
||||
EXPECT_EQ(commands[4].type, UIDrawCommandType::PopClipRect);
|
||||
EXPECT_EQ(commands[5].type, UIDrawCommandType::PushClipRect);
|
||||
EXPECT_EQ(commands[6].type, UIDrawCommandType::Text);
|
||||
EXPECT_FLOAT_EQ(commands[6].position.y, 1.0f);
|
||||
EXPECT_EQ(commands[7].type, UIDrawCommandType::PopClipRect);
|
||||
EXPECT_EQ(commands[8].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(commands[8].text, "V");
|
||||
EXPECT_FLOAT_EQ(commands[8].position.y, 2.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
52
tests/UI/Editor/unit/test_ui_editor_field_row_layout.cpp
Normal file
52
tests/UI/Editor/unit/test_ui_editor_field_row_layout.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorFieldRowLayout.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorFieldRowLayout;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorFieldRowLayoutMetrics;
|
||||
|
||||
TEST(UIEditorFieldRowLayoutTest, WideRowsKeepStableControlColumnAnchor) {
|
||||
UIEditorFieldRowLayoutMetrics metrics = {};
|
||||
const auto layout = BuildUIEditorFieldRowLayout(
|
||||
UIRect(10.0f, 20.0f, 392.0f, 22.0f),
|
||||
96.0f,
|
||||
metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.bounds.height, 22.0f);
|
||||
EXPECT_FLOAT_EQ(layout.labelRect.x, 22.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.x, 246.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.y, 21.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.height, 20.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorFieldRowLayoutTest, NarrowRowsCompressFromRightWithoutMagicRatioFallback) {
|
||||
UIEditorFieldRowLayoutMetrics metrics = {};
|
||||
const auto layout = BuildUIEditorFieldRowLayout(
|
||||
UIRect(10.0f, 20.0f, 280.0f, 22.0f),
|
||||
96.0f,
|
||||
metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.x, 186.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.width, 96.0f);
|
||||
EXPECT_FLOAT_EQ(layout.labelRect.width, 144.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorFieldRowLayoutTest, ZeroHeightFallsBackToMetricRowHeight) {
|
||||
UIEditorFieldRowLayoutMetrics metrics = {};
|
||||
metrics.rowHeight = 24.0f;
|
||||
metrics.controlInsetY = 2.0f;
|
||||
|
||||
const auto layout = BuildUIEditorFieldRowLayout(
|
||||
UIRect(0.0f, 0.0f, 360.0f, 0.0f),
|
||||
120.0f,
|
||||
metrics);
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.bounds.height, 24.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.y, 2.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.height, 20.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -16,7 +16,9 @@ using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridFieldLocation;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridSectionIndex;
|
||||
using XCEngine::UI::Editor::Widgets::FindUIEditorPropertyGridVisibleFieldIndex;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorPropertyGrid;
|
||||
using XCEngine::UI::Editor::Widgets::ResolveUIEditorPropertyGridFieldValueText;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridState;
|
||||
@@ -36,23 +38,80 @@ bool ContainsTextCommand(
|
||||
return false;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeTextField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::string value,
|
||||
bool readOnly = false) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.valueText = std::move(value);
|
||||
field.readOnly = readOnly;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeBoolField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
bool value) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Bool;
|
||||
field.boolValue = value;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeNumberField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
double value,
|
||||
bool integerMode = true) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Number;
|
||||
field.numberValue.value = value;
|
||||
field.numberValue.step = 1.0;
|
||||
field.numberValue.minValue = 0.0;
|
||||
field.numberValue.maxValue = 5000.0;
|
||||
field.numberValue.integerMode = integerMode;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeEnumField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::vector<std::string> options,
|
||||
std::size_t selectedIndex) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Enum;
|
||||
field.enumValue.options = std::move(options);
|
||||
field.enumValue.selectedIndex = selectedIndex;
|
||||
return field;
|
||||
}
|
||||
|
||||
std::vector<UIEditorPropertyGridSection> BuildSections() {
|
||||
return {
|
||||
{
|
||||
"transform",
|
||||
"Transform",
|
||||
"inspector",
|
||||
"Inspector",
|
||||
{
|
||||
{ "position", "Position", "0, 0, 0", false, 0.0f },
|
||||
{ "rotation", "Rotation", "0, 45, 0", false, 0.0f }
|
||||
MakeBoolField("enabled", "Enabled", true),
|
||||
MakeNumberField("render_queue", "Render Queue", 2000.0),
|
||||
MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u),
|
||||
MakeTextField("tag", "Tag", "Player")
|
||||
},
|
||||
0.0f
|
||||
},
|
||||
{
|
||||
"rendering",
|
||||
"Rendering",
|
||||
"metadata",
|
||||
"Metadata",
|
||||
{
|
||||
{ "material", "Material", "Metal", false, 0.0f },
|
||||
{ "guid", "GUID", "asset-guid-001", true, 0.0f }
|
||||
MakeTextField("guid", "GUID", "asset-guid-001", true)
|
||||
},
|
||||
0.0f
|
||||
}
|
||||
@@ -68,55 +127,64 @@ UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
|
||||
TEST(UIEditorPropertyGridTest, FindSectionAndFieldLocationReturnStableIndices) {
|
||||
const auto sections = BuildSections();
|
||||
|
||||
EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "transform"), 0u);
|
||||
EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "rendering"), 1u);
|
||||
EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "inspector"), 0u);
|
||||
EXPECT_EQ(FindUIEditorPropertyGridSectionIndex(sections, "metadata"), 1u);
|
||||
EXPECT_EQ(
|
||||
FindUIEditorPropertyGridSectionIndex(sections, "missing"),
|
||||
static_cast<std::size_t>(-1));
|
||||
|
||||
const auto materialLocation =
|
||||
FindUIEditorPropertyGridFieldLocation(sections, "material");
|
||||
EXPECT_TRUE(materialLocation.IsValid());
|
||||
EXPECT_EQ(materialLocation.sectionIndex, 1u);
|
||||
EXPECT_EQ(materialLocation.fieldIndex, 0u);
|
||||
const auto renderModeLocation =
|
||||
FindUIEditorPropertyGridFieldLocation(sections, "render_mode");
|
||||
EXPECT_TRUE(renderModeLocation.IsValid());
|
||||
EXPECT_EQ(renderModeLocation.sectionIndex, 0u);
|
||||
EXPECT_EQ(renderModeLocation.fieldIndex, 2u);
|
||||
|
||||
const auto missingLocation =
|
||||
FindUIEditorPropertyGridFieldLocation(sections, "unknown");
|
||||
EXPECT_FALSE(missingLocation.IsValid());
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridTest, LayoutBuildsSectionHeadersAndVisibleFieldRects) {
|
||||
TEST(UIEditorPropertyGridTest, LayoutBuildsTypedFieldRectsWithStableColumns) {
|
||||
const auto sections = BuildSections();
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("transform");
|
||||
expansionModel.Expand("inspector");
|
||||
|
||||
const auto layout = BuildUIEditorPropertyGridLayout(
|
||||
UIRect(10.0f, 20.0f, 420.0f, 240.0f),
|
||||
UIRect(10.0f, 20.0f, 420.0f, 260.0f),
|
||||
sections,
|
||||
expansionModel);
|
||||
|
||||
ASSERT_EQ(layout.sectionHeaderRects.size(), sections.size());
|
||||
EXPECT_EQ(layout.visibleFieldIndices.size(), 2u);
|
||||
ASSERT_EQ(layout.visibleFieldIndices.size(), 4u);
|
||||
EXPECT_EQ(layout.visibleFieldSectionIndices[0], 0u);
|
||||
EXPECT_EQ(layout.visibleFieldIndices[1], 1u);
|
||||
EXPECT_FLOAT_EQ(layout.sectionHeaderRects[0].x, 18.0f);
|
||||
EXPECT_FLOAT_EQ(layout.sectionHeaderRects[0].y, 28.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[0].x, 254.0f);
|
||||
EXPECT_GT(layout.fieldValueRects[0].width, 0.0f);
|
||||
|
||||
EXPECT_EQ(
|
||||
FindUIEditorPropertyGridVisibleFieldIndex(layout, "position", sections),
|
||||
FindUIEditorPropertyGridVisibleFieldIndex(layout, "enabled", sections),
|
||||
0u);
|
||||
EXPECT_EQ(
|
||||
FindUIEditorPropertyGridVisibleFieldIndex(layout, "material", sections),
|
||||
FindUIEditorPropertyGridVisibleFieldIndex(layout, "guid", sections),
|
||||
static_cast<std::size_t>(-1));
|
||||
|
||||
EXPECT_FLOAT_EQ(layout.fieldLabelRects[0].x, layout.fieldRowRects[0].x + 12.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[0].x, layout.fieldRowRects[0].x + 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[0].y, layout.fieldRowRects[0].y);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[1].x, layout.fieldRowRects[1].x + 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[1].y, layout.fieldRowRects[1].y + 4.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[2].x, layout.fieldRowRects[2].x + 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[2].y, layout.fieldRowRects[2].y + 4.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[3].x, layout.fieldRowRects[3].x + 236.0f);
|
||||
EXPECT_FLOAT_EQ(layout.fieldValueRects[3].y, layout.fieldRowRects[3].y + 4.0f);
|
||||
EXPECT_GT(layout.fieldValueRects[2].width, 0.0f);
|
||||
EXPECT_GT(layout.fieldValueRects[3].width, 0.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridTest, HitTestResolvesHeaderRowAndValueBox) {
|
||||
TEST(UIEditorPropertyGridTest, HitTestResolvesHeaderRowAndTypedValueHosts) {
|
||||
const auto sections = BuildSections();
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("transform");
|
||||
expansionModel.Expand("rendering");
|
||||
expansionModel.Expand("inspector");
|
||||
expansionModel.Expand("metadata");
|
||||
|
||||
const auto layout = BuildUIEditorPropertyGridLayout(
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
@@ -134,32 +202,46 @@ TEST(UIEditorPropertyGridTest, HitTestResolvesHeaderRowAndValueBox) {
|
||||
layout.fieldRowRects[1].x + 16.0f,
|
||||
layout.fieldRowRects[1].y + layout.fieldRowRects[1].height * 0.5f));
|
||||
EXPECT_EQ(rowHit.kind, UIEditorPropertyGridHitTargetKind::FieldRow);
|
||||
EXPECT_EQ(rowHit.sectionIndex, 0u);
|
||||
EXPECT_EQ(rowHit.fieldIndex, 1u);
|
||||
|
||||
const auto valueHit =
|
||||
const auto boolValueHit =
|
||||
HitTestUIEditorPropertyGrid(layout, RectCenter(layout.fieldValueRects[0]));
|
||||
EXPECT_EQ(boolValueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
|
||||
EXPECT_EQ(boolValueHit.fieldIndex, 0u);
|
||||
|
||||
const auto boolTrailingHit = HitTestUIEditorPropertyGrid(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.fieldValueRects[0].x + layout.fieldValueRects[0].width - 6.0f,
|
||||
layout.fieldValueRects[0].y + layout.fieldValueRects[0].height * 0.5f));
|
||||
EXPECT_EQ(boolTrailingHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
|
||||
EXPECT_EQ(boolTrailingHit.fieldIndex, 0u);
|
||||
|
||||
const auto enumValueHit =
|
||||
HitTestUIEditorPropertyGrid(layout, RectCenter(layout.fieldValueRects[2]));
|
||||
EXPECT_EQ(valueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
|
||||
EXPECT_EQ(valueHit.sectionIndex, 1u);
|
||||
EXPECT_EQ(valueHit.fieldIndex, 0u);
|
||||
EXPECT_EQ(enumValueHit.kind, UIEditorPropertyGridHitTargetKind::ValueBox);
|
||||
EXPECT_EQ(enumValueHit.fieldIndex, 2u);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitStableCommands) {
|
||||
TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitTypedCommandsAndPopupOverlay) {
|
||||
const auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("rotation");
|
||||
selectionModel.SetSelection("render_mode");
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("transform");
|
||||
expansionModel.Expand("rendering");
|
||||
expansionModel.Expand("inspector");
|
||||
expansionModel.Expand("metadata");
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
propertyEditModel.BeginEdit("material", "Metal");
|
||||
propertyEditModel.UpdateStagedValue("Mat_Inst");
|
||||
propertyEditModel.BeginEdit("tag", "Player");
|
||||
propertyEditModel.UpdateStagedValue("Hero");
|
||||
UIEditorPropertyGridState state = {};
|
||||
state.focused = true;
|
||||
state.hoveredFieldId = "position";
|
||||
state.hoveredFieldId = "enabled";
|
||||
state.hoveredHitTarget = UIEditorPropertyGridHitTargetKind::ValueBox;
|
||||
state.popupFieldId = "render_mode";
|
||||
state.popupHighlightedIndex = 1u;
|
||||
|
||||
const auto layout = BuildUIEditorPropertyGridLayout(
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
expansionModel);
|
||||
|
||||
@@ -176,14 +258,19 @@ TEST(UIEditorPropertyGridTest, BackgroundAndForegroundEmitStableCommands) {
|
||||
drawList,
|
||||
layout,
|
||||
sections,
|
||||
state,
|
||||
propertyEditModel);
|
||||
|
||||
const auto& commands = drawList.GetCommands();
|
||||
ASSERT_GE(commands.size(), 12u);
|
||||
ASSERT_GE(commands.size(), 16u);
|
||||
EXPECT_EQ(commands[0].type, XCEngine::UI::UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(commands[1].type, XCEngine::UI::UIDrawCommandType::RectOutline);
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Transform"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Position"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Mat_Inst"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Inspector"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Enabled"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Render Queue"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Opaque"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Hero"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "EDIT"));
|
||||
EXPECT_TRUE(ContainsTextCommand(drawData, "Cutout"));
|
||||
EXPECT_EQ(ResolveUIEditorPropertyGridFieldValueText(sections[0].fields[1]), "2000");
|
||||
}
|
||||
|
||||
@@ -17,16 +17,75 @@ using XCEngine::UI::Widgets::UIPropertyEditModel;
|
||||
using XCEngine::UI::Widgets::UISelectionModel;
|
||||
using XCEngine::UI::Editor::UIEditorPropertyGridInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorPropertyGridInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridFieldKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorPropertyGridSection;
|
||||
|
||||
UIEditorPropertyGridField MakeTextField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::string value,
|
||||
bool readOnly = false) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.valueText = std::move(value);
|
||||
field.readOnly = readOnly;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeBoolField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
bool value) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Bool;
|
||||
field.boolValue = value;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeNumberField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
double value) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Number;
|
||||
field.numberValue.value = value;
|
||||
field.numberValue.step = 1.0;
|
||||
field.numberValue.minValue = 0.0;
|
||||
field.numberValue.maxValue = 5000.0;
|
||||
field.numberValue.integerMode = true;
|
||||
return field;
|
||||
}
|
||||
|
||||
UIEditorPropertyGridField MakeEnumField(
|
||||
std::string id,
|
||||
std::string label,
|
||||
std::vector<std::string> options,
|
||||
std::size_t selectedIndex) {
|
||||
UIEditorPropertyGridField field = {};
|
||||
field.fieldId = std::move(id);
|
||||
field.label = std::move(label);
|
||||
field.kind = UIEditorPropertyGridFieldKind::Enum;
|
||||
field.enumValue.options = std::move(options);
|
||||
field.enumValue.selectedIndex = selectedIndex;
|
||||
return field;
|
||||
}
|
||||
|
||||
std::vector<UIEditorPropertyGridSection> BuildSections() {
|
||||
return {
|
||||
{
|
||||
"transform",
|
||||
"Transform",
|
||||
"inspector",
|
||||
"Inspector",
|
||||
{
|
||||
{ "position", "Position", "10", false, 0.0f },
|
||||
{ "rotation", "Rotation", "0", false, 0.0f }
|
||||
MakeBoolField("enabled", "Enabled", true),
|
||||
MakeNumberField("render_queue", "Render Queue", 2000.0),
|
||||
MakeEnumField("render_mode", "Render Mode", { "Opaque", "Cutout", "Fade" }, 0u),
|
||||
MakeTextField("tag", "Tag", "Player")
|
||||
},
|
||||
0.0f
|
||||
},
|
||||
@@ -34,8 +93,7 @@ std::vector<UIEditorPropertyGridSection> BuildSections() {
|
||||
"metadata",
|
||||
"Metadata",
|
||||
{
|
||||
{ "tag", "Tag", "", false, 0.0f },
|
||||
{ "guid", "GUID", "asset-guid-001", true, 0.0f }
|
||||
MakeTextField("guid", "GUID", "asset-guid-001", true)
|
||||
},
|
||||
0.0f
|
||||
}
|
||||
@@ -84,14 +142,14 @@ UIPoint RectCenter(const XCEngine::UI::UIRect& rect) {
|
||||
}
|
||||
|
||||
void ExpandAll(UIExpansionModel& expansionModel) {
|
||||
expansionModel.Expand("transform");
|
||||
expansionModel.Expand("inspector");
|
||||
expansionModel.Expand("metadata");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndField) {
|
||||
const auto sections = BuildSections();
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
@@ -103,7 +161,7 @@ TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndFie
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
|
||||
@@ -112,12 +170,12 @@ TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndFie
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{ MakePointerMove(
|
||||
initialFrame.layout.sectionHeaderRects[0].x + 24.0f,
|
||||
initialFrame.layout.sectionHeaderRects[0].y + 16.0f) });
|
||||
EXPECT_EQ(state.propertyGridState.hoveredSectionId, "transform");
|
||||
EXPECT_EQ(state.propertyGridState.hoveredSectionId, "inspector");
|
||||
EXPECT_TRUE(state.propertyGridState.hoveredFieldId.empty());
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
@@ -125,20 +183,20 @@ TEST(UIEditorPropertyGridInteractionTest, PointerMoveUpdatesHoveredSectionAndFie
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{ MakePointerMove(
|
||||
initialFrame.layout.fieldRowRects[1].x + 16.0f,
|
||||
initialFrame.layout.fieldRowRects[1].y + 16.0f) });
|
||||
EXPECT_EQ(frame.result.hitTarget.fieldIndex, 1u);
|
||||
EXPECT_EQ(state.propertyGridState.hoveredFieldId, "rotation");
|
||||
EXPECT_EQ(state.propertyGridState.hoveredFieldId, "render_queue");
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion) {
|
||||
const auto sections = BuildSections();
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
expansionModel.Expand("transform");
|
||||
expansionModel.Expand("inspector");
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
@@ -147,7 +205,7 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
const UIPoint metadataHeaderCenter = RectCenter(initialFrame.layout.sectionHeaderRects[1]);
|
||||
@@ -157,7 +215,7 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(metadataHeaderCenter.x, metadataHeaderCenter.y),
|
||||
@@ -170,8 +228,8 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickSectionHeaderTogglesExpansion
|
||||
EXPECT_TRUE(state.propertyGridState.focused);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, LeftClickFieldRowSelectsFieldAndFocusesGrid) {
|
||||
const auto sections = BuildSections();
|
||||
TEST(UIEditorPropertyGridInteractionTest, LeftClickBoolValueHostTogglesValueAndSelectsField) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
@@ -183,32 +241,34 @@ TEST(UIEditorPropertyGridInteractionTest, LeftClickFieldRowSelectsFieldAndFocuse
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
const UIPoint rowCenter = RectCenter(initialFrame.layout.fieldRowRects[1]);
|
||||
const UIPoint boolValueCenter = RectCenter(initialFrame.layout.fieldValueRects[0]);
|
||||
|
||||
const auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(rowCenter.x, rowCenter.y),
|
||||
MakePointerUp(rowCenter.x, rowCenter.y)
|
||||
MakePointerDown(boolValueCenter.x, boolValueCenter.y),
|
||||
MakePointerUp(boolValueCenter.x, boolValueCenter.y)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "rotation");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("rotation"));
|
||||
EXPECT_TRUE(state.propertyGridState.focused);
|
||||
EXPECT_TRUE(frame.result.fieldValueChanged);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "enabled");
|
||||
EXPECT_EQ(frame.result.changedFieldId, "enabled");
|
||||
EXPECT_EQ(frame.result.changedValue, "false");
|
||||
EXPECT_FALSE(sections[0].fields[0].boolValue);
|
||||
EXPECT_TRUE(selectionModel.IsSelected("enabled"));
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, ValueBoxEditCanCommitWithEnter) {
|
||||
const auto sections = BuildSections();
|
||||
TEST(UIEditorPropertyGridInteractionTest, NumberValueBoxEditCanCommitWithEnter) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
@@ -220,25 +280,25 @@ TEST(UIEditorPropertyGridInteractionTest, ValueBoxEditCanCommitWithEnter) {
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
const UIPoint tagValueCenter = RectCenter(initialFrame.layout.fieldValueRects[2]);
|
||||
const UIPoint numberValueCenter = RectCenter(initialFrame.layout.fieldValueRects[1]);
|
||||
|
||||
auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(tagValueCenter.x, tagValueCenter.y),
|
||||
MakePointerUp(tagValueCenter.x, tagValueCenter.y)
|
||||
MakePointerDown(numberValueCenter.x, numberValueCenter.y),
|
||||
MakePointerUp(numberValueCenter.x, numberValueCenter.y)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.consumed);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "tag");
|
||||
EXPECT_EQ(frame.result.activeFieldId, "tag");
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "render_queue");
|
||||
EXPECT_EQ(frame.result.activeFieldId, "render_queue");
|
||||
EXPECT_TRUE(propertyEditModel.HasActiveEdit());
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
@@ -246,22 +306,32 @@ TEST(UIEditorPropertyGridInteractionTest, ValueBoxEditCanCommitWithEnter) {
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakeCharacter('A'),
|
||||
MakeCharacter('B'),
|
||||
MakeKeyDown(KeyCode::Backspace),
|
||||
MakeKeyDown(KeyCode::Backspace),
|
||||
MakeKeyDown(KeyCode::Backspace),
|
||||
MakeKeyDown(KeyCode::Backspace),
|
||||
MakeCharacter('2'),
|
||||
MakeCharacter('5'),
|
||||
MakeCharacter('0'),
|
||||
MakeCharacter('0'),
|
||||
MakeKeyDown(KeyCode::Enter)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_EQ(frame.result.committedFieldId, "tag");
|
||||
EXPECT_EQ(frame.result.committedValue, "AB");
|
||||
EXPECT_TRUE(frame.result.fieldValueChanged);
|
||||
EXPECT_EQ(frame.result.committedFieldId, "render_queue");
|
||||
EXPECT_EQ(frame.result.committedValue, "2500");
|
||||
EXPECT_EQ(frame.result.changedFieldId, "render_queue");
|
||||
EXPECT_EQ(frame.result.changedValue, "2500");
|
||||
EXPECT_FALSE(propertyEditModel.HasActiveEdit());
|
||||
EXPECT_DOUBLE_EQ(sections[0].fields[1].numberValue.value, 2500.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClearsFocus) {
|
||||
const auto sections = BuildSections();
|
||||
TEST(UIEditorPropertyGridInteractionTest, EnumPopupCanOpenNavigateSelectAndClose) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
@@ -273,17 +343,70 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
const UIPoint tagValueCenter = RectCenter(initialFrame.layout.fieldValueRects[2]);
|
||||
const UIPoint enumValueCenter = RectCenter(initialFrame.layout.fieldValueRects[2]);
|
||||
|
||||
auto frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(enumValueCenter.x, enumValueCenter.y),
|
||||
MakePointerUp(enumValueCenter.x, enumValueCenter.y)
|
||||
});
|
||||
EXPECT_TRUE(frame.result.popupOpened);
|
||||
EXPECT_EQ(state.propertyGridState.popupFieldId, "render_mode");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("render_mode"));
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakeKeyDown(KeyCode::Down),
|
||||
MakeKeyDown(KeyCode::Enter)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.popupClosed);
|
||||
EXPECT_TRUE(frame.result.fieldValueChanged);
|
||||
EXPECT_EQ(frame.result.changedFieldId, "render_mode");
|
||||
EXPECT_EQ(frame.result.changedValue, "Cutout");
|
||||
EXPECT_TRUE(state.propertyGridState.popupFieldId.empty());
|
||||
EXPECT_EQ(sections[0].fields[2].enumValue.selectedIndex, 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClearsFocus) {
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
UIEditorPropertyGridInteractionState state = {};
|
||||
|
||||
const auto initialFrame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{});
|
||||
const UIPoint tagValueCenter = RectCenter(initialFrame.layout.fieldValueRects[3]);
|
||||
|
||||
UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(tagValueCenter.x, tagValueCenter.y),
|
||||
@@ -295,7 +418,7 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakeCharacter('A'),
|
||||
@@ -303,14 +426,14 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears
|
||||
});
|
||||
EXPECT_TRUE(frame.result.editCanceled);
|
||||
EXPECT_FALSE(propertyEditModel.HasActiveEdit());
|
||||
EXPECT_TRUE(selectionModel.IsSelected("tag"));
|
||||
EXPECT_EQ(sections[0].fields[3].valueText, "Player");
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{
|
||||
MakePointerDown(520.0f, 360.0f),
|
||||
@@ -321,9 +444,9 @@ TEST(UIEditorPropertyGridInteractionTest, EscapeCancelsEditAndOutsideClickClears
|
||||
}
|
||||
|
||||
TEST(UIEditorPropertyGridInteractionTest, ArrowAndHomeEndKeysNavigateVisibleFields) {
|
||||
const auto sections = BuildSections();
|
||||
auto sections = BuildSections();
|
||||
UISelectionModel selectionModel = {};
|
||||
selectionModel.SetSelection("rotation");
|
||||
selectionModel.SetSelection("render_queue");
|
||||
UIExpansionModel expansionModel = {};
|
||||
ExpandAll(expansionModel);
|
||||
UIPropertyEditModel propertyEditModel = {};
|
||||
@@ -335,31 +458,31 @@ TEST(UIEditorPropertyGridInteractionTest, ArrowAndHomeEndKeysNavigateVisibleFiel
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{ MakeKeyDown(KeyCode::Down) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "tag");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("tag"));
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "render_mode");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("render_mode"));
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{ MakeKeyDown(KeyCode::Home) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "position");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("position"));
|
||||
EXPECT_EQ(frame.result.selectedFieldId, "enabled");
|
||||
EXPECT_TRUE(selectionModel.IsSelected("enabled"));
|
||||
|
||||
frame = UpdateUIEditorPropertyGridInteraction(
|
||||
state,
|
||||
selectionModel,
|
||||
expansionModel,
|
||||
propertyEditModel,
|
||||
UIRect(0.0f, 0.0f, 420.0f, 320.0f),
|
||||
UIRect(0.0f, 0.0f, 420.0f, 340.0f),
|
||||
sections,
|
||||
{ MakeKeyDown(KeyCode::End) });
|
||||
EXPECT_TRUE(frame.result.keyboardNavigated);
|
||||
|
||||
36
tests/UI/Editor/unit/test_ui_editor_text_field.cpp
Normal file
36
tests/UI/Editor/unit/test_ui_editor_text_field.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorTextField.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorTextFieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorTextField;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec;
|
||||
|
||||
TEST(UIEditorTextFieldTest, LayoutBuildsValueRect) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
const auto layout = BuildUIEditorTextFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||
|
||||
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||
EXPECT_GT(layout.controlRect.width, 0.0f);
|
||||
EXPECT_GT(layout.valueRect.width, 0.0f);
|
||||
EXPECT_FLOAT_EQ(layout.controlRect.width, layout.valueRect.width);
|
||||
}
|
||||
|
||||
TEST(UIEditorTextFieldTest, HitTestResolvesValueBoxAndRow) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
const auto layout = BuildUIEditorTextFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorTextField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
|
||||
UIEditorTextFieldHitTargetKind::ValueBox);
|
||||
EXPECT_EQ(
|
||||
HitTestUIEditorTextField(layout, UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f)).kind,
|
||||
UIEditorTextFieldHitTargetKind::Row);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
152
tests/UI/Editor/unit/test_ui_editor_text_field_interaction.cpp
Normal file
152
tests/UI/Editor/unit/test_ui_editor_text_field_interaction.cpp
Normal file
@@ -0,0 +1,152 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Core/UIEditorTextFieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorTextFieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorTextFieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorTextFieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacter(char character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorTextFieldInteractionTest, ClickValueBoxStartsEditing) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
UIEditorTextFieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.valueRect.x + 2.0f,
|
||||
frame.layout.valueRect.y + 2.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.valueRect.x + 2.0f,
|
||||
frame.layout.valueRect.y + 2.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.textFieldState.editing);
|
||||
EXPECT_EQ(spec.value, "Player");
|
||||
}
|
||||
|
||||
TEST(UIEditorTextFieldInteractionTest, EnterStartsEditingAndCommitUpdatesValue) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
UIEditorTextFieldInteractionState state = {};
|
||||
state.textFieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.textFieldState.editing);
|
||||
|
||||
frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeCharacter('X'), MakeKey(KeyCode::Enter) });
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_FALSE(state.textFieldState.editing);
|
||||
EXPECT_EQ(spec.value, "PlayerX");
|
||||
EXPECT_EQ(frame.result.committedText, "PlayerX");
|
||||
}
|
||||
|
||||
TEST(UIEditorTextFieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
UIEditorTextFieldInteractionState state = {};
|
||||
state.textFieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeCharacter('N') });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.textFieldState.editing);
|
||||
EXPECT_EQ(state.textFieldState.displayText, "N");
|
||||
|
||||
frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Escape) });
|
||||
EXPECT_TRUE(frame.result.editCanceled);
|
||||
EXPECT_FALSE(state.textFieldState.editing);
|
||||
EXPECT_EQ(spec.value, "Player");
|
||||
EXPECT_EQ(state.textFieldState.displayText, "Player");
|
||||
}
|
||||
|
||||
TEST(UIEditorTextFieldInteractionTest, FocusLostCommitsEdit) {
|
||||
UIEditorTextFieldSpec spec = { "name", "Name", "Player", false };
|
||||
UIEditorTextFieldInteractionState state = {};
|
||||
state.textFieldState.focused = true;
|
||||
|
||||
auto frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Enter), MakeCharacter('1') });
|
||||
EXPECT_TRUE(state.textFieldState.editing);
|
||||
|
||||
frame = UpdateUIEditorTextFieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||
{
|
||||
UIInputEvent {
|
||||
.type = UIInputEventType::FocusLost
|
||||
}
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_EQ(spec.value, "Player1");
|
||||
EXPECT_FALSE(state.textFieldState.editing);
|
||||
EXPECT_FALSE(state.textFieldState.focused);
|
||||
}
|
||||
371
tests/UI/Editor/unit/test_ui_editor_theme.cpp
Normal file
371
tests/UI/Editor/unit/test_ui_editor_theme.cpp
Normal file
@@ -0,0 +1,371 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Core/UIEditorTheme.h>
|
||||
|
||||
#include <XCEngine/Core/Math/Color.h>
|
||||
#include <XCEngine/UI/Style/StyleTypes.h>
|
||||
#include <XCEngine/UI/Style/Theme.h>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Math = XCEngine::Math;
|
||||
namespace Style = XCEngine::UI::Style;
|
||||
namespace UI = XCEngine::UI;
|
||||
namespace Editor = XCEngine::UI::Editor;
|
||||
|
||||
Style::UITheme BuildEditorFieldTheme() {
|
||||
Style::UIThemeDefinition definition = {};
|
||||
definition.SetToken("editor.size.field.row", Style::UIStyleValue(26.0f));
|
||||
definition.SetToken("editor.space.field.padding_x", Style::UIStyleValue(8.0f));
|
||||
definition.SetToken("editor.space.field.label_gap", Style::UIStyleValue(12.0f));
|
||||
definition.SetToken("editor.layout.field.control_column", Style::UIStyleValue(220.0f));
|
||||
definition.SetToken("editor.size.field.checkbox", Style::UIStyleValue(15.0f));
|
||||
definition.SetToken("editor.space.field.control_inset_y", Style::UIStyleValue(3.0f));
|
||||
definition.SetToken("editor.space.field.label_inset_y", Style::UIStyleValue(5.0f));
|
||||
definition.SetToken("editor.space.field.value_inset_x", Style::UIStyleValue(6.0f));
|
||||
definition.SetToken("editor.space.field.value_inset_y", Style::UIStyleValue(4.0f));
|
||||
definition.SetToken("editor.space.field.checkbox_glyph_inset_x", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.space.field.checkbox_glyph_inset_y", Style::UIStyleValue(-2.0f));
|
||||
definition.SetToken("editor.size.field.control_min_width", Style::UIStyleValue(88.0f));
|
||||
definition.SetToken("editor.space.field.vector_component_gap", Style::UIStyleValue(6.0f));
|
||||
definition.SetToken("editor.size.field.vector_component_min_width", Style::UIStyleValue(74.0f));
|
||||
definition.SetToken("editor.size.field.vector_prefix_width", Style::UIStyleValue(18.0f));
|
||||
definition.SetToken("editor.space.field.control_trailing_inset", Style::UIStyleValue(9.0f));
|
||||
definition.SetToken("editor.space.field.vector_prefix_gap", Style::UIStyleValue(5.0f));
|
||||
definition.SetToken("editor.space.field.vector_prefix_inset_x", Style::UIStyleValue(4.0f));
|
||||
definition.SetToken("editor.space.field.vector_prefix_inset_y", Style::UIStyleValue(5.0f));
|
||||
definition.SetToken("editor.size.field.dropdown_arrow_width", Style::UIStyleValue(14.0f));
|
||||
definition.SetToken("editor.space.field.dropdown_arrow_inset_x", Style::UIStyleValue(4.0f));
|
||||
definition.SetToken("editor.space.field.dropdown_arrow_inset_y", Style::UIStyleValue(4.0f));
|
||||
definition.SetToken("editor.radius.field.row", Style::UIStyleValue(2.0f));
|
||||
definition.SetToken("editor.radius.field.control", Style::UIStyleValue(2.0f));
|
||||
definition.SetToken("editor.border.field", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.border.field.focus", Style::UIStyleValue(2.0f));
|
||||
definition.SetToken("editor.font.field.label", Style::UIStyleValue(11.0f));
|
||||
definition.SetToken("editor.font.field.value", Style::UIStyleValue(12.0f));
|
||||
definition.SetToken("editor.font.field.glyph", Style::UIStyleValue(10.0f));
|
||||
definition.SetToken("editor.space.menu_popup.padding_x", Style::UIStyleValue(6.0f));
|
||||
definition.SetToken("editor.space.menu_popup.padding_y", Style::UIStyleValue(5.0f));
|
||||
definition.SetToken("editor.size.menu_popup.item", Style::UIStyleValue(24.0f));
|
||||
definition.SetToken("editor.size.menu_popup.separator", Style::UIStyleValue(8.0f));
|
||||
definition.SetToken("editor.size.menu_popup.check_column", Style::UIStyleValue(16.0f));
|
||||
definition.SetToken("editor.space.menu_popup.shortcut_gap", Style::UIStyleValue(18.0f));
|
||||
definition.SetToken("editor.size.menu_popup.submenu_indicator", Style::UIStyleValue(12.0f));
|
||||
definition.SetToken("editor.radius.menu_popup.row", Style::UIStyleValue(3.0f));
|
||||
definition.SetToken("editor.radius.menu_popup.surface", Style::UIStyleValue(4.0f));
|
||||
definition.SetToken("editor.space.menu_popup.label_inset_x", Style::UIStyleValue(10.0f));
|
||||
definition.SetToken("editor.space.menu_popup.label_inset_y", Style::UIStyleValue(-1.0f));
|
||||
definition.SetToken("editor.font.menu_popup.label", Style::UIStyleValue(11.0f));
|
||||
definition.SetToken("editor.space.menu_popup.shortcut_inset_right", Style::UIStyleValue(18.0f));
|
||||
definition.SetToken("editor.size.menu_popup.estimated_glyph_width", Style::UIStyleValue(6.0f));
|
||||
definition.SetToken("editor.font.menu_popup.glyph", Style::UIStyleValue(10.0f));
|
||||
definition.SetToken("editor.border.menu_popup.separator", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.border.menu_popup.surface", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.color.field.row", Style::UIStyleValue(Math::Color(0.16f, 0.16f, 0.16f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.row_hover", Style::UIStyleValue(Math::Color(0.20f, 0.20f, 0.20f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.row_active", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.border", Style::UIStyleValue(Math::Color(0.12f, 0.12f, 0.12f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.border_focus", Style::UIStyleValue(Math::Color(0.78f, 0.78f, 0.78f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.label", Style::UIStyleValue(Math::Color(0.84f, 0.84f, 0.84f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.value", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.value_readonly", Style::UIStyleValue(Math::Color(0.60f, 0.60f, 0.60f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.control", Style::UIStyleValue(Math::Color(0.18f, 0.18f, 0.18f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.control_hover", Style::UIStyleValue(Math::Color(0.21f, 0.21f, 0.21f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.control_editing", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.control_readonly", Style::UIStyleValue(Math::Color(0.15f, 0.15f, 0.15f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.control_border", Style::UIStyleValue(Math::Color(0.30f, 0.30f, 0.30f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.control_border_focus", Style::UIStyleValue(Math::Color(0.64f, 0.64f, 0.64f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.vector_prefix", Style::UIStyleValue(Math::Color(0.20f, 0.20f, 0.20f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.vector_prefix_border", Style::UIStyleValue(Math::Color(0.31f, 0.31f, 0.31f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.vector_axis_x", Style::UIStyleValue(Math::Color(0.78f, 0.42f, 0.42f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.vector_axis_y", Style::UIStyleValue(Math::Color(0.56f, 0.72f, 0.46f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.vector_axis_z", Style::UIStyleValue(Math::Color(0.45f, 0.62f, 0.82f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.checkbox", Style::UIStyleValue(Math::Color(0.19f, 0.19f, 0.19f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.checkbox_hover", Style::UIStyleValue(Math::Color(0.22f, 0.22f, 0.22f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.checkbox_border", Style::UIStyleValue(Math::Color(0.33f, 0.33f, 0.33f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.checkbox_mark", Style::UIStyleValue(Math::Color(0.90f, 0.90f, 0.90f, 1.0f)));
|
||||
definition.SetToken("editor.color.field.dropdown_arrow", Style::UIStyleValue(Math::Color(0.88f, 0.88f, 0.88f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu_popup.surface", Style::UIStyleValue(Math::Color(0.15f, 0.15f, 0.15f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu_popup.border", Style::UIStyleValue(Math::Color(0.32f, 0.32f, 0.32f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu_popup.item_hover", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu_popup.item_open", Style::UIStyleValue(Math::Color(0.27f, 0.27f, 0.27f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu_popup.separator", Style::UIStyleValue(Math::Color(0.35f, 0.35f, 0.35f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu_popup.label", Style::UIStyleValue(Math::Color(0.91f, 0.91f, 0.91f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu_popup.shortcut", Style::UIStyleValue(Math::Color(0.76f, 0.76f, 0.76f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu_popup.text_disabled", Style::UIStyleValue(Math::Color(0.48f, 0.48f, 0.48f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu_popup.glyph", Style::UIStyleValue(Math::Color(0.86f, 0.86f, 0.86f, 1.0f)));
|
||||
return Style::BuildTheme(definition);
|
||||
}
|
||||
|
||||
Style::UITheme BuildPropertyGridTheme() {
|
||||
Style::UIThemeDefinition definition = {};
|
||||
definition.SetToken("editor.space.property.content_inset", Style::UIStyleValue(6.0f));
|
||||
definition.SetToken("editor.space.property.section_gap", Style::UIStyleValue(4.0f));
|
||||
definition.SetToken("editor.size.property.section_header", Style::UIStyleValue(24.0f));
|
||||
definition.SetToken("editor.size.property.field_row", Style::UIStyleValue(26.0f));
|
||||
definition.SetToken("editor.space.property.row_gap", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.size.property.disclosure", Style::UIStyleValue(10.0f));
|
||||
definition.SetToken("editor.space.property.disclosure_label_gap", Style::UIStyleValue(6.0f));
|
||||
definition.SetToken("editor.space.property.section_inset_y", Style::UIStyleValue(4.0f));
|
||||
definition.SetToken("editor.space.property.disclosure_glyph_inset_x", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.space.property.disclosure_glyph_inset_y", Style::UIStyleValue(-1.0f));
|
||||
definition.SetToken("editor.space.property.label_inset_y", Style::UIStyleValue(5.0f));
|
||||
definition.SetToken("editor.space.property.value_inset_y", Style::UIStyleValue(4.0f));
|
||||
definition.SetToken("editor.space.property.value_box_inset_y", Style::UIStyleValue(3.0f));
|
||||
definition.SetToken("editor.space.property.value_box_inset_x", Style::UIStyleValue(6.0f));
|
||||
definition.SetToken("editor.radius.property.panel", Style::UIStyleValue(0.0f));
|
||||
definition.SetToken("editor.radius.property.value", Style::UIStyleValue(2.0f));
|
||||
definition.SetToken("editor.border.property", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.border.property.focus", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.border.property.edit", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.font.property.section", Style::UIStyleValue(11.0f));
|
||||
definition.SetToken("editor.font.property.disclosure", Style::UIStyleValue(10.0f));
|
||||
definition.SetToken("editor.font.property.label", Style::UIStyleValue(11.0f));
|
||||
definition.SetToken("editor.font.property.value", Style::UIStyleValue(12.0f));
|
||||
definition.SetToken("editor.font.property.tag", Style::UIStyleValue(10.0f));
|
||||
definition.SetToken("editor.color.property.surface", Style::UIStyleValue(Math::Color(0.17f, 0.17f, 0.17f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.border", Style::UIStyleValue(Math::Color(0.10f, 0.10f, 0.10f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.border_focus", Style::UIStyleValue(Math::Color(0.75f, 0.75f, 0.75f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.section", Style::UIStyleValue(Math::Color(0.21f, 0.21f, 0.21f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.section_hover", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.field_hover", Style::UIStyleValue(Math::Color(0.22f, 0.22f, 0.22f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.field_selected", Style::UIStyleValue(Math::Color(0.26f, 0.26f, 0.26f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.field_selected_focused", Style::UIStyleValue(Math::Color(0.30f, 0.30f, 0.30f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.value", Style::UIStyleValue(Math::Color(0.18f, 0.18f, 0.18f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.value_hover", Style::UIStyleValue(Math::Color(0.21f, 0.21f, 0.21f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.value_editing", Style::UIStyleValue(Math::Color(0.23f, 0.23f, 0.23f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.value_readonly", Style::UIStyleValue(Math::Color(0.15f, 0.15f, 0.15f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.value_border", Style::UIStyleValue(Math::Color(0.28f, 0.28f, 0.28f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.value_border_editing", Style::UIStyleValue(Math::Color(0.72f, 0.72f, 0.72f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.disclosure", Style::UIStyleValue(Math::Color(0.84f, 0.84f, 0.84f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.section_text", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.label", Style::UIStyleValue(Math::Color(0.80f, 0.80f, 0.80f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.value_text", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.value_text_readonly", Style::UIStyleValue(Math::Color(0.60f, 0.60f, 0.60f, 1.0f)));
|
||||
definition.SetToken("editor.color.property.edit_tag", Style::UIStyleValue(Math::Color(0.55f, 0.70f, 0.96f, 1.0f)));
|
||||
definition.SetToken("editor.space.menu.padding_x", Style::UIStyleValue(7.0f));
|
||||
definition.SetToken("editor.space.menu.padding_y", Style::UIStyleValue(5.0f));
|
||||
definition.SetToken("editor.size.menu.item", Style::UIStyleValue(24.0f));
|
||||
definition.SetToken("editor.size.menu.separator", Style::UIStyleValue(8.0f));
|
||||
definition.SetToken("editor.size.menu.check_column", Style::UIStyleValue(16.0f));
|
||||
definition.SetToken("editor.space.menu.shortcut_gap", Style::UIStyleValue(18.0f));
|
||||
definition.SetToken("editor.size.menu.submenu_indicator", Style::UIStyleValue(12.0f));
|
||||
definition.SetToken("editor.radius.menu.row", Style::UIStyleValue(3.0f));
|
||||
definition.SetToken("editor.radius.menu.surface", Style::UIStyleValue(6.0f));
|
||||
definition.SetToken("editor.space.menu.label_inset_x", Style::UIStyleValue(11.0f));
|
||||
definition.SetToken("editor.space.menu.label_inset_y", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.font.menu.label", Style::UIStyleValue(11.0f));
|
||||
definition.SetToken("editor.space.menu.shortcut_inset_right", Style::UIStyleValue(18.0f));
|
||||
definition.SetToken("editor.size.menu.glyph_width", Style::UIStyleValue(6.0f));
|
||||
definition.SetToken("editor.font.menu.glyph", Style::UIStyleValue(10.0f));
|
||||
definition.SetToken("editor.border.menu.separator", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.border.menu.surface", Style::UIStyleValue(1.0f));
|
||||
definition.SetToken("editor.color.menu.surface", Style::UIStyleValue(Math::Color(0.16f, 0.16f, 0.16f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu.border", Style::UIStyleValue(Math::Color(0.24f, 0.24f, 0.24f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu.item_hover", Style::UIStyleValue(Math::Color(0.23f, 0.23f, 0.23f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu.item_open", Style::UIStyleValue(Math::Color(0.28f, 0.28f, 0.28f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu.separator", Style::UIStyleValue(Math::Color(0.30f, 0.30f, 0.30f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu.text", Style::UIStyleValue(Math::Color(0.92f, 0.92f, 0.92f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu.text_muted", Style::UIStyleValue(Math::Color(0.74f, 0.74f, 0.74f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu.text_disabled", Style::UIStyleValue(Math::Color(0.47f, 0.47f, 0.47f, 1.0f)));
|
||||
definition.SetToken("editor.color.menu.glyph", Style::UIStyleValue(Math::Color(0.86f, 0.86f, 0.86f, 1.0f)));
|
||||
return Style::BuildTheme(definition);
|
||||
}
|
||||
|
||||
TEST(UIEditorThemeTest, FieldResolversReadEditorThemeTokens) {
|
||||
const Style::UITheme theme = BuildEditorFieldTheme();
|
||||
|
||||
const auto boolMetrics = Editor::ResolveUIEditorBoolFieldMetrics(theme);
|
||||
const auto boolPalette = Editor::ResolveUIEditorBoolFieldPalette(theme);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.rowHeight, 26.0f);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.horizontalPadding, 8.0f);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.controlTrailingInset, 9.0f);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.checkboxGlyphFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(boolPalette.rowHoverColor.r, 0.20f);
|
||||
EXPECT_FLOAT_EQ(boolPalette.checkboxBorderColor.r, 0.33f);
|
||||
|
||||
const auto numberMetrics = Editor::ResolveUIEditorNumberFieldMetrics(theme);
|
||||
const auto numberPalette = Editor::ResolveUIEditorNumberFieldPalette(theme);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.controlTrailingInset, 9.0f);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.controlInsetY, 3.0f);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.valueTextInsetX, 6.0f);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.valueFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(numberPalette.valueBoxEditingColor.r, 0.24f);
|
||||
EXPECT_FLOAT_EQ(numberPalette.controlBorderColor.r, 0.30f);
|
||||
EXPECT_FLOAT_EQ(numberPalette.controlFocusedBorderColor.r, 0.64f);
|
||||
|
||||
const auto textMetrics = Editor::ResolveUIEditorTextFieldMetrics(theme);
|
||||
const auto textPalette = Editor::ResolveUIEditorTextFieldPalette(theme);
|
||||
EXPECT_FLOAT_EQ(textMetrics.controlTrailingInset, 9.0f);
|
||||
EXPECT_FLOAT_EQ(textMetrics.valueBoxMinWidth, 88.0f);
|
||||
EXPECT_FLOAT_EQ(textMetrics.valueTextInsetY, 4.0f);
|
||||
EXPECT_FLOAT_EQ(textMetrics.valueFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(textPalette.valueBoxEditingColor.r, 0.24f);
|
||||
EXPECT_FLOAT_EQ(textPalette.readOnlyValueColor.r, 0.60f);
|
||||
EXPECT_FLOAT_EQ(textPalette.controlFocusedBorderColor.r, 0.64f);
|
||||
|
||||
const auto vector2Metrics = Editor::ResolveUIEditorVector2FieldMetrics(theme);
|
||||
const auto vector2Palette = Editor::ResolveUIEditorVector2FieldPalette(theme);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.componentGap, 6.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.componentPrefixWidth, 18.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.controlTrailingInset, 9.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.componentLabelGap, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.prefixFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Palette.componentFocusedBorderColor.r, 0.64f);
|
||||
EXPECT_FLOAT_EQ(vector2Palette.axisXColor.r, 0.78f);
|
||||
EXPECT_FLOAT_EQ(vector2Palette.axisYColor.g, 0.72f);
|
||||
|
||||
const auto vector3Metrics = Editor::ResolveUIEditorVector3FieldMetrics(theme);
|
||||
const auto vector3Palette = Editor::ResolveUIEditorVector3FieldPalette(theme);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.componentGap, 6.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.componentPrefixWidth, 18.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.controlTrailingInset, 9.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.componentLabelGap, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.prefixFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Palette.componentFocusedBorderColor.r, 0.64f);
|
||||
EXPECT_FLOAT_EQ(vector3Palette.axisZColor.b, 0.82f);
|
||||
|
||||
const auto enumMetrics = Editor::ResolveUIEditorEnumFieldMetrics(theme);
|
||||
const auto enumPalette = Editor::ResolveUIEditorEnumFieldPalette(theme);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 9.0f);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.valueBoxMinWidth, 88.0f);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowWidth, 14.0f);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(enumPalette.arrowColor.r, 0.88f);
|
||||
|
||||
const auto popupMetrics = Editor::ResolveUIEditorMenuPopupMetrics(theme);
|
||||
const auto popupPalette = Editor::ResolveUIEditorMenuPopupPalette(theme);
|
||||
EXPECT_FLOAT_EQ(popupMetrics.contentPaddingX, 6.0f);
|
||||
EXPECT_FLOAT_EQ(popupMetrics.itemHeight, 24.0f);
|
||||
EXPECT_FLOAT_EQ(popupMetrics.glyphFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(popupPalette.popupColor.r, 0.15f);
|
||||
EXPECT_FLOAT_EQ(popupPalette.textPrimary.r, 0.91f);
|
||||
EXPECT_FLOAT_EQ(popupPalette.textMuted.r, 0.76f);
|
||||
}
|
||||
|
||||
TEST(UIEditorThemeTest, PropertyGridResolversSupportOverridesAndFallbacks) {
|
||||
const Style::UITheme theme = BuildPropertyGridTheme();
|
||||
const Style::UITheme fallbackTheme = Style::UITheme();
|
||||
|
||||
UI::Editor::Widgets::UIEditorPropertyGridMetrics fallbackMetrics = {};
|
||||
fallbackMetrics.sectionHeaderHeight = 40.0f;
|
||||
fallbackMetrics.disclosureGlyphFontSize = 15.0f;
|
||||
fallbackMetrics.tagFontSize = 13.0f;
|
||||
const auto fallbackResolvedMetrics =
|
||||
Editor::ResolveUIEditorPropertyGridMetrics(fallbackTheme, fallbackMetrics);
|
||||
EXPECT_FLOAT_EQ(fallbackResolvedMetrics.sectionHeaderHeight, 40.0f);
|
||||
EXPECT_FLOAT_EQ(fallbackResolvedMetrics.disclosureGlyphFontSize, 15.0f);
|
||||
EXPECT_FLOAT_EQ(fallbackResolvedMetrics.tagFontSize, 13.0f);
|
||||
|
||||
UI::Editor::Widgets::UIEditorPropertyGridPalette fallbackPalette = {};
|
||||
fallbackPalette.surfaceColor = UI::UIColor(0.5f, 0.4f, 0.3f, 1.0f);
|
||||
const auto fallbackResolvedPalette =
|
||||
Editor::ResolveUIEditorPropertyGridPalette(fallbackTheme, fallbackPalette);
|
||||
EXPECT_FLOAT_EQ(fallbackResolvedPalette.surfaceColor.r, 0.5f);
|
||||
|
||||
const auto themedMetrics = Editor::ResolveUIEditorPropertyGridMetrics(theme);
|
||||
const auto themedPalette = Editor::ResolveUIEditorPropertyGridPalette(theme);
|
||||
const auto popupMetrics = Editor::ResolveUIEditorMenuPopupMetrics(theme);
|
||||
const auto popupPalette = Editor::ResolveUIEditorMenuPopupPalette(theme);
|
||||
EXPECT_FLOAT_EQ(themedMetrics.contentInset, 6.0f);
|
||||
EXPECT_FLOAT_EQ(themedMetrics.sectionHeaderHeight, 24.0f);
|
||||
EXPECT_FLOAT_EQ(themedMetrics.disclosureGlyphFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(themedMetrics.tagFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(themedPalette.sectionHeaderColor.r, 0.21f);
|
||||
EXPECT_FLOAT_EQ(themedPalette.valueBoxEditingBorderColor.r, 0.72f);
|
||||
EXPECT_FLOAT_EQ(themedPalette.valueTextColor.r, 0.92f);
|
||||
EXPECT_FLOAT_EQ(themedPalette.readOnlyValueTextColor.r, 0.60f);
|
||||
EXPECT_FLOAT_EQ(popupMetrics.contentPaddingX, 7.0f);
|
||||
EXPECT_FLOAT_EQ(popupMetrics.itemHeight, 24.0f);
|
||||
EXPECT_FLOAT_EQ(popupMetrics.popupCornerRounding, 6.0f);
|
||||
EXPECT_FLOAT_EQ(popupMetrics.glyphFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(popupPalette.popupColor.r, 0.16f);
|
||||
EXPECT_FLOAT_EQ(popupPalette.itemHoverColor.r, 0.23f);
|
||||
EXPECT_FLOAT_EQ(popupPalette.glyphColor.r, 0.86f);
|
||||
}
|
||||
|
||||
TEST(UIEditorThemeTest, HostedFieldBuildersInheritPropertyGridMetricsAndPalette) {
|
||||
UI::Editor::Widgets::UIEditorPropertyGridMetrics propertyMetrics = {};
|
||||
propertyMetrics.fieldRowHeight = 25.0f;
|
||||
propertyMetrics.horizontalPadding = 7.0f;
|
||||
propertyMetrics.labelControlGap = 11.0f;
|
||||
propertyMetrics.controlColumnStart = 210.0f;
|
||||
propertyMetrics.labelTextInsetY = 4.0f;
|
||||
propertyMetrics.labelFontSize = 10.0f;
|
||||
propertyMetrics.valueTextInsetY = 3.0f;
|
||||
propertyMetrics.valueFontSize = 12.0f;
|
||||
propertyMetrics.valueBoxInsetY = 2.0f;
|
||||
propertyMetrics.valueBoxInsetX = 5.0f;
|
||||
propertyMetrics.cornerRounding = 1.0f;
|
||||
propertyMetrics.valueBoxRounding = 2.0f;
|
||||
propertyMetrics.borderThickness = 1.0f;
|
||||
propertyMetrics.focusedBorderThickness = 2.0f;
|
||||
|
||||
UI::Editor::Widgets::UIEditorPropertyGridPalette propertyPalette = {};
|
||||
propertyPalette.valueBoxColor = UI::UIColor(0.2f, 0.2f, 0.2f, 1.0f);
|
||||
propertyPalette.valueBoxHoverColor = UI::UIColor(0.3f, 0.3f, 0.3f, 1.0f);
|
||||
propertyPalette.valueBoxEditingColor = UI::UIColor(0.4f, 0.4f, 0.4f, 1.0f);
|
||||
propertyPalette.valueBoxReadOnlyColor = UI::UIColor(0.15f, 0.15f, 0.15f, 1.0f);
|
||||
propertyPalette.valueBoxBorderColor = UI::UIColor(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
propertyPalette.valueBoxEditingBorderColor = UI::UIColor(0.55f, 0.55f, 0.55f, 1.0f);
|
||||
propertyPalette.labelTextColor = UI::UIColor(0.8f, 0.8f, 0.8f, 1.0f);
|
||||
propertyPalette.valueTextColor = UI::UIColor(0.9f, 0.9f, 0.9f, 1.0f);
|
||||
propertyPalette.readOnlyValueTextColor = UI::UIColor(0.6f, 0.6f, 0.6f, 1.0f);
|
||||
|
||||
const auto boolMetrics = Editor::BuildUIEditorHostedBoolFieldMetrics(propertyMetrics);
|
||||
const auto boolPalette = Editor::BuildUIEditorHostedBoolFieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.rowHeight, 25.0f);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.labelFontSize, 10.0f);
|
||||
EXPECT_FLOAT_EQ(boolMetrics.checkboxGlyphFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(boolPalette.checkboxColor.r, 0.2f);
|
||||
EXPECT_FLOAT_EQ(boolPalette.labelColor.r, 0.8f);
|
||||
|
||||
const auto numberMetrics = Editor::BuildUIEditorHostedNumberFieldMetrics(propertyMetrics);
|
||||
const auto numberPalette = Editor::BuildUIEditorHostedNumberFieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(numberMetrics.valueFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(numberPalette.valueBoxEditingColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(numberPalette.readOnlyValueColor.r, 0.6f);
|
||||
EXPECT_FLOAT_EQ(numberPalette.controlFocusedBorderColor.r, 0.55f);
|
||||
|
||||
const auto textMetrics = Editor::BuildUIEditorHostedTextFieldMetrics(propertyMetrics);
|
||||
const auto textPalette = Editor::BuildUIEditorHostedTextFieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(textMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(textMetrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(textMetrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(textMetrics.valueFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(textPalette.valueBoxEditingColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(textPalette.readOnlyValueColor.r, 0.6f);
|
||||
EXPECT_FLOAT_EQ(textPalette.controlFocusedBorderColor.r, 0.55f);
|
||||
|
||||
const auto vector2Metrics = Editor::BuildUIEditorHostedVector2FieldMetrics(propertyMetrics);
|
||||
const auto vector2Palette = Editor::BuildUIEditorHostedVector2FieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Metrics.componentRounding, 2.0f);
|
||||
EXPECT_FLOAT_EQ(vector2Palette.componentEditingColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(vector2Palette.componentFocusedBorderColor.r, 0.55f);
|
||||
|
||||
const auto vector3Metrics = Editor::BuildUIEditorHostedVector3FieldMetrics(propertyMetrics);
|
||||
const auto vector3Palette = Editor::BuildUIEditorHostedVector3FieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Metrics.componentRounding, 2.0f);
|
||||
EXPECT_FLOAT_EQ(vector3Palette.componentEditingColor.r, 0.4f);
|
||||
EXPECT_FLOAT_EQ(vector3Palette.componentFocusedBorderColor.r, 0.55f);
|
||||
|
||||
const auto enumMetrics = Editor::BuildUIEditorHostedEnumFieldMetrics(propertyMetrics);
|
||||
const auto enumPalette = Editor::BuildUIEditorHostedEnumFieldPalette(propertyPalette);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.controlTrailingInset, 5.0f);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.controlInsetY, 2.0f);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.valueTextInsetX, 5.0f);
|
||||
EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowFontSize, 12.0f);
|
||||
EXPECT_FLOAT_EQ(enumPalette.arrowColor.r, 0.9f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
89
tests/UI/Editor/unit/test_ui_editor_vector2_field.cpp
Normal file
89
tests/UI/Editor/unit/test_ui_editor_vector2_field.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorVector2Field.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorVector2FieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector2FieldComponentValue;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector2Field;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec;
|
||||
|
||||
UIRect MakeInspectorBounds() {
|
||||
return UIRect(0.0f, 0.0f, 392.0f, 22.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldTest, FormatSupportsPerComponentDisplay) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.25, -3.5 };
|
||||
|
||||
EXPECT_EQ(FormatUIEditorVector2FieldComponentValue(spec, 0u), "1.25");
|
||||
EXPECT_EQ(FormatUIEditorVector2FieldComponentValue(spec, 1u), "-3.5");
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldTest, LayoutBuildsTwoComponentRects) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec);
|
||||
|
||||
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||
EXPECT_GT(layout.controlRect.width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[0].width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[1].width, 0.0f);
|
||||
EXPECT_LT(layout.componentRects[0].x + layout.componentRects[0].width, layout.componentRects[1].x);
|
||||
EXPECT_GT(layout.componentPrefixRects[0].width, 0.0f);
|
||||
EXPECT_GT(layout.componentValueRects[0].width, 0.0f);
|
||||
EXPECT_GT(layout.componentValueRects[0].x, layout.componentPrefixRects[0].x + layout.componentPrefixRects[0].width);
|
||||
EXPECT_GT(layout.componentValueRects[1].x, layout.componentPrefixRects[1].x + layout.componentPrefixRects[1].width);
|
||||
EXPECT_EQ(layout.componentValueRects[0].height, layout.componentRects[0].height);
|
||||
EXPECT_EQ(layout.componentValueRects[1].height, layout.componentRects[1].height);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldTest, HitTestTreatsAxisLabelAreaAsComponentHost) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec);
|
||||
|
||||
const auto prefixHit = HitTestUIEditorVector2Field(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.componentPrefixRects[0].x + layout.componentPrefixRects[0].width * 0.5f,
|
||||
layout.componentPrefixRects[0].y + layout.componentPrefixRects[0].height * 0.5f));
|
||||
EXPECT_EQ(prefixHit.kind, UIEditorVector2FieldHitTargetKind::Component);
|
||||
EXPECT_EQ(prefixHit.componentIndex, 0u);
|
||||
|
||||
const auto valueHit = HitTestUIEditorVector2Field(
|
||||
layout,
|
||||
UIPoint(
|
||||
layout.componentValueRects[1].x + 4.0f,
|
||||
layout.componentValueRects[1].y + 4.0f));
|
||||
EXPECT_EQ(valueHit.kind, UIEditorVector2FieldHitTargetKind::Component);
|
||||
EXPECT_EQ(valueHit.componentIndex, 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldTest, HitTestResolvesComponentAndRow) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
const auto layout = BuildUIEditorVector2FieldLayout(MakeInspectorBounds(), spec);
|
||||
|
||||
const auto firstHit = HitTestUIEditorVector2Field(
|
||||
layout,
|
||||
UIPoint(layout.componentRects[0].x + 4.0f, layout.componentRects[0].y + 4.0f));
|
||||
EXPECT_EQ(firstHit.kind, UIEditorVector2FieldHitTargetKind::Component);
|
||||
EXPECT_EQ(firstHit.componentIndex, 0u);
|
||||
|
||||
const auto rowHit = HitTestUIEditorVector2Field(
|
||||
layout,
|
||||
UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f));
|
||||
EXPECT_EQ(rowHit.kind, UIEditorVector2FieldHitTargetKind::Row);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,199 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Core/UIEditorVector2FieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorVector2FieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorVector2FieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector2FieldSpec;
|
||||
|
||||
UIRect MakeInspectorBounds() {
|
||||
return UIRect(0.0f, 0.0f, 392.0f, 22.0f);
|
||||
}
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacter(char character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorVector2FieldInteractionTest, ClickSecondComponentStartsEditing) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0 };
|
||||
UIEditorVector2FieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.componentRects[1].x + 4.0f,
|
||||
frame.layout.componentRects[1].y + 4.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.componentRects[1].x + 4.0f,
|
||||
frame.layout.componentRects[1].y + 4.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector2FieldState.editing);
|
||||
EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 1u);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldInteractionTest, ClickAxisLabelAreaAlsoStartsEditing) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0 };
|
||||
UIEditorVector2FieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{});
|
||||
|
||||
const float clickX =
|
||||
frame.layout.componentPrefixRects[0].x + frame.layout.componentPrefixRects[0].width * 0.5f;
|
||||
const float clickY =
|
||||
frame.layout.componentPrefixRects[0].y + frame.layout.componentPrefixRects[0].height * 0.5f;
|
||||
|
||||
frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{
|
||||
MakePointer(UIInputEventType::PointerButtonDown, clickX, clickY, UIPointerButton::Left),
|
||||
MakePointer(UIInputEventType::PointerButtonUp, clickX, clickY, UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector2FieldState.editing);
|
||||
EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 0u);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldInteractionTest, TabSelectsNextComponentAndArrowAppliesStep) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0 };
|
||||
spec.step = 0.5;
|
||||
UIEditorVector2FieldInteractionState state = {};
|
||||
state.vector2FieldState.focused = true;
|
||||
state.vector2FieldState.selectedComponentIndex = 0u;
|
||||
|
||||
auto frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeKey(KeyCode::Tab) });
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(state.vector2FieldState.selectedComponentIndex, 1u);
|
||||
|
||||
frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeKey(KeyCode::Up) });
|
||||
EXPECT_TRUE(frame.result.stepApplied);
|
||||
EXPECT_EQ(frame.result.changedComponentIndex, 1u);
|
||||
EXPECT_DOUBLE_EQ(spec.values[1], 2.5);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldInteractionTest, EnterStartsEditingAndCommitUpdatesSelectedComponent) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0 };
|
||||
spec.integerMode = true;
|
||||
UIEditorVector2FieldInteractionState state = {};
|
||||
state.vector2FieldState.focused = true;
|
||||
state.vector2FieldState.selectedComponentIndex = 0u;
|
||||
|
||||
auto frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector2FieldState.editing);
|
||||
|
||||
frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeCharacter('4'), MakeKey(KeyCode::Enter) });
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_EQ(frame.result.changedComponentIndex, 0u);
|
||||
EXPECT_DOUBLE_EQ(spec.values[0], 14.0);
|
||||
EXPECT_DOUBLE_EQ(spec.values[1], 2.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector2FieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
|
||||
UIEditorVector2FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0 };
|
||||
UIEditorVector2FieldInteractionState state = {};
|
||||
state.vector2FieldState.focused = true;
|
||||
state.vector2FieldState.selectedComponentIndex = 1u;
|
||||
|
||||
auto frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeCharacter('9') });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector2FieldState.editing);
|
||||
EXPECT_EQ(state.vector2FieldState.displayTexts[1], "9");
|
||||
|
||||
frame = UpdateUIEditorVector2FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
MakeInspectorBounds(),
|
||||
{ MakeKey(KeyCode::Escape) });
|
||||
EXPECT_TRUE(frame.result.editCanceled);
|
||||
EXPECT_FALSE(state.vector2FieldState.editing);
|
||||
EXPECT_DOUBLE_EQ(spec.values[1], 2.0);
|
||||
}
|
||||
58
tests/UI/Editor/unit/test_ui_editor_vector3_field.cpp
Normal file
58
tests/UI/Editor/unit/test_ui_editor_vector3_field.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Widgets/UIEditorVector3Field.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::Widgets::BuildUIEditorVector3FieldLayout;
|
||||
using XCEngine::UI::Editor::Widgets::FormatUIEditorVector3FieldComponentValue;
|
||||
using XCEngine::UI::Editor::Widgets::HitTestUIEditorVector3Field;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldHitTargetKind;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec;
|
||||
|
||||
TEST(UIEditorVector3FieldTest, FormatSupportsPerComponentDisplay) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.25, -3.5, 8.0 };
|
||||
|
||||
EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 0u), "1.25");
|
||||
EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 1u), "-3.5");
|
||||
EXPECT_EQ(FormatUIEditorVector3FieldComponentValue(spec, 2u), "8");
|
||||
}
|
||||
|
||||
TEST(UIEditorVector3FieldTest, LayoutBuildsThreeComponentRects) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
const auto layout = BuildUIEditorVector3FieldLayout(UIRect(0.0f, 0.0f, 520.0f, 32.0f), spec);
|
||||
|
||||
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[0].width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[1].width, 0.0f);
|
||||
EXPECT_GT(layout.componentRects[2].width, 0.0f);
|
||||
EXPECT_LT(layout.componentRects[0].x + layout.componentRects[0].width, layout.componentRects[1].x);
|
||||
EXPECT_LT(layout.componentRects[1].x + layout.componentRects[1].width, layout.componentRects[2].x);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector3FieldTest, HitTestResolvesComponentAndRow) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
const auto layout = BuildUIEditorVector3FieldLayout(UIRect(0.0f, 0.0f, 520.0f, 32.0f), spec);
|
||||
|
||||
const auto thirdHit = HitTestUIEditorVector3Field(
|
||||
layout,
|
||||
UIPoint(layout.componentRects[2].x + 4.0f, layout.componentRects[2].y + 4.0f));
|
||||
EXPECT_EQ(thirdHit.kind, UIEditorVector3FieldHitTargetKind::Component);
|
||||
EXPECT_EQ(thirdHit.componentIndex, 2u);
|
||||
|
||||
const auto rowHit = HitTestUIEditorVector3Field(
|
||||
layout,
|
||||
UIPoint(layout.labelRect.x + 4.0f, layout.labelRect.y + 4.0f));
|
||||
EXPECT_EQ(rowHit.kind, UIEditorVector3FieldHitTargetKind::Row);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,164 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEditor/Core/UIEditorVector3FieldInteraction.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::Input::KeyCode;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIPoint;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Editor::UIEditorVector3FieldInteractionState;
|
||||
using XCEngine::UI::Editor::UpdateUIEditorVector3FieldInteraction;
|
||||
using XCEngine::UI::Editor::Widgets::UIEditorVector3FieldSpec;
|
||||
|
||||
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.position = UIPoint(x, y);
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||
return event;
|
||||
}
|
||||
|
||||
UIInputEvent MakeCharacter(char character) {
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::Character;
|
||||
event.character = static_cast<std::uint32_t>(character);
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIEditorVector3FieldInteractionTest, ClickThirdComponentStartsEditing) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0, 3.0 };
|
||||
UIEditorVector3FieldInteractionState state = {};
|
||||
|
||||
auto frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{});
|
||||
|
||||
frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonDown,
|
||||
frame.layout.componentRects[2].x + 4.0f,
|
||||
frame.layout.componentRects[2].y + 4.0f,
|
||||
UIPointerButton::Left),
|
||||
MakePointer(
|
||||
UIInputEventType::PointerButtonUp,
|
||||
frame.layout.componentRects[2].x + 4.0f,
|
||||
frame.layout.componentRects[2].y + 4.0f,
|
||||
UIPointerButton::Left)
|
||||
});
|
||||
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector3FieldState.editing);
|
||||
EXPECT_EQ(state.vector3FieldState.selectedComponentIndex, 2u);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector3FieldInteractionTest, TabSelectsNextComponentAndArrowAppliesStep) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0, 3.0 };
|
||||
spec.step = 0.5;
|
||||
UIEditorVector3FieldInteractionState state = {};
|
||||
state.vector3FieldState.focused = true;
|
||||
state.vector3FieldState.selectedComponentIndex = 1u;
|
||||
|
||||
auto frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Tab) });
|
||||
EXPECT_TRUE(frame.result.selectionChanged);
|
||||
EXPECT_EQ(state.vector3FieldState.selectedComponentIndex, 2u);
|
||||
|
||||
frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Up) });
|
||||
EXPECT_TRUE(frame.result.stepApplied);
|
||||
EXPECT_EQ(frame.result.changedComponentIndex, 2u);
|
||||
EXPECT_DOUBLE_EQ(spec.values[2], 3.5);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector3FieldInteractionTest, EnterStartsEditingAndCommitUpdatesSelectedComponent) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0, 3.0 };
|
||||
spec.integerMode = true;
|
||||
UIEditorVector3FieldInteractionState state = {};
|
||||
state.vector3FieldState.focused = true;
|
||||
state.vector3FieldState.selectedComponentIndex = 1u;
|
||||
|
||||
auto frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Enter) });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector3FieldState.editing);
|
||||
|
||||
frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeCharacter('4'), MakeKey(KeyCode::Enter) });
|
||||
|
||||
EXPECT_TRUE(frame.result.editCommitted);
|
||||
EXPECT_TRUE(frame.result.valueChanged);
|
||||
EXPECT_EQ(frame.result.changedComponentIndex, 1u);
|
||||
EXPECT_DOUBLE_EQ(spec.values[0], 1.0);
|
||||
EXPECT_DOUBLE_EQ(spec.values[1], 24.0);
|
||||
EXPECT_DOUBLE_EQ(spec.values[2], 3.0);
|
||||
}
|
||||
|
||||
TEST(UIEditorVector3FieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
|
||||
UIEditorVector3FieldSpec spec = {};
|
||||
spec.fieldId = "position";
|
||||
spec.label = "Position";
|
||||
spec.values = { 1.0, 2.0, 3.0 };
|
||||
UIEditorVector3FieldInteractionState state = {};
|
||||
state.vector3FieldState.focused = true;
|
||||
state.vector3FieldState.selectedComponentIndex = 0u;
|
||||
|
||||
auto frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeCharacter('9') });
|
||||
EXPECT_TRUE(frame.result.editStarted);
|
||||
EXPECT_TRUE(state.vector3FieldState.editing);
|
||||
EXPECT_EQ(state.vector3FieldState.displayTexts[0], "9");
|
||||
|
||||
frame = UpdateUIEditorVector3FieldInteraction(
|
||||
state,
|
||||
spec,
|
||||
UIRect(0.0f, 0.0f, 520.0f, 32.0f),
|
||||
{ MakeKey(KeyCode::Escape) });
|
||||
EXPECT_TRUE(frame.result.editCanceled);
|
||||
EXPECT_FALSE(state.vector3FieldState.editing);
|
||||
EXPECT_DOUBLE_EQ(spec.values[0], 1.0);
|
||||
}
|
||||
Reference in New Issue
Block a user