ui: add typed editor field foundations

This commit is contained in:
2026-04-08 02:52:28 +08:00
parent 805e07bf90
commit 0a392e1311
69 changed files with 11676 additions and 1169 deletions

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

@@ -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 = {});

View File

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

View 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

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

@@ -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 &sections[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 &sections[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);

View 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

File diff suppressed because it is too large Load Diff

View 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

View 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

View File

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

View File

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

View 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

View File

@@ -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);
}
}
}

View File

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

View File

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

View 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

View 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

View 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