Add editor typed field widgets and validation scenarios
This commit is contained in:
@@ -15,10 +15,13 @@ add_library(XCUIEditorLib STATIC
|
|||||||
src/Core/EditorShellAsset.cpp
|
src/Core/EditorShellAsset.cpp
|
||||||
src/Core/UIEditorCommandDispatcher.cpp
|
src/Core/UIEditorCommandDispatcher.cpp
|
||||||
src/Core/UIEditorCommandRegistry.cpp
|
src/Core/UIEditorCommandRegistry.cpp
|
||||||
|
src/Core/UIEditorBoolFieldInteraction.cpp
|
||||||
src/Core/UIEditorDockHostInteraction.cpp
|
src/Core/UIEditorDockHostInteraction.cpp
|
||||||
|
src/Core/UIEditorEnumFieldInteraction.cpp
|
||||||
src/Core/UIEditorListViewInteraction.cpp
|
src/Core/UIEditorListViewInteraction.cpp
|
||||||
src/Core/UIEditorMenuModel.cpp
|
src/Core/UIEditorMenuModel.cpp
|
||||||
src/Core/UIEditorMenuSession.cpp
|
src/Core/UIEditorMenuSession.cpp
|
||||||
|
src/Core/UIEditorNumberFieldInteraction.cpp
|
||||||
src/Core/UIEditorPanelContentHost.cpp
|
src/Core/UIEditorPanelContentHost.cpp
|
||||||
src/Core/UIEditorPanelHostLifecycle.cpp
|
src/Core/UIEditorPanelHostLifecycle.cpp
|
||||||
src/Core/UIEditorPanelRegistry.cpp
|
src/Core/UIEditorPanelRegistry.cpp
|
||||||
@@ -37,10 +40,13 @@ add_library(XCUIEditorLib STATIC
|
|||||||
src/Core/UIEditorWorkspaceModel.cpp
|
src/Core/UIEditorWorkspaceModel.cpp
|
||||||
src/Core/UIEditorWorkspaceSession.cpp
|
src/Core/UIEditorWorkspaceSession.cpp
|
||||||
src/Widgets/UIEditorCollectionPrimitives.cpp
|
src/Widgets/UIEditorCollectionPrimitives.cpp
|
||||||
|
src/Widgets/UIEditorBoolField.cpp
|
||||||
src/Widgets/UIEditorDockHost.cpp
|
src/Widgets/UIEditorDockHost.cpp
|
||||||
|
src/Widgets/UIEditorEnumField.cpp
|
||||||
src/Widgets/UIEditorListView.cpp
|
src/Widgets/UIEditorListView.cpp
|
||||||
src/Widgets/UIEditorMenuBar.cpp
|
src/Widgets/UIEditorMenuBar.cpp
|
||||||
src/Widgets/UIEditorMenuPopup.cpp
|
src/Widgets/UIEditorMenuPopup.cpp
|
||||||
|
src/Widgets/UIEditorNumberField.cpp
|
||||||
src/Widgets/UIEditorPanelFrame.cpp
|
src/Widgets/UIEditorPanelFrame.cpp
|
||||||
src/Widgets/UIEditorPropertyGrid.cpp
|
src/Widgets/UIEditorPropertyGrid.cpp
|
||||||
src/Widgets/UIEditorScrollView.cpp
|
src/Widgets/UIEditorScrollView.cpp
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEditor/Widgets/UIEditorBoolField.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Types.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor {
|
||||||
|
|
||||||
|
struct UIEditorBoolFieldInteractionState {
|
||||||
|
Widgets::UIEditorBoolFieldState fieldState = {};
|
||||||
|
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||||
|
bool hasPointerPosition = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorBoolFieldInteractionResult {
|
||||||
|
bool consumed = false;
|
||||||
|
bool valueChanged = false;
|
||||||
|
bool focusedChanged = false;
|
||||||
|
bool newValue = false;
|
||||||
|
Widgets::UIEditorBoolFieldHitTarget hitTarget = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorBoolFieldInteractionFrame {
|
||||||
|
Widgets::UIEditorBoolFieldLayout layout = {};
|
||||||
|
UIEditorBoolFieldInteractionResult result = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
UIEditorBoolFieldInteractionFrame UpdateUIEditorBoolFieldInteraction(
|
||||||
|
UIEditorBoolFieldInteractionState& state,
|
||||||
|
bool& value,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const Widgets::UIEditorBoolFieldSpec& spec,
|
||||||
|
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||||
|
const Widgets::UIEditorBoolFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Types.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor {
|
||||||
|
|
||||||
|
struct UIEditorEnumFieldInteractionState {
|
||||||
|
Widgets::UIEditorEnumFieldState fieldState = {};
|
||||||
|
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||||
|
bool hasPointerPosition = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorEnumFieldInteractionResult {
|
||||||
|
bool consumed = false;
|
||||||
|
bool selectionChanged = false;
|
||||||
|
bool focusedChanged = false;
|
||||||
|
std::size_t selectedIndex = 0u;
|
||||||
|
Widgets::UIEditorEnumFieldHitTarget hitTarget = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorEnumFieldInteractionFrame {
|
||||||
|
Widgets::UIEditorEnumFieldLayout layout = {};
|
||||||
|
UIEditorEnumFieldInteractionResult result = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
UIEditorEnumFieldInteractionFrame UpdateUIEditorEnumFieldInteraction(
|
||||||
|
UIEditorEnumFieldInteractionState& state,
|
||||||
|
std::size_t& selectedIndex,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const Widgets::UIEditorEnumFieldSpec& spec,
|
||||||
|
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||||
|
const Widgets::UIEditorEnumFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEditor/Widgets/UIEditorNumberField.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 UIEditorNumberFieldInteractionState {
|
||||||
|
Widgets::UIEditorNumberFieldState numberFieldState = {};
|
||||||
|
::XCEngine::UI::Text::UITextInputState textInputState = {};
|
||||||
|
::XCEngine::UI::Widgets::UIPropertyEditModel editModel = {};
|
||||||
|
::XCEngine::UI::UIPoint pointerPosition = {};
|
||||||
|
bool hasPointerPosition = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorNumberFieldInteractionResult {
|
||||||
|
bool consumed = false;
|
||||||
|
bool focusChanged = false;
|
||||||
|
bool valueChanged = false;
|
||||||
|
bool stepApplied = false;
|
||||||
|
bool editStarted = false;
|
||||||
|
bool editCommitted = false;
|
||||||
|
bool editCommitRejected = false;
|
||||||
|
bool editCanceled = false;
|
||||||
|
Widgets::UIEditorNumberFieldHitTarget hitTarget = {};
|
||||||
|
double valueBefore = 0.0;
|
||||||
|
double valueAfter = 0.0;
|
||||||
|
double stepDelta = 0.0;
|
||||||
|
std::string committedText = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorNumberFieldInteractionFrame {
|
||||||
|
Widgets::UIEditorNumberFieldLayout layout = {};
|
||||||
|
UIEditorNumberFieldInteractionResult result = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction(
|
||||||
|
UIEditorNumberFieldInteractionState& state,
|
||||||
|
Widgets::UIEditorNumberFieldSpec& spec,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||||
|
const Widgets::UIEditorNumberFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor
|
||||||
113
new_editor/include/XCEditor/Widgets/UIEditorBoolField.h
Normal file
113
new_editor/include/XCEditor/Widgets/UIEditorBoolField.h
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::Widgets {
|
||||||
|
|
||||||
|
enum class UIEditorBoolFieldHitTargetKind : std::uint8_t {
|
||||||
|
None = 0,
|
||||||
|
Row,
|
||||||
|
Toggle
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorBoolFieldSpec {
|
||||||
|
std::string fieldId = {};
|
||||||
|
std::string label = {};
|
||||||
|
bool value = false;
|
||||||
|
bool readOnly = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorBoolFieldState {
|
||||||
|
UIEditorBoolFieldHitTargetKind hoveredTarget = UIEditorBoolFieldHitTargetKind::None;
|
||||||
|
bool focused = false;
|
||||||
|
bool active = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorBoolFieldMetrics {
|
||||||
|
float rowHeight = 32.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 borderThickness = 1.0f;
|
||||||
|
float focusedBorderThickness = 2.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorBoolFieldPalette {
|
||||||
|
::XCEngine::UI::UIColor surfaceColor =
|
||||||
|
::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor borderColor =
|
||||||
|
::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor focusedBorderColor =
|
||||||
|
::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor rowHoverColor =
|
||||||
|
::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 1.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.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 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorBoolFieldLayout {
|
||||||
|
::XCEngine::UI::UIRect bounds = {};
|
||||||
|
::XCEngine::UI::UIRect labelRect = {};
|
||||||
|
::XCEngine::UI::UIRect toggleRect = {};
|
||||||
|
::XCEngine::UI::UIRect knobRect = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorBoolFieldHitTarget {
|
||||||
|
UIEditorBoolFieldHitTargetKind kind = UIEditorBoolFieldHitTargetKind::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
UIEditorBoolFieldLayout BuildUIEditorBoolFieldLayout(
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const UIEditorBoolFieldSpec& spec,
|
||||||
|
const UIEditorBoolFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
UIEditorBoolFieldHitTarget HitTestUIEditorBoolField(
|
||||||
|
const UIEditorBoolFieldLayout& layout,
|
||||||
|
const ::XCEngine::UI::UIPoint& point);
|
||||||
|
|
||||||
|
void AppendUIEditorBoolFieldBackground(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const UIEditorBoolFieldLayout& layout,
|
||||||
|
const UIEditorBoolFieldSpec& spec,
|
||||||
|
const UIEditorBoolFieldState& state,
|
||||||
|
const UIEditorBoolFieldPalette& palette = {},
|
||||||
|
const UIEditorBoolFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
void AppendUIEditorBoolFieldForeground(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const UIEditorBoolFieldLayout& layout,
|
||||||
|
const UIEditorBoolFieldSpec& spec,
|
||||||
|
const UIEditorBoolFieldPalette& palette = {},
|
||||||
|
const UIEditorBoolFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
void AppendUIEditorBoolField(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const UIEditorBoolFieldSpec& spec,
|
||||||
|
const UIEditorBoolFieldState& state,
|
||||||
|
const UIEditorBoolFieldPalette& palette = {},
|
||||||
|
const UIEditorBoolFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::Widgets
|
||||||
127
new_editor/include/XCEditor/Widgets/UIEditorEnumField.h
Normal file
127
new_editor/include/XCEditor/Widgets/UIEditorEnumField.h
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::Widgets {
|
||||||
|
|
||||||
|
enum class UIEditorEnumFieldHitTargetKind : std::uint8_t {
|
||||||
|
None = 0,
|
||||||
|
Row,
|
||||||
|
PreviousButton,
|
||||||
|
NextButton,
|
||||||
|
ValueBox
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorEnumFieldSpec {
|
||||||
|
std::string fieldId = {};
|
||||||
|
std::string label = {};
|
||||||
|
std::vector<std::string> options = {};
|
||||||
|
std::size_t selectedIndex = 0u;
|
||||||
|
bool readOnly = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorEnumFieldState {
|
||||||
|
UIEditorEnumFieldHitTargetKind hoveredTarget = UIEditorEnumFieldHitTargetKind::None;
|
||||||
|
bool focused = false;
|
||||||
|
bool active = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorEnumFieldMetrics {
|
||||||
|
float rowHeight = 32.0f;
|
||||||
|
float horizontalPadding = 12.0f;
|
||||||
|
float labelControlGap = 20.0f;
|
||||||
|
float controlColumnStart = 236.0f;
|
||||||
|
float buttonWidth = 24.0f;
|
||||||
|
float controlGap = 4.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 borderThickness = 1.0f;
|
||||||
|
float focusedBorderThickness = 2.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorEnumFieldPalette {
|
||||||
|
::XCEngine::UI::UIColor surfaceColor =
|
||||||
|
::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor borderColor =
|
||||||
|
::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor focusedBorderColor =
|
||||||
|
::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor rowHoverColor =
|
||||||
|
::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor rowActiveColor =
|
||||||
|
::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.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 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 labelColor =
|
||||||
|
::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor valueColor =
|
||||||
|
::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 1.0f);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorEnumFieldLayout {
|
||||||
|
::XCEngine::UI::UIRect bounds = {};
|
||||||
|
::XCEngine::UI::UIRect labelRect = {};
|
||||||
|
::XCEngine::UI::UIRect controlRect = {};
|
||||||
|
::XCEngine::UI::UIRect valueRect = {};
|
||||||
|
::XCEngine::UI::UIRect previousRect = {};
|
||||||
|
::XCEngine::UI::UIRect nextRect = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorEnumFieldHitTarget {
|
||||||
|
UIEditorEnumFieldHitTargetKind kind = UIEditorEnumFieldHitTargetKind::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string ResolveUIEditorEnumFieldValueText(const UIEditorEnumFieldSpec& spec);
|
||||||
|
|
||||||
|
UIEditorEnumFieldLayout BuildUIEditorEnumFieldLayout(
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const UIEditorEnumFieldSpec& spec,
|
||||||
|
const UIEditorEnumFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
UIEditorEnumFieldHitTarget HitTestUIEditorEnumField(
|
||||||
|
const UIEditorEnumFieldLayout& layout,
|
||||||
|
const ::XCEngine::UI::UIPoint& point);
|
||||||
|
|
||||||
|
void AppendUIEditorEnumFieldBackground(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const UIEditorEnumFieldLayout& layout,
|
||||||
|
const UIEditorEnumFieldSpec& spec,
|
||||||
|
const UIEditorEnumFieldState& state,
|
||||||
|
const UIEditorEnumFieldPalette& palette = {},
|
||||||
|
const UIEditorEnumFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
void AppendUIEditorEnumFieldForeground(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const UIEditorEnumFieldLayout& layout,
|
||||||
|
const UIEditorEnumFieldSpec& spec,
|
||||||
|
const UIEditorEnumFieldPalette& palette = {},
|
||||||
|
const UIEditorEnumFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
void AppendUIEditorEnumField(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const UIEditorEnumFieldSpec& spec,
|
||||||
|
const UIEditorEnumFieldState& state,
|
||||||
|
const UIEditorEnumFieldPalette& palette = {},
|
||||||
|
const UIEditorEnumFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::Widgets
|
||||||
146
new_editor/include/XCEditor/Widgets/UIEditorNumberField.h
Normal file
146
new_editor/include/XCEditor/Widgets/UIEditorNumberField.h
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::Widgets {
|
||||||
|
|
||||||
|
enum class UIEditorNumberFieldHitTargetKind : std::uint8_t {
|
||||||
|
None = 0,
|
||||||
|
Row,
|
||||||
|
DecrementButton,
|
||||||
|
IncrementButton,
|
||||||
|
ValueBox
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorNumberFieldSpec {
|
||||||
|
std::string fieldId = {};
|
||||||
|
std::string label = {};
|
||||||
|
double value = 0.0;
|
||||||
|
double step = 1.0;
|
||||||
|
double minValue = 0.0;
|
||||||
|
double maxValue = 100.0;
|
||||||
|
bool integerMode = true;
|
||||||
|
bool readOnly = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorNumberFieldState {
|
||||||
|
UIEditorNumberFieldHitTargetKind hoveredTarget = UIEditorNumberFieldHitTargetKind::None;
|
||||||
|
UIEditorNumberFieldHitTargetKind activeTarget = UIEditorNumberFieldHitTargetKind::None;
|
||||||
|
bool focused = false;
|
||||||
|
bool editing = false;
|
||||||
|
std::string displayText = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorNumberFieldMetrics {
|
||||||
|
float rowHeight = 32.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 borderThickness = 1.0f;
|
||||||
|
float focusedBorderThickness = 2.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorNumberFieldPalette {
|
||||||
|
::XCEngine::UI::UIColor surfaceColor =
|
||||||
|
::XCEngine::UI::UIColor(0.14f, 0.14f, 0.14f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor borderColor =
|
||||||
|
::XCEngine::UI::UIColor(0.29f, 0.29f, 0.29f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor focusedBorderColor =
|
||||||
|
::XCEngine::UI::UIColor(0.84f, 0.84f, 0.84f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor rowHoverColor =
|
||||||
|
::XCEngine::UI::UIColor(0.22f, 0.22f, 0.22f, 1.0f);
|
||||||
|
::XCEngine::UI::UIColor rowActiveColor =
|
||||||
|
::XCEngine::UI::UIColor(0.28f, 0.28f, 0.28f, 1.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 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 labelColor =
|
||||||
|
::XCEngine::UI::UIColor(0.92f, 0.92f, 0.92f, 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 {
|
||||||
|
::XCEngine::UI::UIRect bounds = {};
|
||||||
|
::XCEngine::UI::UIRect labelRect = {};
|
||||||
|
::XCEngine::UI::UIRect controlRect = {};
|
||||||
|
::XCEngine::UI::UIRect valueRect = {};
|
||||||
|
::XCEngine::UI::UIRect decrementRect = {};
|
||||||
|
::XCEngine::UI::UIRect incrementRect = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UIEditorNumberFieldHitTarget {
|
||||||
|
UIEditorNumberFieldHitTargetKind kind = UIEditorNumberFieldHitTargetKind::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsUIEditorNumberFieldPointInside(
|
||||||
|
const ::XCEngine::UI::UIRect& rect,
|
||||||
|
const ::XCEngine::UI::UIPoint& point);
|
||||||
|
|
||||||
|
std::string FormatUIEditorNumberFieldValue(const UIEditorNumberFieldSpec& spec);
|
||||||
|
|
||||||
|
UIEditorNumberFieldLayout BuildUIEditorNumberFieldLayout(
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const UIEditorNumberFieldSpec& spec,
|
||||||
|
const UIEditorNumberFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
UIEditorNumberFieldHitTarget HitTestUIEditorNumberField(
|
||||||
|
const UIEditorNumberFieldLayout& layout,
|
||||||
|
const ::XCEngine::UI::UIPoint& point);
|
||||||
|
|
||||||
|
void AppendUIEditorNumberFieldBackground(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const UIEditorNumberFieldLayout& layout,
|
||||||
|
const UIEditorNumberFieldSpec& spec,
|
||||||
|
const UIEditorNumberFieldState& state,
|
||||||
|
const UIEditorNumberFieldPalette& palette = {},
|
||||||
|
const UIEditorNumberFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
void AppendUIEditorNumberFieldForeground(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const UIEditorNumberFieldLayout& layout,
|
||||||
|
const UIEditorNumberFieldSpec& spec,
|
||||||
|
const UIEditorNumberFieldPalette& palette = {},
|
||||||
|
const UIEditorNumberFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
void AppendUIEditorNumberField(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const UIEditorNumberFieldSpec& spec,
|
||||||
|
const UIEditorNumberFieldState& state,
|
||||||
|
const UIEditorNumberFieldPalette& palette = {},
|
||||||
|
const UIEditorNumberFieldMetrics& metrics = {});
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::Widgets
|
||||||
143
new_editor/src/Core/UIEditorBoolFieldInteraction.cpp
Normal file
143
new_editor/src/Core/UIEditorBoolFieldInteraction.cpp
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
#include <XCEditor/Core/UIEditorBoolFieldInteraction.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using ::XCEngine::Input::KeyCode;
|
||||||
|
using ::XCEngine::UI::UIInputEvent;
|
||||||
|
using ::XCEngine::UI::UIInputEventType;
|
||||||
|
using ::XCEngine::UI::UIPointerButton;
|
||||||
|
using Widgets::BuildUIEditorBoolFieldLayout;
|
||||||
|
using Widgets::HitTestUIEditorBoolField;
|
||||||
|
using Widgets::UIEditorBoolFieldHitTargetKind;
|
||||||
|
|
||||||
|
bool ShouldUsePointerPosition(const UIInputEvent& event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case UIInputEventType::PointerMove:
|
||||||
|
case UIInputEventType::PointerEnter:
|
||||||
|
case UIInputEventType::PointerButtonDown:
|
||||||
|
case UIInputEventType::PointerButtonUp:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
UIEditorBoolFieldInteractionFrame UpdateUIEditorBoolFieldInteraction(
|
||||||
|
UIEditorBoolFieldInteractionState& state,
|
||||||
|
bool& value,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const Widgets::UIEditorBoolFieldSpec& spec,
|
||||||
|
const std::vector<UIInputEvent>& inputEvents,
|
||||||
|
const Widgets::UIEditorBoolFieldMetrics& metrics) {
|
||||||
|
Widgets::UIEditorBoolFieldSpec resolvedSpec = spec;
|
||||||
|
resolvedSpec.value = value;
|
||||||
|
|
||||||
|
Widgets::UIEditorBoolFieldLayout layout =
|
||||||
|
BuildUIEditorBoolFieldLayout(bounds, resolvedSpec, metrics);
|
||||||
|
if (state.hasPointerPosition) {
|
||||||
|
state.fieldState.hoveredTarget = HitTestUIEditorBoolField(layout, state.pointerPosition).kind;
|
||||||
|
} else {
|
||||||
|
state.fieldState.hoveredTarget = UIEditorBoolFieldHitTargetKind::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorBoolFieldInteractionResult 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorBoolFieldInteractionResult eventResult = {};
|
||||||
|
switch (event.type) {
|
||||||
|
case UIInputEventType::FocusGained:
|
||||||
|
state.fieldState.focused = true;
|
||||||
|
eventResult.focusedChanged = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::FocusLost:
|
||||||
|
state.fieldState.focused = false;
|
||||||
|
state.fieldState.active = false;
|
||||||
|
state.hasPointerPosition = false;
|
||||||
|
state.fieldState.hoveredTarget = UIEditorBoolFieldHitTargetKind::None;
|
||||||
|
eventResult.focusedChanged = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::PointerMove:
|
||||||
|
case UIInputEventType::PointerEnter:
|
||||||
|
case UIInputEventType::PointerLeave:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::PointerButtonDown:
|
||||||
|
eventResult.hitTarget = state.hasPointerPosition
|
||||||
|
? HitTestUIEditorBoolField(layout, state.pointerPosition)
|
||||||
|
: Widgets::UIEditorBoolFieldHitTarget {};
|
||||||
|
if (event.pointerButton == UIPointerButton::Left &&
|
||||||
|
eventResult.hitTarget.kind != UIEditorBoolFieldHitTargetKind::None) {
|
||||||
|
state.fieldState.focused = true;
|
||||||
|
state.fieldState.active = true;
|
||||||
|
eventResult.consumed = true;
|
||||||
|
} else if (event.pointerButton == UIPointerButton::Left) {
|
||||||
|
state.fieldState.focused = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::PointerButtonUp:
|
||||||
|
eventResult.hitTarget = state.hasPointerPosition
|
||||||
|
? HitTestUIEditorBoolField(layout, state.pointerPosition)
|
||||||
|
: Widgets::UIEditorBoolFieldHitTarget {};
|
||||||
|
if (event.pointerButton == UIPointerButton::Left &&
|
||||||
|
state.fieldState.active &&
|
||||||
|
eventResult.hitTarget.kind != UIEditorBoolFieldHitTargetKind::None &&
|
||||||
|
!resolvedSpec.readOnly) {
|
||||||
|
value = !value;
|
||||||
|
eventResult.valueChanged = true;
|
||||||
|
eventResult.newValue = value;
|
||||||
|
eventResult.consumed = true;
|
||||||
|
}
|
||||||
|
state.fieldState.active = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::KeyDown:
|
||||||
|
if (state.fieldState.focused &&
|
||||||
|
!resolvedSpec.readOnly &&
|
||||||
|
(static_cast<KeyCode>(event.keyCode) == KeyCode::Space ||
|
||||||
|
static_cast<KeyCode>(event.keyCode) == KeyCode::Enter)) {
|
||||||
|
value = !value;
|
||||||
|
eventResult.valueChanged = true;
|
||||||
|
eventResult.newValue = value;
|
||||||
|
eventResult.consumed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedSpec.value = value;
|
||||||
|
layout = BuildUIEditorBoolFieldLayout(bounds, resolvedSpec, metrics);
|
||||||
|
if (state.hasPointerPosition) {
|
||||||
|
state.fieldState.hoveredTarget = HitTestUIEditorBoolField(layout, state.pointerPosition).kind;
|
||||||
|
} else {
|
||||||
|
state.fieldState.hoveredTarget = UIEditorBoolFieldHitTargetKind::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventResult.consumed ||
|
||||||
|
eventResult.valueChanged ||
|
||||||
|
eventResult.focusedChanged ||
|
||||||
|
eventResult.hitTarget.kind != UIEditorBoolFieldHitTargetKind::None) {
|
||||||
|
interactionResult = eventResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { layout, interactionResult };
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor
|
||||||
190
new_editor/src/Core/UIEditorEnumFieldInteraction.cpp
Normal file
190
new_editor/src/Core/UIEditorEnumFieldInteraction.cpp
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#include <XCEditor/Core/UIEditorEnumFieldInteraction.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using ::XCEngine::Input::KeyCode;
|
||||||
|
using ::XCEngine::UI::UIInputEvent;
|
||||||
|
using ::XCEngine::UI::UIInputEventType;
|
||||||
|
using ::XCEngine::UI::UIPointerButton;
|
||||||
|
using Widgets::BuildUIEditorEnumFieldLayout;
|
||||||
|
using Widgets::HitTestUIEditorEnumField;
|
||||||
|
using Widgets::UIEditorEnumFieldHitTargetKind;
|
||||||
|
|
||||||
|
bool ShouldUsePointerPosition(const UIInputEvent& event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case UIInputEventType::PointerMove:
|
||||||
|
case UIInputEventType::PointerEnter:
|
||||||
|
case UIInputEventType::PointerButtonDown:
|
||||||
|
case UIInputEventType::PointerButtonUp:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MoveSelection(std::size_t& selectedIndex, const Widgets::UIEditorEnumFieldSpec& spec, int direction) {
|
||||||
|
if (spec.options.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t nextIndex = selectedIndex;
|
||||||
|
if (direction < 0) {
|
||||||
|
nextIndex = selectedIndex == 0u ? 0u : selectedIndex - 1u;
|
||||||
|
} else {
|
||||||
|
nextIndex = (selectedIndex + 1u >= spec.options.size()) ? spec.options.size() - 1u : selectedIndex + 1u;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextIndex == selectedIndex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIndex = nextIndex;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
UIEditorEnumFieldInteractionFrame UpdateUIEditorEnumFieldInteraction(
|
||||||
|
UIEditorEnumFieldInteractionState& state,
|
||||||
|
std::size_t& selectedIndex,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const Widgets::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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorEnumFieldInteractionResult 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorEnumFieldInteractionResult eventResult = {};
|
||||||
|
switch (event.type) {
|
||||||
|
case UIInputEventType::FocusGained:
|
||||||
|
state.fieldState.focused = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::FocusLost:
|
||||||
|
state.fieldState.focused = false;
|
||||||
|
state.fieldState.active = false;
|
||||||
|
state.hasPointerPosition = false;
|
||||||
|
state.fieldState.hoveredTarget = UIEditorEnumFieldHitTargetKind::None;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::PointerMove:
|
||||||
|
case UIInputEventType::PointerEnter:
|
||||||
|
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) {
|
||||||
|
state.fieldState.focused = true;
|
||||||
|
state.fieldState.active = true;
|
||||||
|
eventResult.consumed = true;
|
||||||
|
} else if (event.pointerButton == UIPointerButton::Left) {
|
||||||
|
state.fieldState.focused = false;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
eventResult.selectionChanged = true;
|
||||||
|
eventResult.selectedIndex = selectedIndex;
|
||||||
|
eventResult.consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.fieldState.active = false;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::KeyDown:
|
||||||
|
if (state.fieldState.focused && !resolvedSpec.readOnly) {
|
||||||
|
bool changed = false;
|
||||||
|
switch (static_cast<KeyCode>(event.keyCode)) {
|
||||||
|
case KeyCode::Left:
|
||||||
|
changed = MoveSelection(selectedIndex, resolvedSpec, -1);
|
||||||
|
break;
|
||||||
|
case KeyCode::Right:
|
||||||
|
case KeyCode::Enter:
|
||||||
|
changed = MoveSelection(selectedIndex, resolvedSpec, 1);
|
||||||
|
break;
|
||||||
|
case KeyCode::Home:
|
||||||
|
changed = !resolvedSpec.options.empty() && selectedIndex != 0u;
|
||||||
|
selectedIndex = 0u;
|
||||||
|
break;
|
||||||
|
case KeyCode::End:
|
||||||
|
if (!resolvedSpec.options.empty()) {
|
||||||
|
const std::size_t lastIndex = resolvedSpec.options.size() - 1u;
|
||||||
|
changed = selectedIndex != lastIndex;
|
||||||
|
selectedIndex = lastIndex;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
eventResult.selectionChanged = true;
|
||||||
|
eventResult.selectedIndex = selectedIndex;
|
||||||
|
eventResult.consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventResult.consumed ||
|
||||||
|
eventResult.selectionChanged ||
|
||||||
|
eventResult.hitTarget.kind != UIEditorEnumFieldHitTargetKind::None) {
|
||||||
|
interactionResult = eventResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { layout, interactionResult };
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor
|
||||||
477
new_editor/src/Core/UIEditorNumberFieldInteraction.cpp
Normal file
477
new_editor/src/Core/UIEditorNumberFieldInteraction.cpp
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
#include <XCEditor/Core/UIEditorNumberFieldInteraction.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#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::FormatUIEditorNumberFieldValue;
|
||||||
|
using ::XCEngine::UI::Editor::Widgets::BuildUIEditorNumberFieldLayout;
|
||||||
|
using ::XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField;
|
||||||
|
using ::XCEngine::UI::Editor::Widgets::IsUIEditorNumberFieldPointInside;
|
||||||
|
using ::XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTarget;
|
||||||
|
using ::XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTargetKind;
|
||||||
|
using ::XCEngine::UI::Editor::Widgets::UIEditorNumberFieldLayout;
|
||||||
|
using ::XCEngine::UI::Editor::Widgets::UIEditorNumberFieldMetrics;
|
||||||
|
using ::XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsPermittedCharacter(const UIEditorNumberFieldSpec& 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>('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (!state.numberFieldState.editing) {
|
||||||
|
state.numberFieldState.displayText = FormatUIEditorNumberFieldValue(spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncHoverTarget(
|
||||||
|
UIEditorNumberFieldInteractionState& state,
|
||||||
|
const UIEditorNumberFieldLayout& layout) {
|
||||||
|
if (!state.hasPointerPosition) {
|
||||||
|
state.numberFieldState.hoveredTarget = UIEditorNumberFieldHitTargetKind::None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.numberFieldState.hoveredTarget =
|
||||||
|
HitTestUIEditorNumberField(layout, state.pointerPosition).kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BeginEdit(
|
||||||
|
UIEditorNumberFieldInteractionState& state,
|
||||||
|
const UIEditorNumberFieldSpec& spec,
|
||||||
|
bool clearText) {
|
||||||
|
if (spec.readOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string baseline = FormatUIEditorNumberFieldValue(spec);
|
||||||
|
const bool changed = state.editModel.BeginEdit(spec.fieldId, baseline);
|
||||||
|
if (!changed &&
|
||||||
|
state.editModel.HasActiveEdit() &&
|
||||||
|
state.editModel.GetActiveFieldId() != spec.fieldId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!changed && state.numberFieldState.editing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.numberFieldState.editing = true;
|
||||||
|
state.textInputState.value = clearText ? std::string() : baseline;
|
||||||
|
state.textInputState.caret = state.textInputState.value.size();
|
||||||
|
state.editModel.UpdateStagedValue(state.textInputState.value);
|
||||||
|
state.numberFieldState.displayText = state.textInputState.value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CommitEdit(
|
||||||
|
UIEditorNumberFieldInteractionState& state,
|
||||||
|
UIEditorNumberFieldSpec& spec,
|
||||||
|
UIEditorNumberFieldInteractionResult& result) {
|
||||||
|
if (!state.numberFieldState.editing || !state.editModel.HasActiveEdit()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double parsedValue = spec.value;
|
||||||
|
if (!TryParseValue(spec, state.textInputState.value, parsedValue)) {
|
||||||
|
result.consumed = true;
|
||||||
|
result.editCommitRejected = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.valueBefore = spec.value;
|
||||||
|
spec.value = parsedValue;
|
||||||
|
result.valueAfter = spec.value;
|
||||||
|
result.valueChanged = result.valueBefore != result.valueAfter;
|
||||||
|
result.editCommitted = true;
|
||||||
|
result.consumed = true;
|
||||||
|
result.committedText = FormatUIEditorNumberFieldValue(spec);
|
||||||
|
|
||||||
|
state.editModel.CommitEdit();
|
||||||
|
state.textInputState = {};
|
||||||
|
state.numberFieldState.editing = false;
|
||||||
|
state.numberFieldState.displayText = result.committedText;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CancelEdit(
|
||||||
|
UIEditorNumberFieldInteractionState& state,
|
||||||
|
const UIEditorNumberFieldSpec& spec,
|
||||||
|
UIEditorNumberFieldInteractionResult& result) {
|
||||||
|
if (!state.numberFieldState.editing || !state.editModel.HasActiveEdit()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.editModel.CancelEdit();
|
||||||
|
state.textInputState = {};
|
||||||
|
state.numberFieldState.editing = false;
|
||||||
|
state.numberFieldState.displayText = FormatUIEditorNumberFieldValue(spec);
|
||||||
|
result.consumed = true;
|
||||||
|
result.editCanceled = true;
|
||||||
|
result.valueBefore = spec.value;
|
||||||
|
result.valueAfter = spec.value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ApplyStep(
|
||||||
|
UIEditorNumberFieldInteractionState& state,
|
||||||
|
UIEditorNumberFieldSpec& spec,
|
||||||
|
double direction,
|
||||||
|
UIEditorNumberFieldInteractionResult& result) {
|
||||||
|
if (spec.readOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.numberFieldState.editing && !CommitEdit(state, spec, result)) {
|
||||||
|
return result.editCommitRejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double step = spec.step == 0.0 ? 1.0 : spec.step;
|
||||||
|
const double before = spec.value;
|
||||||
|
spec.value = NormalizeValue(spec, spec.value + step * direction);
|
||||||
|
|
||||||
|
result.consumed = true;
|
||||||
|
result.stepApplied = true;
|
||||||
|
result.valueBefore = before;
|
||||||
|
result.valueAfter = spec.value;
|
||||||
|
result.stepDelta = step * direction;
|
||||||
|
result.valueChanged = before != spec.value || result.valueChanged;
|
||||||
|
state.numberFieldState.displayText = FormatUIEditorNumberFieldValue(spec);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ApplyKeyboardStep(
|
||||||
|
UIEditorNumberFieldInteractionState& state,
|
||||||
|
UIEditorNumberFieldSpec& spec,
|
||||||
|
KeyCode keyCode,
|
||||||
|
UIEditorNumberFieldInteractionResult& result) {
|
||||||
|
switch (keyCode) {
|
||||||
|
case KeyCode::Left:
|
||||||
|
case KeyCode::Down:
|
||||||
|
return ApplyStep(state, spec, -1.0, result);
|
||||||
|
|
||||||
|
case KeyCode::Right:
|
||||||
|
case KeyCode::Up:
|
||||||
|
return ApplyStep(state, spec, 1.0, result);
|
||||||
|
|
||||||
|
case KeyCode::Home: {
|
||||||
|
if (spec.readOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double before = spec.value;
|
||||||
|
spec.value = NormalizeValue(spec, (std::min)(spec.minValue, spec.maxValue));
|
||||||
|
result.consumed = true;
|
||||||
|
result.stepApplied = true;
|
||||||
|
result.valueBefore = before;
|
||||||
|
result.valueAfter = spec.value;
|
||||||
|
result.valueChanged = before != spec.value;
|
||||||
|
state.numberFieldState.displayText = FormatUIEditorNumberFieldValue(spec);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case KeyCode::End: {
|
||||||
|
if (spec.readOnly) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double before = spec.value;
|
||||||
|
spec.value = NormalizeValue(spec, (std::max)(spec.minValue, spec.maxValue));
|
||||||
|
result.consumed = true;
|
||||||
|
result.stepApplied = true;
|
||||||
|
result.valueBefore = before;
|
||||||
|
result.valueAfter = spec.value;
|
||||||
|
result.valueChanged = before != spec.value;
|
||||||
|
state.numberFieldState.displayText = FormatUIEditorNumberFieldValue(spec);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
UIEditorNumberFieldInteractionFrame UpdateUIEditorNumberFieldInteraction(
|
||||||
|
UIEditorNumberFieldInteractionState& state,
|
||||||
|
UIEditorNumberFieldSpec& spec,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const std::vector<UIInputEvent>& inputEvents,
|
||||||
|
const UIEditorNumberFieldMetrics& metrics) {
|
||||||
|
UIEditorNumberFieldLayout layout = BuildUIEditorNumberFieldLayout(bounds, spec, metrics);
|
||||||
|
SyncDisplayText(state, spec);
|
||||||
|
SyncHoverTarget(state, layout);
|
||||||
|
|
||||||
|
UIEditorNumberFieldInteractionResult 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorNumberFieldInteractionResult eventResult = {};
|
||||||
|
switch (event.type) {
|
||||||
|
case UIInputEventType::FocusGained:
|
||||||
|
eventResult.focusChanged = !state.numberFieldState.focused;
|
||||||
|
state.numberFieldState.focused = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::FocusLost:
|
||||||
|
eventResult.focusChanged = state.numberFieldState.focused;
|
||||||
|
state.numberFieldState.focused = false;
|
||||||
|
state.numberFieldState.activeTarget = UIEditorNumberFieldHitTargetKind::None;
|
||||||
|
state.hasPointerPosition = false;
|
||||||
|
if (state.numberFieldState.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 UIEditorNumberFieldHitTarget hitTarget =
|
||||||
|
state.hasPointerPosition
|
||||||
|
? HitTestUIEditorNumberField(layout, state.pointerPosition)
|
||||||
|
: UIEditorNumberFieldHitTarget {};
|
||||||
|
eventResult.hitTarget = hitTarget;
|
||||||
|
|
||||||
|
if (event.pointerButton != UIPointerButton::Left) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool insideField =
|
||||||
|
state.hasPointerPosition &&
|
||||||
|
IsUIEditorNumberFieldPointInside(layout.bounds, state.pointerPosition);
|
||||||
|
if (insideField) {
|
||||||
|
eventResult.focusChanged = !state.numberFieldState.focused;
|
||||||
|
state.numberFieldState.focused = true;
|
||||||
|
state.numberFieldState.activeTarget =
|
||||||
|
hitTarget.kind == UIEditorNumberFieldHitTargetKind::None
|
||||||
|
? UIEditorNumberFieldHitTargetKind::Row
|
||||||
|
: hitTarget.kind;
|
||||||
|
eventResult.consumed = true;
|
||||||
|
} else {
|
||||||
|
if (state.numberFieldState.editing) {
|
||||||
|
CommitEdit(state, spec, eventResult);
|
||||||
|
if (!eventResult.editCommitRejected) {
|
||||||
|
eventResult.focusChanged = state.numberFieldState.focused;
|
||||||
|
state.numberFieldState.focused = false;
|
||||||
|
}
|
||||||
|
} else if (state.numberFieldState.focused) {
|
||||||
|
eventResult.focusChanged = true;
|
||||||
|
state.numberFieldState.focused = false;
|
||||||
|
}
|
||||||
|
state.numberFieldState.activeTarget = UIEditorNumberFieldHitTargetKind::None;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case UIInputEventType::PointerButtonUp: {
|
||||||
|
const UIEditorNumberFieldHitTarget hitTarget =
|
||||||
|
state.hasPointerPosition
|
||||||
|
? HitTestUIEditorNumberField(layout, state.pointerPosition)
|
||||||
|
: UIEditorNumberFieldHitTarget {};
|
||||||
|
eventResult.hitTarget = hitTarget;
|
||||||
|
|
||||||
|
if (event.pointerButton == UIPointerButton::Left) {
|
||||||
|
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 &&
|
||||||
|
hitTarget.kind == UIEditorNumberFieldHitTargetKind::ValueBox) {
|
||||||
|
if (!state.numberFieldState.editing) {
|
||||||
|
eventResult.editStarted = BeginEdit(state, spec, false);
|
||||||
|
}
|
||||||
|
eventResult.consumed = true;
|
||||||
|
} else if (hitTarget.kind == UIEditorNumberFieldHitTargetKind::Row) {
|
||||||
|
eventResult.consumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case UIInputEventType::KeyDown:
|
||||||
|
if (!state.numberFieldState.focused) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.numberFieldState.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.numberFieldState.displayText = state.textInputState.value;
|
||||||
|
eventResult.consumed = true;
|
||||||
|
if (textResult.valueChanged) {
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
ApplyKeyboardStep(
|
||||||
|
state,
|
||||||
|
spec,
|
||||||
|
static_cast<KeyCode>(event.keyCode),
|
||||||
|
eventResult);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::Character:
|
||||||
|
if (!state.numberFieldState.focused ||
|
||||||
|
spec.readOnly ||
|
||||||
|
event.modifiers.control ||
|
||||||
|
event.modifiers.alt ||
|
||||||
|
event.modifiers.super ||
|
||||||
|
!IsPermittedCharacter(spec, event.character)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.numberFieldState.editing) {
|
||||||
|
eventResult.editStarted = BeginEdit(state, spec, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InsertCharacter(state.textInputState, event.character)) {
|
||||||
|
state.editModel.UpdateStagedValue(state.textInputState.value);
|
||||||
|
state.numberFieldState.displayText = state.textInputState.value;
|
||||||
|
eventResult.consumed = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
layout = BuildUIEditorNumberFieldLayout(bounds, spec, metrics);
|
||||||
|
SyncDisplayText(state, spec);
|
||||||
|
SyncHoverTarget(state, layout);
|
||||||
|
if (eventResult.hitTarget.kind == UIEditorNumberFieldHitTargetKind::None &&
|
||||||
|
state.hasPointerPosition) {
|
||||||
|
eventResult.hitTarget = HitTestUIEditorNumberField(layout, state.pointerPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventResult.consumed ||
|
||||||
|
eventResult.focusChanged ||
|
||||||
|
eventResult.valueChanged ||
|
||||||
|
eventResult.stepApplied ||
|
||||||
|
eventResult.editStarted ||
|
||||||
|
eventResult.editCommitted ||
|
||||||
|
eventResult.editCommitRejected ||
|
||||||
|
eventResult.editCanceled ||
|
||||||
|
eventResult.hitTarget.kind != UIEditorNumberFieldHitTargetKind::None) {
|
||||||
|
interactionResult = std::move(eventResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout = BuildUIEditorNumberFieldLayout(bounds, spec, metrics);
|
||||||
|
SyncDisplayText(state, spec);
|
||||||
|
SyncHoverTarget(state, layout);
|
||||||
|
if (interactionResult.hitTarget.kind == UIEditorNumberFieldHitTargetKind::None &&
|
||||||
|
state.hasPointerPosition) {
|
||||||
|
interactionResult.hitTarget = HitTestUIEditorNumberField(layout, state.pointerPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
std::move(layout),
|
||||||
|
std::move(interactionResult)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor
|
||||||
133
new_editor/src/Widgets/UIEditorBoolField.cpp
Normal file
133
new_editor/src/Widgets/UIEditorBoolField.cpp
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
#include <XCEditor/Widgets/UIEditorBoolField.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::Widgets {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool ContainsPoint(const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIPoint& point) {
|
||||||
|
return point.x >= rect.x &&
|
||||||
|
point.x <= rect.x + rect.width &&
|
||||||
|
point.y >= rect.y &&
|
||||||
|
point.y <= rect.y + rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
UIEditorBoolFieldLayout BuildUIEditorBoolFieldLayout(
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const UIEditorBoolFieldSpec& spec,
|
||||||
|
const UIEditorBoolFieldMetrics& metrics) {
|
||||||
|
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);
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorBoolFieldHitTarget HitTestUIEditorBoolField(
|
||||||
|
const UIEditorBoolFieldLayout& layout,
|
||||||
|
const ::XCEngine::UI::UIPoint& point) {
|
||||||
|
if (!ContainsPoint(layout.bounds, point)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ContainsPoint(layout.toggleRect, point)) {
|
||||||
|
return { UIEditorBoolFieldHitTargetKind::Toggle };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { UIEditorBoolFieldHitTargetKind::Row };
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendUIEditorBoolFieldBackground(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const UIEditorBoolFieldLayout& layout,
|
||||||
|
const UIEditorBoolFieldSpec& spec,
|
||||||
|
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);
|
||||||
|
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,
|
||||||
|
metrics.borderThickness,
|
||||||
|
layout.toggleRect.height * 0.5f);
|
||||||
|
drawList.AddFilledRect(layout.knobRect, palette.knobColor, layout.knobRect.height * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendUIEditorBoolFieldForeground(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const UIEditorBoolFieldLayout& layout,
|
||||||
|
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.AddText(
|
||||||
|
::XCEngine::UI::UIPoint(
|
||||||
|
layout.toggleRect.x - 42.0f,
|
||||||
|
layout.bounds.y + metrics.textInsetY),
|
||||||
|
spec.value ? "On" : "Off",
|
||||||
|
palette.valueColor,
|
||||||
|
12.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendUIEditorBoolField(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const UIEditorBoolFieldSpec& spec,
|
||||||
|
const UIEditorBoolFieldState& state,
|
||||||
|
const UIEditorBoolFieldPalette& palette,
|
||||||
|
const UIEditorBoolFieldMetrics& metrics) {
|
||||||
|
const UIEditorBoolFieldLayout layout = BuildUIEditorBoolFieldLayout(bounds, spec, metrics);
|
||||||
|
AppendUIEditorBoolFieldBackground(drawList, layout, spec, state, palette, metrics);
|
||||||
|
AppendUIEditorBoolFieldForeground(drawList, layout, spec, palette, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::Widgets
|
||||||
184
new_editor/src/Widgets/UIEditorEnumField.cpp
Normal file
184
new_editor/src/Widgets/UIEditorEnumField.cpp
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::Widgets {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool ContainsPoint(const ::XCEngine::UI::UIRect& rect, const ::XCEngine::UI::UIPoint& point) {
|
||||||
|
return point.x >= rect.x &&
|
||||||
|
point.x <= rect.x + rect.width &&
|
||||||
|
point.y >= rect.y &&
|
||||||
|
point.y <= rect.y + rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t ClampSelectedIndex(const UIEditorEnumFieldSpec& spec) {
|
||||||
|
if (spec.options.empty()) {
|
||||||
|
return 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (std::min)(spec.selectedIndex, spec.options.size() - 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string ResolveUIEditorEnumFieldValueText(const UIEditorEnumFieldSpec& spec) {
|
||||||
|
if (spec.options.empty()) {
|
||||||
|
return "(none)";
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec.options[ClampSelectedIndex(spec)];
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorEnumFieldLayout BuildUIEditorEnumFieldLayout(
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const UIEditorEnumFieldSpec& spec,
|
||||||
|
const UIEditorEnumFieldMetrics& metrics) {
|
||||||
|
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);
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorEnumFieldHitTarget HitTestUIEditorEnumField(
|
||||||
|
const UIEditorEnumFieldLayout& layout,
|
||||||
|
const ::XCEngine::UI::UIPoint& point) {
|
||||||
|
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.valueRect, point)) {
|
||||||
|
return { UIEditorEnumFieldHitTargetKind::ValueBox };
|
||||||
|
}
|
||||||
|
return { UIEditorEnumFieldHitTargetKind::Row };
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendUIEditorEnumFieldBackground(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const UIEditorEnumFieldLayout& layout,
|
||||||
|
const UIEditorEnumFieldSpec& spec,
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendUIEditorEnumFieldForeground(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const UIEditorEnumFieldLayout& layout,
|
||||||
|
const UIEditorEnumFieldSpec& spec,
|
||||||
|
const UIEditorEnumFieldPalette& palette,
|
||||||
|
const UIEditorEnumFieldMetrics& metrics) {
|
||||||
|
drawList.PushClipRect(layout.labelRect);
|
||||||
|
drawList.AddText(
|
||||||
|
::XCEngine::UI::UIPoint(layout.labelRect.x, layout.labelRect.y + metrics.labelTextInsetY),
|
||||||
|
spec.label,
|
||||||
|
palette.labelColor,
|
||||||
|
12.0f);
|
||||||
|
drawList.PopClipRect();
|
||||||
|
|
||||||
|
drawList.PushClipRect(layout.valueRect);
|
||||||
|
drawList.AddText(
|
||||||
|
::XCEngine::UI::UIPoint(
|
||||||
|
layout.valueRect.x + metrics.valueTextInsetX,
|
||||||
|
layout.valueRect.y + metrics.valueTextInsetY),
|
||||||
|
ResolveUIEditorEnumFieldValueText(spec),
|
||||||
|
palette.valueColor,
|
||||||
|
12.0f);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendUIEditorEnumField(
|
||||||
|
::XCEngine::UI::UIDrawList& drawList,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
const UIEditorEnumFieldSpec& spec,
|
||||||
|
const UIEditorEnumFieldState& state,
|
||||||
|
const UIEditorEnumFieldPalette& palette,
|
||||||
|
const UIEditorEnumFieldMetrics& metrics) {
|
||||||
|
const UIEditorEnumFieldLayout layout = BuildUIEditorEnumFieldLayout(bounds, spec, metrics);
|
||||||
|
AppendUIEditorEnumFieldBackground(drawList, layout, spec, state, palette, metrics);
|
||||||
|
AppendUIEditorEnumFieldForeground(drawList, layout, spec, palette, metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::Widgets
|
||||||
284
new_editor/src/Widgets/UIEditorNumberField.cpp
Normal file
284
new_editor/src/Widgets/UIEditorNumberField.cpp
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FormatNumberValue(double value, bool integerMode) {
|
||||||
|
std::ostringstream stream = {};
|
||||||
|
if (integerMode) {
|
||||||
|
stream << static_cast<long long>(std::llround(value));
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
stream << std::fixed << std::setprecision(3) << value;
|
||||||
|
std::string text = stream.str();
|
||||||
|
while (!text.empty() && text.back() == '0') {
|
||||||
|
text.pop_back();
|
||||||
|
}
|
||||||
|
if (!text.empty() && text.back() == '.') {
|
||||||
|
text.pop_back();
|
||||||
|
}
|
||||||
|
return text.empty() ? "0" : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
::XCEngine::UI::UIColor ResolveRowFillColor(
|
||||||
|
const UIEditorNumberFieldState& state,
|
||||||
|
const UIEditorNumberFieldPalette& palette) {
|
||||||
|
if (state.activeTarget != UIEditorNumberFieldHitTargetKind::None) {
|
||||||
|
return palette.rowActiveColor;
|
||||||
|
}
|
||||||
|
if (state.hoveredTarget != UIEditorNumberFieldHitTargetKind::None) {
|
||||||
|
return palette.rowHoverColor;
|
||||||
|
}
|
||||||
|
return palette.surfaceColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
::XCEngine::UI::UIColor ResolveValueFillColor(
|
||||||
|
const UIEditorNumberFieldSpec& spec,
|
||||||
|
const UIEditorNumberFieldState& state,
|
||||||
|
const UIEditorNumberFieldPalette& palette) {
|
||||||
|
if (spec.readOnly) {
|
||||||
|
return palette.readOnlyColor;
|
||||||
|
}
|
||||||
|
if (state.editing) {
|
||||||
|
return palette.valueBoxEditingColor;
|
||||||
|
}
|
||||||
|
if (state.hoveredTarget == UIEditorNumberFieldHitTargetKind::ValueBox) {
|
||||||
|
return palette.valueBoxHoverColor;
|
||||||
|
}
|
||||||
|
return palette.valueBoxColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
::XCEngine::UI::UIColor ResolveButtonFillColor(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool IsUIEditorNumberFieldPointInside(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FormatUIEditorNumberFieldValue(const UIEditorNumberFieldSpec& spec) {
|
||||||
|
return FormatNumberValue(NormalizeRangeValue(spec, spec.value), spec.integerMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
if (IsUIEditorNumberFieldPointInside(layout.bounds, point)) {
|
||||||
|
return { UIEditorNumberFieldHitTargetKind::Row };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendUIEditorNumberFieldBackground(
|
||||||
|
UIDrawList& drawList,
|
||||||
|
const UIEditorNumberFieldLayout& layout,
|
||||||
|
const UIEditorNumberFieldSpec& spec,
|
||||||
|
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);
|
||||||
|
|
||||||
|
drawList.AddFilledRect(
|
||||||
|
layout.valueRect,
|
||||||
|
ResolveValueFillColor(spec, state, palette),
|
||||||
|
metrics.valueBoxRounding);
|
||||||
|
drawList.AddRectOutline(
|
||||||
|
layout.valueRect,
|
||||||
|
palette.controlBorderColor,
|
||||||
|
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 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendUIEditorNumberField(
|
||||||
|
UIDrawList& drawList,
|
||||||
|
const UIRect& bounds,
|
||||||
|
const UIEditorNumberFieldSpec& spec,
|
||||||
|
const UIEditorNumberFieldState& state,
|
||||||
|
const UIEditorNumberFieldPalette& palette,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::Widgets
|
||||||
@@ -58,6 +58,21 @@ if(TARGET editor_ui_property_grid_basic_validation)
|
|||||||
editor_ui_property_grid_basic_validation)
|
editor_ui_property_grid_basic_validation)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(TARGET editor_ui_bool_field_basic_validation)
|
||||||
|
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
|
||||||
|
editor_ui_bool_field_basic_validation)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET editor_ui_number_field_basic_validation)
|
||||||
|
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
|
||||||
|
editor_ui_number_field_basic_validation)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET editor_ui_enum_field_basic_validation)
|
||||||
|
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
|
||||||
|
editor_ui_enum_field_basic_validation)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(TARGET editor_ui_list_view_basic_validation)
|
if(TARGET editor_ui_list_view_basic_validation)
|
||||||
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
|
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
|
||||||
editor_ui_list_view_basic_validation)
|
editor_ui_list_view_basic_validation)
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ Layout:
|
|||||||
- `shell/panel_frame_basic/`: panel frame layout/state/hit-test only
|
- `shell/panel_frame_basic/`: panel frame layout/state/hit-test only
|
||||||
- `shell/scroll_view_basic/`: ScrollView viewport, clip, thumb drag, wheel offset only
|
- `shell/scroll_view_basic/`: ScrollView viewport, clip, thumb drag, wheel offset only
|
||||||
- `shell/property_grid_basic/`: PropertyGrid section toggle, field selection, value edit, keyboard navigation only
|
- `shell/property_grid_basic/`: PropertyGrid section toggle, field selection, value edit, keyboard navigation only
|
||||||
|
- `shell/bool_field_basic/`: BoolField click toggle, keyboard toggle, hover/focus/value feedback only
|
||||||
|
- `shell/number_field_basic/`: NumberField step buttons, direct text edit, Enter commit, Esc cancel only
|
||||||
|
- `shell/enum_field_basic/`: EnumField previous/next switch, keyboard switch, hover/focus/selection feedback only
|
||||||
- `shell/status_bar_basic/`: status bar slot/segment/hit-test only
|
- `shell/status_bar_basic/`: status bar slot/segment/hit-test only
|
||||||
- `shell/tree_view_basic/`: TreeView row layout, indent, disclosure, selection, focus, hit-test only
|
- `shell/tree_view_basic/`: TreeView row layout, indent, disclosure, selection, focus, hit-test only
|
||||||
- `shell/list_view_basic/`: ListView row layout, selection, focus, keyboard navigation, hit-test only
|
- `shell/list_view_basic/`: ListView row layout, selection, focus, keyboard navigation, hit-test only
|
||||||
@@ -90,6 +93,21 @@ Scenarios:
|
|||||||
Executable: `XCUIEditorPropertyGridBasicValidation.exe`
|
Executable: `XCUIEditorPropertyGridBasicValidation.exe`
|
||||||
Scope: PropertyGrid 基础控件验证;只检查 section toggle、field selection、value edit、Enter/Esc、keyboard navigation,不涉及业务 Inspector
|
Scope: PropertyGrid 基础控件验证;只检查 section toggle、field selection、value edit、Enter/Esc、keyboard navigation,不涉及业务 Inspector
|
||||||
|
|
||||||
|
- `editor.shell.bool_field_basic`
|
||||||
|
Build target: `editor_ui_bool_field_basic_validation`
|
||||||
|
Executable: `XCUIEditorBoolFieldBasicValidation.exe`
|
||||||
|
Scope: BoolField 基础控件契约;只验证点击切换、Space/Enter 切换、hover/focus/value/result 联动
|
||||||
|
|
||||||
|
- `editor.shell.number_field_basic`
|
||||||
|
Build target: `editor_ui_number_field_basic_validation`
|
||||||
|
Executable: `XCUIEditorNumberFieldBasicValidation.exe`
|
||||||
|
Scope: NumberField 基础控件契约;只验证步进按钮、直接字符编辑、Enter 提交、Esc 取消、hover/focus/result 联动
|
||||||
|
|
||||||
|
- `editor.shell.enum_field_basic`
|
||||||
|
Build target: `editor_ui_enum_field_basic_validation`
|
||||||
|
Executable: `XCUIEditorEnumFieldBasicValidation.exe`
|
||||||
|
Scope: EnumField 基础控件契约;只验证前后切换按钮、Left/Right/Home/End 键盘切换、hover/focus/result 联动
|
||||||
|
|
||||||
- `editor.shell.status_bar_basic`
|
- `editor.shell.status_bar_basic`
|
||||||
Build target: `editor_ui_status_bar_basic_validation`
|
Build target: `editor_ui_status_bar_basic_validation`
|
||||||
Executable: `XCUIEditorStatusBarBasicValidation.exe`
|
Executable: `XCUIEditorStatusBarBasicValidation.exe`
|
||||||
@@ -195,6 +213,12 @@ Selected controls:
|
|||||||
- `shell/scroll_view_basic/`
|
- `shell/scroll_view_basic/`
|
||||||
把鼠标移到右侧日志区内滚轮滚动,拖拽 scrollbar thumb,检查 `Hover / Focused / Thumb Dragging / Offset / Has Scrollbar / Result`,按 `重置`、`截图(F12)` 或直接按 `F12`。
|
把鼠标移到右侧日志区内滚轮滚动,拖拽 scrollbar thumb,检查 `Hover / Focused / Thumb Dragging / Offset / Has Scrollbar / Result`,按 `重置`、`截图(F12)` 或直接按 `F12`。
|
||||||
|
|
||||||
|
- `shell/bool_field_basic/`
|
||||||
|
先看顶部中文说明,再点击 `BoolField` 的 row 或 toggle;控件获得 focus 后按 `Space / Enter`,检查 `Hover / Focused / Value / Result` 是否同步更新,按 `重置`、`截图(F12)` 或直接按 `F12`。
|
||||||
|
- `shell/number_field_basic/`
|
||||||
|
先看顶部中文说明,再点击 `- / +` 检查步进;点击数值框后直接输入字符并按 `Enter` 提交,或按 `Esc` 取消,检查 `Hover / Focused / Editing / Value / Result`,按 `重置`、`截图(F12)` 或直接按 `F12`。
|
||||||
|
- `shell/enum_field_basic/`
|
||||||
|
先看顶部中文说明,再点击 `< / >` 切换选项;控件获得 focus 后按 `Left / Right / Home / End`,检查 `Hover / Focused / Selected / Result`,按 `重置`、`截图(F12)` 或直接按 `F12`。
|
||||||
- `shell/status_bar_basic/`
|
- `shell/status_bar_basic/`
|
||||||
Move the mouse across leading/trailing segments, click interactive segments, toggle focus/active, press `F12`.
|
Move the mouse across leading/trailing segments, click interactive segments, toggle focus/active, press `F12`.
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ endif()
|
|||||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/property_grid_basic/CMakeLists.txt")
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/property_grid_basic/CMakeLists.txt")
|
||||||
add_subdirectory(property_grid_basic)
|
add_subdirectory(property_grid_basic)
|
||||||
endif()
|
endif()
|
||||||
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/bool_field_basic/CMakeLists.txt")
|
||||||
|
add_subdirectory(bool_field_basic)
|
||||||
|
endif()
|
||||||
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/number_field_basic/CMakeLists.txt")
|
||||||
|
add_subdirectory(number_field_basic)
|
||||||
|
endif()
|
||||||
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/enum_field_basic/CMakeLists.txt")
|
||||||
|
add_subdirectory(enum_field_basic)
|
||||||
|
endif()
|
||||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tree_view_basic/CMakeLists.txt")
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tree_view_basic/CMakeLists.txt")
|
||||||
add_subdirectory(tree_view_basic)
|
add_subdirectory(tree_view_basic)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
add_executable(editor_ui_bool_field_basic_validation WIN32
|
||||||
|
main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(editor_ui_bool_field_basic_validation PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/new_editor/include
|
||||||
|
${CMAKE_SOURCE_DIR}/new_editor/app
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(editor_ui_bool_field_basic_validation PRIVATE
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(editor_ui_bool_field_basic_validation PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET editor_ui_bool_field_basic_validation PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(editor_ui_bool_field_basic_validation PRIVATE
|
||||||
|
XCUIEditorLib
|
||||||
|
XCUIEditorHost
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(editor_ui_bool_field_basic_validation PROPERTIES
|
||||||
|
OUTPUT_NAME "XCUIEditorBoolFieldBasicValidation"
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
670
tests/UI/Editor/integration/shell/bool_field_basic/main.cpp
Normal file
670
tests/UI/Editor/integration/shell/bool_field_basic/main.cpp
Normal file
@@ -0,0 +1,670 @@
|
|||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <XCEditor/Core/UIEditorBoolFieldInteraction.h>
|
||||||
|
#include <XCEditor/Widgets/UIEditorBoolField.h>
|
||||||
|
#include "Host/AutoScreenshot.h"
|
||||||
|
#include "Host/NativeRenderer.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
|
||||||
|
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Input::KeyCode;
|
||||||
|
using XCEngine::UI::UIColor;
|
||||||
|
using XCEngine::UI::UIDrawData;
|
||||||
|
using XCEngine::UI::UIDrawList;
|
||||||
|
using XCEngine::UI::UIInputEvent;
|
||||||
|
using XCEngine::UI::UIInputEventType;
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIPointerButton;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::Host::AutoScreenshotController;
|
||||||
|
using XCEngine::UI::Editor::Host::NativeRenderer;
|
||||||
|
using XCEngine::UI::Editor::UIEditorBoolFieldInteractionFrame;
|
||||||
|
using XCEngine::UI::Editor::UIEditorBoolFieldInteractionResult;
|
||||||
|
using XCEngine::UI::Editor::UIEditorBoolFieldInteractionState;
|
||||||
|
using XCEngine::UI::Editor::UpdateUIEditorBoolFieldInteraction;
|
||||||
|
using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolField;
|
||||||
|
using XCEngine::UI::Editor::Widgets::HitTestUIEditorBoolField;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTarget;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
|
||||||
|
|
||||||
|
constexpr const wchar_t* kWindowClassName = L"XCUIEditorBoolFieldBasicValidation";
|
||||||
|
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | BoolField Basic";
|
||||||
|
|
||||||
|
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
|
||||||
|
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
|
||||||
|
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f);
|
||||||
|
constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f);
|
||||||
|
constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f);
|
||||||
|
constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f);
|
||||||
|
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
|
||||||
|
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
|
||||||
|
|
||||||
|
enum class ActionId : unsigned char {
|
||||||
|
Reset = 0,
|
||||||
|
Capture
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ButtonLayout {
|
||||||
|
ActionId action = ActionId::Reset;
|
||||||
|
const char* label = "";
|
||||||
|
UIRect rect = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScenarioLayout {
|
||||||
|
UIRect introRect = {};
|
||||||
|
UIRect controlRect = {};
|
||||||
|
UIRect stateRect = {};
|
||||||
|
UIRect previewRect = {};
|
||||||
|
UIRect fieldRect = {};
|
||||||
|
std::vector<ButtonLayout> buttons = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::filesystem::path ResolveRepoRootPath() {
|
||||||
|
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
|
||||||
|
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||||
|
root = root.substr(1u, root.size() - 2u);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::filesystem::path(root).lexically_normal();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||||
|
return x >= rect.x &&
|
||||||
|
x <= rect.x + rect.width &&
|
||||||
|
y >= rect.y &&
|
||||||
|
y <= rect.y + rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int32_t MapBoolFieldKey(UINT keyCode) {
|
||||||
|
switch (keyCode) {
|
||||||
|
case VK_SPACE:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Space);
|
||||||
|
case VK_RETURN:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Enter);
|
||||||
|
default:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||||
|
constexpr float margin = 20.0f;
|
||||||
|
constexpr float leftWidth = 430.0f;
|
||||||
|
constexpr float gap = 16.0f;
|
||||||
|
|
||||||
|
ScenarioLayout layout = {};
|
||||||
|
layout.introRect = UIRect(margin, margin, leftWidth, 214.0f);
|
||||||
|
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
|
||||||
|
layout.stateRect = UIRect(
|
||||||
|
margin,
|
||||||
|
layout.controlRect.y + layout.controlRect.height + gap,
|
||||||
|
leftWidth,
|
||||||
|
(std::max)(220.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
|
||||||
|
layout.previewRect = UIRect(
|
||||||
|
leftWidth + margin * 2.0f,
|
||||||
|
margin,
|
||||||
|
(std::max)(420.0f, width - leftWidth - margin * 3.0f),
|
||||||
|
height - margin * 2.0f);
|
||||||
|
layout.fieldRect = UIRect(
|
||||||
|
layout.previewRect.x + 24.0f,
|
||||||
|
layout.previewRect.y + 82.0f,
|
||||||
|
260.0f,
|
||||||
|
32.0f);
|
||||||
|
|
||||||
|
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
|
||||||
|
const float buttonY = layout.controlRect.y + 32.0f;
|
||||||
|
layout.buttons = {
|
||||||
|
{ ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
|
||||||
|
{ ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
|
||||||
|
};
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawCard(
|
||||||
|
UIDrawList& drawList,
|
||||||
|
const UIRect& rect,
|
||||||
|
std::string_view title,
|
||||||
|
std::string_view subtitle = {}) {
|
||||||
|
drawList.AddFilledRect(rect, kCardBg, 10.0f);
|
||||||
|
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
|
||||||
|
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
|
||||||
|
if (!subtitle.empty()) {
|
||||||
|
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawButton(
|
||||||
|
UIDrawList& drawList,
|
||||||
|
const ButtonLayout& button,
|
||||||
|
bool hovered) {
|
||||||
|
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
|
||||||
|
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
|
||||||
|
button.label,
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DescribeHitTarget(const UIEditorBoolFieldHitTarget& hitTarget) {
|
||||||
|
switch (hitTarget.kind) {
|
||||||
|
case UIEditorBoolFieldHitTargetKind::Toggle:
|
||||||
|
return "toggle";
|
||||||
|
case UIEditorBoolFieldHitTargetKind::Row:
|
||||||
|
return "row";
|
||||||
|
case UIEditorBoolFieldHitTargetKind::None:
|
||||||
|
default:
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakePointerEvent(
|
||||||
|
UIInputEventType type,
|
||||||
|
const UIPoint& position,
|
||||||
|
UIPointerButton button = UIPointerButton::None) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
|
event.position = position;
|
||||||
|
event.pointerButton = button;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakeKeyEvent(std::int32_t keyCode) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::KeyDown;
|
||||||
|
event.keyCode = keyCode;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScenarioApp {
|
||||||
|
public:
|
||||||
|
int Run(HINSTANCE hInstance, int nCmdShow) {
|
||||||
|
if (!Initialize(hInstance, nCmdShow)) {
|
||||||
|
Shutdown();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
MSG message = {};
|
||||||
|
while (message.message != WM_QUIT) {
|
||||||
|
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
|
||||||
|
TranslateMessage(&message);
|
||||||
|
DispatchMessageW(&message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderFrame();
|
||||||
|
Sleep(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
Shutdown();
|
||||||
|
return static_cast<int>(message.wParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||||
|
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
|
||||||
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||||
|
switch (message) {
|
||||||
|
case WM_SIZE:
|
||||||
|
if (app != nullptr && wParam != SIZE_MINIMIZED) {
|
||||||
|
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_MOUSEMOVE:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleMouseMove(
|
||||||
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_MOUSELEAVE:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleMouseLeave();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_LBUTTONDOWN:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleLeftButtonDown(
|
||||||
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_LBUTTONUP:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleLeftButtonUp(
|
||||||
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_KEYDOWN:
|
||||||
|
case WM_SYSKEYDOWN:
|
||||||
|
if (app != nullptr) {
|
||||||
|
if (wParam == VK_F12) {
|
||||||
|
app->m_autoScreenshot.RequestCapture("manual_f12");
|
||||||
|
app->m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||||
|
InvalidateRect(hwnd, nullptr, FALSE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::int32_t keyCode = MapBoolFieldKey(static_cast<UINT>(wParam));
|
||||||
|
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
|
||||||
|
app->HandleKeyDown(keyCode);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_PAINT:
|
||||||
|
if (app != nullptr) {
|
||||||
|
PAINTSTRUCT paintStruct = {};
|
||||||
|
BeginPaint(hwnd, &paintStruct);
|
||||||
|
app->RenderFrame();
|
||||||
|
EndPaint(hwnd, &paintStruct);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_ERASEBKGND:
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
case WM_DESTROY:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->m_hwnd = nullptr;
|
||||||
|
}
|
||||||
|
PostQuitMessage(0);
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||||
|
WNDCLASSEXW windowClass = {};
|
||||||
|
windowClass.cbSize = sizeof(windowClass);
|
||||||
|
windowClass.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
windowClass.lpfnWndProc = &ScenarioApp::WndProc;
|
||||||
|
windowClass.hInstance = hInstance;
|
||||||
|
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||||
|
windowClass.lpszClassName = kWindowClassName;
|
||||||
|
|
||||||
|
m_windowClassAtom = RegisterClassExW(&windowClass);
|
||||||
|
if (m_windowClassAtom == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hwnd = CreateWindowExW(
|
||||||
|
0,
|
||||||
|
kWindowClassName,
|
||||||
|
kWindowTitle,
|
||||||
|
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
1480,
|
||||||
|
920,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
hInstance,
|
||||||
|
this);
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowWindow(m_hwnd, nCmdShow);
|
||||||
|
UpdateWindow(m_hwnd);
|
||||||
|
|
||||||
|
if (!m_renderer.Initialize(m_hwnd)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_captureRoot =
|
||||||
|
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/bool_field_basic/captures";
|
||||||
|
m_autoScreenshot.Initialize(m_captureRoot);
|
||||||
|
|
||||||
|
ResetScenario();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shutdown() {
|
||||||
|
m_autoScreenshot.Shutdown();
|
||||||
|
m_renderer.Shutdown();
|
||||||
|
|
||||||
|
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
|
||||||
|
DestroyWindow(m_hwnd);
|
||||||
|
}
|
||||||
|
m_hwnd = nullptr;
|
||||||
|
|
||||||
|
if (m_windowClassAtom != 0) {
|
||||||
|
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
|
||||||
|
m_windowClassAtom = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScenarioLayout GetLayout() const {
|
||||||
|
RECT clientRect = {};
|
||||||
|
GetClientRect(m_hwnd, &clientRect);
|
||||||
|
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||||
|
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||||
|
return BuildScenarioLayout(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetScenario() {
|
||||||
|
m_value = false;
|
||||||
|
m_spec = {};
|
||||||
|
m_spec.fieldId = "enabled";
|
||||||
|
m_spec.label = "Enabled";
|
||||||
|
m_interactionState = {};
|
||||||
|
m_interactionState.fieldState.focused = true;
|
||||||
|
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||||
|
m_hoveredAction = ActionId::Reset;
|
||||||
|
m_hasHoveredAction = false;
|
||||||
|
m_lastResult = "已重置到默认 BoolField 状态";
|
||||||
|
RefreshFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefreshFrame() {
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
m_frame = UpdateUIEditorBoolFieldInteraction(
|
||||||
|
m_interactionState,
|
||||||
|
m_value,
|
||||||
|
layout.fieldRect,
|
||||||
|
m_spec,
|
||||||
|
{});
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnResize(UINT width, UINT height) {
|
||||||
|
if (width == 0u || height == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_renderer.Resize(width, height);
|
||||||
|
RefreshFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleMouseMove(float x, float y) {
|
||||||
|
m_mousePosition = UIPoint(x, y);
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
UpdateHoveredAction(layout, x, y);
|
||||||
|
|
||||||
|
TRACKMOUSEEVENT trackEvent = {};
|
||||||
|
trackEvent.cbSize = sizeof(trackEvent);
|
||||||
|
trackEvent.dwFlags = TME_LEAVE;
|
||||||
|
trackEvent.hwndTrack = m_hwnd;
|
||||||
|
TrackMouseEvent(&trackEvent);
|
||||||
|
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleMouseLeave() {
|
||||||
|
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||||
|
m_hasHoveredAction = false;
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleLeftButtonDown(float x, float y) {
|
||||||
|
m_mousePosition = UIPoint(x, y);
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
if (HitTestAction(layout, x, y) != nullptr) {
|
||||||
|
UpdateHoveredAction(layout, x, y);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleLeftButtonUp(float x, float y) {
|
||||||
|
m_mousePosition = UIPoint(x, y);
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||||
|
if (button != nullptr) {
|
||||||
|
ExecuteAction(button->action);
|
||||||
|
UpdateHoveredAction(layout, x, y);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIEditorBoolFieldInteractionResult result =
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
|
||||||
|
UpdateResultText(result);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleKeyDown(std::int32_t keyCode) {
|
||||||
|
const UIEditorBoolFieldInteractionResult result =
|
||||||
|
PumpEvents({ MakeKeyEvent(keyCode) });
|
||||||
|
UpdateResultText(result);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
|
||||||
|
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||||
|
if (button == nullptr) {
|
||||||
|
m_hasHoveredAction = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hoveredAction = button->action;
|
||||||
|
m_hasHoveredAction = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const {
|
||||||
|
for (const ButtonLayout& button : layout.buttons) {
|
||||||
|
if (ContainsPoint(button.rect, x, y)) {
|
||||||
|
return &button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorBoolFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
m_frame = UpdateUIEditorBoolFieldInteraction(
|
||||||
|
m_interactionState,
|
||||||
|
m_value,
|
||||||
|
layout.fieldRect,
|
||||||
|
m_spec,
|
||||||
|
std::move(events));
|
||||||
|
return m_frame.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateResultText(const UIEditorBoolFieldInteractionResult& result) {
|
||||||
|
if (result.valueChanged) {
|
||||||
|
m_lastResult = std::string("值已切换到 ") + (m_value ? "true" : "false");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.consumed) {
|
||||||
|
m_lastResult = "控件已消费输入";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_lastResult = "等待交互";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExecuteAction(ActionId action) {
|
||||||
|
switch (action) {
|
||||||
|
case ActionId::Reset:
|
||||||
|
ResetScenario();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ActionId::Capture:
|
||||||
|
m_autoScreenshot.RequestCapture("manual_button");
|
||||||
|
m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderFrame() {
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT clientRect = {};
|
||||||
|
GetClientRect(m_hwnd, &clientRect);
|
||||||
|
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||||
|
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||||
|
const ScenarioLayout layout = BuildScenarioLayout(width, height);
|
||||||
|
RefreshFrame();
|
||||||
|
|
||||||
|
const UIEditorBoolFieldHitTarget currentHit =
|
||||||
|
HitTestUIEditorBoolField(m_frame.layout, m_mousePosition);
|
||||||
|
|
||||||
|
UIDrawData drawData = {};
|
||||||
|
UIDrawList& drawList = drawData.EmplaceDrawList("EditorBoolFieldBasic");
|
||||||
|
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
|
||||||
|
|
||||||
|
DrawCard(
|
||||||
|
drawList,
|
||||||
|
layout.introRect,
|
||||||
|
"这个测试在验证什么功能",
|
||||||
|
"只验证 Editor BoolField 的基础交互契约,不涉及 PropertyGrid 或任何业务 Inspector。");
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
|
||||||
|
"1. 点击 row 或 toggle,检查 true / false 是否稳定切换。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
||||||
|
"2. 控件获得 focus 后按 Space / Enter,也必须能切换值。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
|
||||||
|
"3. 检查 Hover / Focus / Value / Result 是否同步更新。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
|
||||||
|
"4. 按 F12 或点击截图按钮,确认自动截图路径正确。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
|
||||||
|
DrawCard(drawList, layout.controlRect, "操作");
|
||||||
|
for (const ButtonLayout& button : layout.buttons) {
|
||||||
|
DrawButton(
|
||||||
|
drawList,
|
||||||
|
button,
|
||||||
|
m_hasHoveredAction && m_hoveredAction == button.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawCard(drawList, layout.stateRect, "状态摘要", "重点检查 hit / focus / value / result。");
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
|
||||||
|
"Hover: " + DescribeHitTarget(currentHit),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
|
||||||
|
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "开" : "关"),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
|
||||||
|
std::string("Active: ") + (m_interactionState.fieldState.active ? "开" : "关"),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
|
||||||
|
std::string("Value: ") + (m_value ? "true" : "false"),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
|
||||||
|
"Result: " + m_lastResult,
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
|
||||||
|
const std::string captureSummary =
|
||||||
|
m_autoScreenshot.HasPendingCapture()
|
||||||
|
? "截图排队中..."
|
||||||
|
: (m_autoScreenshot.GetLastCaptureSummary().empty()
|
||||||
|
? std::string("F12 -> tests/UI/Editor/integration/shell/bool_field_basic/captures/")
|
||||||
|
: m_autoScreenshot.GetLastCaptureSummary());
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
|
||||||
|
captureSummary,
|
||||||
|
kTextWeak,
|
||||||
|
12.0f);
|
||||||
|
|
||||||
|
DrawCard(drawList, layout.previewRect, "BoolField 预览", "这里只放一个 BoolField。");
|
||||||
|
UIEditorBoolFieldSpec previewSpec = m_spec;
|
||||||
|
previewSpec.value = m_value;
|
||||||
|
AppendUIEditorBoolField(
|
||||||
|
drawList,
|
||||||
|
layout.fieldRect,
|
||||||
|
previewSpec,
|
||||||
|
m_interactionState.fieldState);
|
||||||
|
|
||||||
|
const bool framePresented = m_renderer.Render(drawData);
|
||||||
|
m_autoScreenshot.CaptureIfRequested(
|
||||||
|
m_renderer,
|
||||||
|
drawData,
|
||||||
|
static_cast<unsigned int>(width),
|
||||||
|
static_cast<unsigned int>(height),
|
||||||
|
framePresented);
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND m_hwnd = nullptr;
|
||||||
|
ATOM m_windowClassAtom = 0;
|
||||||
|
NativeRenderer m_renderer = {};
|
||||||
|
AutoScreenshotController m_autoScreenshot = {};
|
||||||
|
std::filesystem::path m_captureRoot = {};
|
||||||
|
UIEditorBoolFieldSpec m_spec = {};
|
||||||
|
bool m_value = false;
|
||||||
|
UIEditorBoolFieldInteractionState m_interactionState = {};
|
||||||
|
UIEditorBoolFieldInteractionFrame m_frame = {};
|
||||||
|
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||||
|
ActionId m_hoveredAction = ActionId::Reset;
|
||||||
|
bool m_hasHoveredAction = false;
|
||||||
|
std::string m_lastResult = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||||
|
return ScenarioApp().Run(hInstance, nCmdShow);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
add_executable(editor_ui_enum_field_basic_validation WIN32
|
||||||
|
main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(editor_ui_enum_field_basic_validation PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/new_editor/include
|
||||||
|
${CMAKE_SOURCE_DIR}/new_editor/app
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(editor_ui_enum_field_basic_validation PRIVATE
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(editor_ui_enum_field_basic_validation PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET editor_ui_enum_field_basic_validation PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(editor_ui_enum_field_basic_validation PRIVATE
|
||||||
|
XCUIEditorLib
|
||||||
|
XCUIEditorHost
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(editor_ui_enum_field_basic_validation PROPERTIES
|
||||||
|
OUTPUT_NAME "XCUIEditorEnumFieldBasicValidation"
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
673
tests/UI/Editor/integration/shell/enum_field_basic/main.cpp
Normal file
673
tests/UI/Editor/integration/shell/enum_field_basic/main.cpp
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <XCEditor/Core/UIEditorEnumFieldInteraction.h>
|
||||||
|
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
||||||
|
#include "Host/AutoScreenshot.h"
|
||||||
|
#include "Host/NativeRenderer.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
|
||||||
|
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Input::KeyCode;
|
||||||
|
using XCEngine::UI::UIColor;
|
||||||
|
using XCEngine::UI::UIDrawData;
|
||||||
|
using XCEngine::UI::UIDrawList;
|
||||||
|
using XCEngine::UI::UIInputEvent;
|
||||||
|
using XCEngine::UI::UIInputEventType;
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIPointerButton;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::Host::AutoScreenshotController;
|
||||||
|
using XCEngine::UI::Editor::Host::NativeRenderer;
|
||||||
|
using XCEngine::UI::Editor::UIEditorEnumFieldInteractionFrame;
|
||||||
|
using XCEngine::UI::Editor::UIEditorEnumFieldInteractionResult;
|
||||||
|
using XCEngine::UI::Editor::UIEditorEnumFieldInteractionState;
|
||||||
|
using XCEngine::UI::Editor::UpdateUIEditorEnumFieldInteraction;
|
||||||
|
using XCEngine::UI::Editor::Widgets::AppendUIEditorEnumField;
|
||||||
|
using XCEngine::UI::Editor::Widgets::HitTestUIEditorEnumField;
|
||||||
|
using XCEngine::UI::Editor::Widgets::ResolveUIEditorEnumFieldValueText;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTarget;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
|
||||||
|
|
||||||
|
constexpr const wchar_t* kWindowClassName = L"XCUIEditorEnumFieldBasicValidation";
|
||||||
|
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | EnumField Basic";
|
||||||
|
|
||||||
|
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
|
||||||
|
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
|
||||||
|
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f);
|
||||||
|
constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f);
|
||||||
|
constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f);
|
||||||
|
constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f);
|
||||||
|
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
|
||||||
|
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
|
||||||
|
|
||||||
|
enum class ActionId : unsigned char {
|
||||||
|
Reset = 0,
|
||||||
|
Capture
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ButtonLayout {
|
||||||
|
ActionId action = ActionId::Reset;
|
||||||
|
const char* label = "";
|
||||||
|
UIRect rect = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScenarioLayout {
|
||||||
|
UIRect introRect = {};
|
||||||
|
UIRect controlRect = {};
|
||||||
|
UIRect stateRect = {};
|
||||||
|
UIRect previewRect = {};
|
||||||
|
UIRect fieldRect = {};
|
||||||
|
std::vector<ButtonLayout> buttons = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::filesystem::path ResolveRepoRootPath() {
|
||||||
|
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
|
||||||
|
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||||
|
root = root.substr(1u, root.size() - 2u);
|
||||||
|
}
|
||||||
|
return std::filesystem::path(root).lexically_normal();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||||
|
return x >= rect.x &&
|
||||||
|
x <= rect.x + rect.width &&
|
||||||
|
y >= rect.y &&
|
||||||
|
y <= rect.y + rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int32_t MapEnumFieldKey(UINT keyCode) {
|
||||||
|
switch (keyCode) {
|
||||||
|
case VK_LEFT:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Left);
|
||||||
|
case VK_RIGHT:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Right);
|
||||||
|
case VK_HOME:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Home);
|
||||||
|
case VK_END:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::End);
|
||||||
|
case VK_RETURN:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Enter);
|
||||||
|
default:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||||
|
constexpr float margin = 20.0f;
|
||||||
|
constexpr float leftWidth = 430.0f;
|
||||||
|
constexpr float gap = 16.0f;
|
||||||
|
|
||||||
|
ScenarioLayout layout = {};
|
||||||
|
layout.introRect = UIRect(margin, margin, leftWidth, 220.0f);
|
||||||
|
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
|
||||||
|
layout.stateRect = UIRect(
|
||||||
|
margin,
|
||||||
|
layout.controlRect.y + layout.controlRect.height + gap,
|
||||||
|
leftWidth,
|
||||||
|
(std::max)(220.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
|
||||||
|
layout.previewRect = UIRect(
|
||||||
|
leftWidth + margin * 2.0f,
|
||||||
|
margin,
|
||||||
|
(std::max)(420.0f, width - leftWidth - margin * 3.0f),
|
||||||
|
height - margin * 2.0f);
|
||||||
|
layout.fieldRect = UIRect(
|
||||||
|
layout.previewRect.x + 24.0f,
|
||||||
|
layout.previewRect.y + 82.0f,
|
||||||
|
320.0f,
|
||||||
|
32.0f);
|
||||||
|
|
||||||
|
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
|
||||||
|
const float buttonY = layout.controlRect.y + 32.0f;
|
||||||
|
layout.buttons = {
|
||||||
|
{ ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
|
||||||
|
{ ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
|
||||||
|
};
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawCard(
|
||||||
|
UIDrawList& drawList,
|
||||||
|
const UIRect& rect,
|
||||||
|
std::string_view title,
|
||||||
|
std::string_view subtitle = {}) {
|
||||||
|
drawList.AddFilledRect(rect, kCardBg, 10.0f);
|
||||||
|
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
|
||||||
|
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
|
||||||
|
if (!subtitle.empty()) {
|
||||||
|
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawButton(
|
||||||
|
UIDrawList& drawList,
|
||||||
|
const ButtonLayout& button,
|
||||||
|
bool hovered) {
|
||||||
|
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
|
||||||
|
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
|
||||||
|
drawList.AddText(UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DescribeHitTarget(const UIEditorEnumFieldHitTarget& hitTarget) {
|
||||||
|
switch (hitTarget.kind) {
|
||||||
|
case UIEditorEnumFieldHitTargetKind::PreviousButton:
|
||||||
|
return "previous";
|
||||||
|
case UIEditorEnumFieldHitTargetKind::NextButton:
|
||||||
|
return "next";
|
||||||
|
case UIEditorEnumFieldHitTargetKind::ValueBox:
|
||||||
|
return "value_box";
|
||||||
|
case UIEditorEnumFieldHitTargetKind::Row:
|
||||||
|
return "row";
|
||||||
|
case UIEditorEnumFieldHitTargetKind::None:
|
||||||
|
default:
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakePointerEvent(
|
||||||
|
UIInputEventType type,
|
||||||
|
const UIPoint& position,
|
||||||
|
UIPointerButton button = UIPointerButton::None) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
|
event.position = position;
|
||||||
|
event.pointerButton = button;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakeKeyEvent(std::int32_t keyCode) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::KeyDown;
|
||||||
|
event.keyCode = keyCode;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScenarioApp {
|
||||||
|
public:
|
||||||
|
int Run(HINSTANCE hInstance, int nCmdShow) {
|
||||||
|
if (!Initialize(hInstance, nCmdShow)) {
|
||||||
|
Shutdown();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
MSG message = {};
|
||||||
|
while (message.message != WM_QUIT) {
|
||||||
|
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
|
||||||
|
TranslateMessage(&message);
|
||||||
|
DispatchMessageW(&message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderFrame();
|
||||||
|
Sleep(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
Shutdown();
|
||||||
|
return static_cast<int>(message.wParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||||
|
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
|
||||||
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||||
|
switch (message) {
|
||||||
|
case WM_SIZE:
|
||||||
|
if (app != nullptr && wParam != SIZE_MINIMIZED) {
|
||||||
|
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_MOUSEMOVE:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleMouseMove(
|
||||||
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_MOUSELEAVE:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleMouseLeave();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_LBUTTONDOWN:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleLeftButtonDown(
|
||||||
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_LBUTTONUP:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleLeftButtonUp(
|
||||||
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_KEYDOWN:
|
||||||
|
case WM_SYSKEYDOWN:
|
||||||
|
if (app != nullptr) {
|
||||||
|
if (wParam == VK_F12) {
|
||||||
|
app->m_autoScreenshot.RequestCapture("manual_f12");
|
||||||
|
app->m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||||
|
InvalidateRect(hwnd, nullptr, FALSE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::int32_t keyCode = MapEnumFieldKey(static_cast<UINT>(wParam));
|
||||||
|
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
|
||||||
|
app->HandleKeyDown(keyCode);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_PAINT:
|
||||||
|
if (app != nullptr) {
|
||||||
|
PAINTSTRUCT paintStruct = {};
|
||||||
|
BeginPaint(hwnd, &paintStruct);
|
||||||
|
app->RenderFrame();
|
||||||
|
EndPaint(hwnd, &paintStruct);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_ERASEBKGND:
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
case WM_DESTROY:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->m_hwnd = nullptr;
|
||||||
|
}
|
||||||
|
PostQuitMessage(0);
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||||
|
WNDCLASSEXW windowClass = {};
|
||||||
|
windowClass.cbSize = sizeof(windowClass);
|
||||||
|
windowClass.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
windowClass.lpfnWndProc = &ScenarioApp::WndProc;
|
||||||
|
windowClass.hInstance = hInstance;
|
||||||
|
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||||
|
windowClass.lpszClassName = kWindowClassName;
|
||||||
|
|
||||||
|
m_windowClassAtom = RegisterClassExW(&windowClass);
|
||||||
|
if (m_windowClassAtom == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hwnd = CreateWindowExW(
|
||||||
|
0,
|
||||||
|
kWindowClassName,
|
||||||
|
kWindowTitle,
|
||||||
|
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
1480,
|
||||||
|
920,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
hInstance,
|
||||||
|
this);
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowWindow(m_hwnd, nCmdShow);
|
||||||
|
UpdateWindow(m_hwnd);
|
||||||
|
|
||||||
|
if (!m_renderer.Initialize(m_hwnd)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_captureRoot =
|
||||||
|
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/enum_field_basic/captures";
|
||||||
|
m_autoScreenshot.Initialize(m_captureRoot);
|
||||||
|
|
||||||
|
ResetScenario();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shutdown() {
|
||||||
|
m_autoScreenshot.Shutdown();
|
||||||
|
m_renderer.Shutdown();
|
||||||
|
|
||||||
|
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
|
||||||
|
DestroyWindow(m_hwnd);
|
||||||
|
}
|
||||||
|
m_hwnd = nullptr;
|
||||||
|
|
||||||
|
if (m_windowClassAtom != 0) {
|
||||||
|
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
|
||||||
|
m_windowClassAtom = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScenarioLayout GetLayout() const {
|
||||||
|
RECT clientRect = {};
|
||||||
|
GetClientRect(m_hwnd, &clientRect);
|
||||||
|
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||||
|
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||||
|
return BuildScenarioLayout(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetScenario() {
|
||||||
|
m_spec = {};
|
||||||
|
m_spec.fieldId = "render_mode";
|
||||||
|
m_spec.label = "Render Mode";
|
||||||
|
m_spec.options = { "Opaque", "Cutout", "Fade", "Transparent" };
|
||||||
|
m_selectedIndex = 1u;
|
||||||
|
m_spec.selectedIndex = m_selectedIndex;
|
||||||
|
m_interactionState = {};
|
||||||
|
m_interactionState.fieldState.focused = true;
|
||||||
|
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||||
|
m_hoveredAction = ActionId::Reset;
|
||||||
|
m_hasHoveredAction = false;
|
||||||
|
m_lastResult = "已重置到默认 EnumField 状态";
|
||||||
|
RefreshFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefreshFrame() {
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_spec.selectedIndex = m_selectedIndex;
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
m_frame = UpdateUIEditorEnumFieldInteraction(
|
||||||
|
m_interactionState,
|
||||||
|
m_selectedIndex,
|
||||||
|
layout.fieldRect,
|
||||||
|
m_spec,
|
||||||
|
{});
|
||||||
|
m_spec.selectedIndex = m_selectedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnResize(UINT width, UINT height) {
|
||||||
|
if (width == 0u || height == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_renderer.Resize(width, height);
|
||||||
|
RefreshFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleMouseMove(float x, float y) {
|
||||||
|
m_mousePosition = UIPoint(x, y);
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
UpdateHoveredAction(layout, x, y);
|
||||||
|
|
||||||
|
TRACKMOUSEEVENT trackEvent = {};
|
||||||
|
trackEvent.cbSize = sizeof(trackEvent);
|
||||||
|
trackEvent.dwFlags = TME_LEAVE;
|
||||||
|
trackEvent.hwndTrack = m_hwnd;
|
||||||
|
TrackMouseEvent(&trackEvent);
|
||||||
|
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleMouseLeave() {
|
||||||
|
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||||
|
m_hasHoveredAction = false;
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleLeftButtonDown(float x, float y) {
|
||||||
|
m_mousePosition = UIPoint(x, y);
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
if (HitTestAction(layout, x, y) != nullptr) {
|
||||||
|
UpdateHoveredAction(layout, x, y);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleLeftButtonUp(float x, float y) {
|
||||||
|
m_mousePosition = UIPoint(x, y);
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||||
|
if (button != nullptr) {
|
||||||
|
ExecuteAction(button->action);
|
||||||
|
UpdateHoveredAction(layout, x, y);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIEditorEnumFieldInteractionResult result =
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
|
||||||
|
UpdateResultText(result);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleKeyDown(std::int32_t keyCode) {
|
||||||
|
const UIEditorEnumFieldInteractionResult result =
|
||||||
|
PumpEvents({ MakeKeyEvent(keyCode) });
|
||||||
|
UpdateResultText(result);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
|
||||||
|
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||||
|
if (button == nullptr) {
|
||||||
|
m_hasHoveredAction = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hoveredAction = button->action;
|
||||||
|
m_hasHoveredAction = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const {
|
||||||
|
for (const ButtonLayout& button : layout.buttons) {
|
||||||
|
if (ContainsPoint(button.rect, x, y)) {
|
||||||
|
return &button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorEnumFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
m_spec.selectedIndex = m_selectedIndex;
|
||||||
|
m_frame = UpdateUIEditorEnumFieldInteraction(
|
||||||
|
m_interactionState,
|
||||||
|
m_selectedIndex,
|
||||||
|
layout.fieldRect,
|
||||||
|
m_spec,
|
||||||
|
std::move(events));
|
||||||
|
m_spec.selectedIndex = m_selectedIndex;
|
||||||
|
return m_frame.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateResultText(const UIEditorEnumFieldInteractionResult& result) {
|
||||||
|
if (result.selectionChanged && m_selectedIndex < m_spec.options.size()) {
|
||||||
|
m_lastResult = std::string("已切换到: ") + m_spec.options[m_selectedIndex];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.consumed) {
|
||||||
|
m_lastResult = "控件已消费输入";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_lastResult = "等待交互";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExecuteAction(ActionId action) {
|
||||||
|
switch (action) {
|
||||||
|
case ActionId::Reset:
|
||||||
|
ResetScenario();
|
||||||
|
break;
|
||||||
|
case ActionId::Capture:
|
||||||
|
m_autoScreenshot.RequestCapture("manual_button");
|
||||||
|
m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderFrame() {
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT clientRect = {};
|
||||||
|
GetClientRect(m_hwnd, &clientRect);
|
||||||
|
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||||
|
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||||
|
const ScenarioLayout layout = BuildScenarioLayout(width, height);
|
||||||
|
RefreshFrame();
|
||||||
|
|
||||||
|
const UIEditorEnumFieldHitTarget currentHit =
|
||||||
|
HitTestUIEditorEnumField(m_frame.layout, m_mousePosition);
|
||||||
|
|
||||||
|
UIDrawData drawData = {};
|
||||||
|
UIDrawList& drawList = drawData.EmplaceDrawList("EditorEnumFieldBasic");
|
||||||
|
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
|
||||||
|
|
||||||
|
DrawCard(
|
||||||
|
drawList,
|
||||||
|
layout.introRect,
|
||||||
|
"这个测试在验证什么功能",
|
||||||
|
"只验证 Editor EnumField 基础控件,不涉及 PropertyGrid 或业务 Inspector。");
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
|
||||||
|
"1. 点击 < / > 按钮,检查枚举选项是否稳定切换。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
||||||
|
"2. 控件获得 focus 后按 Left / Right / Home / End,检查键盘切换。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
|
||||||
|
"3. 检查 Hover / Focus / Selected / Result 是否同步更新。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
|
||||||
|
"4. 这个场景只覆盖基础交互契约,不提前承载任何业务面板。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
|
||||||
|
DrawCard(drawList, layout.controlRect, "操作");
|
||||||
|
for (const ButtonLayout& button : layout.buttons) {
|
||||||
|
DrawButton(drawList, button, m_hasHoveredAction && m_hoveredAction == button.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawCard(drawList, layout.stateRect, "状态摘要");
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
|
||||||
|
"Hover: " + DescribeHitTarget(currentHit),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
|
||||||
|
std::string("Focused: ") + (m_interactionState.fieldState.focused ? "是" : "否"),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
|
||||||
|
std::string("Active: ") + (m_interactionState.fieldState.active ? "是" : "否"),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
|
||||||
|
"Selected: " + ResolveUIEditorEnumFieldValueText(m_spec),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
|
||||||
|
"Result: " + m_lastResult,
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
|
||||||
|
const std::string captureSummary =
|
||||||
|
m_autoScreenshot.HasPendingCapture()
|
||||||
|
? "截图排队中..."
|
||||||
|
: (m_autoScreenshot.GetLastCaptureSummary().empty()
|
||||||
|
? std::string("F12 -> tests/UI/Editor/integration/shell/enum_field_basic/captures/")
|
||||||
|
: m_autoScreenshot.GetLastCaptureSummary());
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
|
||||||
|
captureSummary,
|
||||||
|
kTextWeak,
|
||||||
|
12.0f);
|
||||||
|
|
||||||
|
DrawCard(drawList, layout.previewRect, "EnumField 预览", "这里只放一个 EnumField。");
|
||||||
|
AppendUIEditorEnumField(
|
||||||
|
drawList,
|
||||||
|
layout.fieldRect,
|
||||||
|
m_spec,
|
||||||
|
m_interactionState.fieldState);
|
||||||
|
|
||||||
|
const bool framePresented = m_renderer.Render(drawData);
|
||||||
|
m_autoScreenshot.CaptureIfRequested(
|
||||||
|
m_renderer,
|
||||||
|
drawData,
|
||||||
|
static_cast<unsigned int>(width),
|
||||||
|
static_cast<unsigned int>(height),
|
||||||
|
framePresented);
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND m_hwnd = nullptr;
|
||||||
|
ATOM m_windowClassAtom = 0;
|
||||||
|
NativeRenderer m_renderer = {};
|
||||||
|
AutoScreenshotController m_autoScreenshot = {};
|
||||||
|
std::filesystem::path m_captureRoot = {};
|
||||||
|
UIEditorEnumFieldSpec m_spec = {};
|
||||||
|
std::size_t m_selectedIndex = 0u;
|
||||||
|
UIEditorEnumFieldInteractionState m_interactionState = {};
|
||||||
|
UIEditorEnumFieldInteractionFrame m_frame = {};
|
||||||
|
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||||
|
ActionId m_hoveredAction = ActionId::Reset;
|
||||||
|
bool m_hasHoveredAction = false;
|
||||||
|
std::string m_lastResult = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||||
|
return ScenarioApp().Run(hInstance, nCmdShow);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
add_executable(editor_ui_number_field_basic_validation WIN32
|
||||||
|
main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(editor_ui_number_field_basic_validation PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/new_editor/include
|
||||||
|
${CMAKE_SOURCE_DIR}/new_editor/app
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(editor_ui_number_field_basic_validation PRIVATE
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(editor_ui_number_field_basic_validation PRIVATE /utf-8 /FS)
|
||||||
|
set_property(TARGET editor_ui_number_field_basic_validation PROPERTY
|
||||||
|
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_link_libraries(editor_ui_number_field_basic_validation PRIVATE
|
||||||
|
XCUIEditorLib
|
||||||
|
XCUIEditorHost
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(editor_ui_number_field_basic_validation PROPERTIES
|
||||||
|
OUTPUT_NAME "XCUIEditorNumberFieldBasicValidation"
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
727
tests/UI/Editor/integration/shell/number_field_basic/main.cpp
Normal file
727
tests/UI/Editor/integration/shell/number_field_basic/main.cpp
Normal file
@@ -0,0 +1,727 @@
|
|||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <XCEditor/Core/UIEditorNumberFieldInteraction.h>
|
||||||
|
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||||
|
#include "Host/AutoScreenshot.h"
|
||||||
|
#include "Host/NativeRenderer.h"
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
|
||||||
|
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Input::KeyCode;
|
||||||
|
using XCEngine::UI::UIColor;
|
||||||
|
using XCEngine::UI::UIDrawData;
|
||||||
|
using XCEngine::UI::UIDrawList;
|
||||||
|
using XCEngine::UI::UIInputEvent;
|
||||||
|
using XCEngine::UI::UIInputEventType;
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIPointerButton;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::Host::AutoScreenshotController;
|
||||||
|
using XCEngine::UI::Editor::Host::NativeRenderer;
|
||||||
|
using XCEngine::UI::Editor::UIEditorNumberFieldInteractionFrame;
|
||||||
|
using XCEngine::UI::Editor::UIEditorNumberFieldInteractionResult;
|
||||||
|
using XCEngine::UI::Editor::UIEditorNumberFieldInteractionState;
|
||||||
|
using XCEngine::UI::Editor::UpdateUIEditorNumberFieldInteraction;
|
||||||
|
using XCEngine::UI::Editor::Widgets::AppendUIEditorNumberField;
|
||||||
|
using XCEngine::UI::Editor::Widgets::FormatUIEditorNumberFieldValue;
|
||||||
|
using XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTarget;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTargetKind;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
|
||||||
|
|
||||||
|
constexpr const wchar_t* kWindowClassName = L"XCUIEditorNumberFieldBasicValidation";
|
||||||
|
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | NumberField Basic";
|
||||||
|
|
||||||
|
constexpr UIColor kWindowBg(0.13f, 0.13f, 0.13f, 1.0f);
|
||||||
|
constexpr UIColor kCardBg(0.18f, 0.18f, 0.18f, 1.0f);
|
||||||
|
constexpr UIColor kCardBorder(0.29f, 0.29f, 0.29f, 1.0f);
|
||||||
|
constexpr UIColor kTextPrimary(0.94f, 0.94f, 0.94f, 1.0f);
|
||||||
|
constexpr UIColor kTextMuted(0.72f, 0.72f, 0.72f, 1.0f);
|
||||||
|
constexpr UIColor kTextWeak(0.56f, 0.56f, 0.56f, 1.0f);
|
||||||
|
constexpr UIColor kButtonBg(0.25f, 0.25f, 0.25f, 1.0f);
|
||||||
|
constexpr UIColor kButtonHoverBg(0.32f, 0.32f, 0.32f, 1.0f);
|
||||||
|
|
||||||
|
enum class ActionId : unsigned char {
|
||||||
|
Reset = 0,
|
||||||
|
Capture
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ButtonLayout {
|
||||||
|
ActionId action = ActionId::Reset;
|
||||||
|
const char* label = "";
|
||||||
|
UIRect rect = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScenarioLayout {
|
||||||
|
UIRect introRect = {};
|
||||||
|
UIRect controlRect = {};
|
||||||
|
UIRect stateRect = {};
|
||||||
|
UIRect previewRect = {};
|
||||||
|
UIRect fieldRect = {};
|
||||||
|
std::vector<ButtonLayout> buttons = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::filesystem::path ResolveRepoRootPath() {
|
||||||
|
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
|
||||||
|
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
|
||||||
|
root = root.substr(1u, root.size() - 2u);
|
||||||
|
}
|
||||||
|
return std::filesystem::path(root).lexically_normal();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContainsPoint(const UIRect& rect, float x, float y) {
|
||||||
|
return x >= rect.x &&
|
||||||
|
x <= rect.x + rect.width &&
|
||||||
|
y >= rect.y &&
|
||||||
|
y <= rect.y + rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int32_t MapNumberFieldKey(UINT keyCode) {
|
||||||
|
switch (keyCode) {
|
||||||
|
case VK_LEFT:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Left);
|
||||||
|
case VK_RIGHT:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Right);
|
||||||
|
case VK_UP:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Up);
|
||||||
|
case VK_DOWN:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Down);
|
||||||
|
case VK_HOME:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Home);
|
||||||
|
case VK_END:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::End);
|
||||||
|
case VK_RETURN:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Enter);
|
||||||
|
case VK_ESCAPE:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::Escape);
|
||||||
|
default:
|
||||||
|
return static_cast<std::int32_t>(KeyCode::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScenarioLayout BuildScenarioLayout(float width, float height) {
|
||||||
|
constexpr float margin = 20.0f;
|
||||||
|
constexpr float leftWidth = 430.0f;
|
||||||
|
constexpr float gap = 16.0f;
|
||||||
|
|
||||||
|
ScenarioLayout layout = {};
|
||||||
|
layout.introRect = UIRect(margin, margin, leftWidth, 232.0f);
|
||||||
|
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
|
||||||
|
layout.stateRect = UIRect(
|
||||||
|
margin,
|
||||||
|
layout.controlRect.y + layout.controlRect.height + gap,
|
||||||
|
leftWidth,
|
||||||
|
(std::max)(240.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
|
||||||
|
layout.previewRect = UIRect(
|
||||||
|
leftWidth + margin * 2.0f,
|
||||||
|
margin,
|
||||||
|
(std::max)(420.0f, width - leftWidth - margin * 3.0f),
|
||||||
|
height - margin * 2.0f);
|
||||||
|
layout.fieldRect = UIRect(
|
||||||
|
layout.previewRect.x + 24.0f,
|
||||||
|
layout.previewRect.y + 82.0f,
|
||||||
|
320.0f,
|
||||||
|
32.0f);
|
||||||
|
|
||||||
|
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
|
||||||
|
const float buttonY = layout.controlRect.y + 32.0f;
|
||||||
|
layout.buttons = {
|
||||||
|
{ ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
|
||||||
|
{ ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
|
||||||
|
};
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawCard(
|
||||||
|
UIDrawList& drawList,
|
||||||
|
const UIRect& rect,
|
||||||
|
std::string_view title,
|
||||||
|
std::string_view subtitle = {}) {
|
||||||
|
drawList.AddFilledRect(rect, kCardBg, 10.0f);
|
||||||
|
drawList.AddRectOutline(rect, kCardBorder, 1.0f, 10.0f);
|
||||||
|
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 14.0f), std::string(title), kTextPrimary, 17.0f);
|
||||||
|
if (!subtitle.empty()) {
|
||||||
|
drawList.AddText(UIPoint(rect.x + 16.0f, rect.y + 40.0f), std::string(subtitle), kTextMuted, 12.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawButton(
|
||||||
|
UIDrawList& drawList,
|
||||||
|
const ButtonLayout& button,
|
||||||
|
bool hovered) {
|
||||||
|
drawList.AddFilledRect(button.rect, hovered ? kButtonHoverBg : kButtonBg, 8.0f);
|
||||||
|
drawList.AddRectOutline(button.rect, kCardBorder, 1.0f, 8.0f);
|
||||||
|
drawList.AddText(UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f), button.label, kTextPrimary, 12.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DescribeHitTarget(const UIEditorNumberFieldHitTarget& hitTarget) {
|
||||||
|
switch (hitTarget.kind) {
|
||||||
|
case UIEditorNumberFieldHitTargetKind::DecrementButton:
|
||||||
|
return "decrement";
|
||||||
|
case UIEditorNumberFieldHitTargetKind::IncrementButton:
|
||||||
|
return "increment";
|
||||||
|
case UIEditorNumberFieldHitTargetKind::ValueBox:
|
||||||
|
return "value_box";
|
||||||
|
case UIEditorNumberFieldHitTargetKind::Row:
|
||||||
|
return "row";
|
||||||
|
case UIEditorNumberFieldHitTargetKind::None:
|
||||||
|
default:
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakePointerEvent(
|
||||||
|
UIInputEventType type,
|
||||||
|
const UIPoint& position,
|
||||||
|
UIPointerButton button = UIPointerButton::None) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
|
event.position = position;
|
||||||
|
event.pointerButton = button;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakeKeyEvent(std::int32_t keyCode) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::KeyDown;
|
||||||
|
event.keyCode = keyCode;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakeCharacterEvent(wchar_t character) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::Character;
|
||||||
|
event.character = static_cast<std::uint32_t>(character);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScenarioApp {
|
||||||
|
public:
|
||||||
|
int Run(HINSTANCE hInstance, int nCmdShow) {
|
||||||
|
if (!Initialize(hInstance, nCmdShow)) {
|
||||||
|
Shutdown();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
MSG message = {};
|
||||||
|
while (message.message != WM_QUIT) {
|
||||||
|
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
|
||||||
|
TranslateMessage(&message);
|
||||||
|
DispatchMessageW(&message);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderFrame();
|
||||||
|
Sleep(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
Shutdown();
|
||||||
|
return static_cast<int>(message.wParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||||||
|
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
|
||||||
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||||
|
switch (message) {
|
||||||
|
case WM_SIZE:
|
||||||
|
if (app != nullptr && wParam != SIZE_MINIMIZED) {
|
||||||
|
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_MOUSEMOVE:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleMouseMove(
|
||||||
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_MOUSELEAVE:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleMouseLeave();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_LBUTTONDOWN:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleLeftButtonDown(
|
||||||
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_LBUTTONUP:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleLeftButtonUp(
|
||||||
|
static_cast<float>(GET_X_LPARAM(lParam)),
|
||||||
|
static_cast<float>(GET_Y_LPARAM(lParam)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_KEYDOWN:
|
||||||
|
case WM_SYSKEYDOWN:
|
||||||
|
if (app != nullptr) {
|
||||||
|
if (wParam == VK_F12) {
|
||||||
|
app->m_autoScreenshot.RequestCapture("manual_f12");
|
||||||
|
app->m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||||
|
InvalidateRect(hwnd, nullptr, FALSE);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::int32_t keyCode = MapNumberFieldKey(static_cast<UINT>(wParam));
|
||||||
|
if (keyCode != static_cast<std::int32_t>(KeyCode::None)) {
|
||||||
|
app->HandleKeyDown(keyCode);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_CHAR:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->HandleCharacter(static_cast<wchar_t>(wParam));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_PAINT:
|
||||||
|
if (app != nullptr) {
|
||||||
|
PAINTSTRUCT paintStruct = {};
|
||||||
|
BeginPaint(hwnd, &paintStruct);
|
||||||
|
app->RenderFrame();
|
||||||
|
EndPaint(hwnd, &paintStruct);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_ERASEBKGND:
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
case WM_DESTROY:
|
||||||
|
if (app != nullptr) {
|
||||||
|
app->m_hwnd = nullptr;
|
||||||
|
}
|
||||||
|
PostQuitMessage(0);
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
|
||||||
|
WNDCLASSEXW windowClass = {};
|
||||||
|
windowClass.cbSize = sizeof(windowClass);
|
||||||
|
windowClass.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
windowClass.lpfnWndProc = &ScenarioApp::WndProc;
|
||||||
|
windowClass.hInstance = hInstance;
|
||||||
|
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||||
|
windowClass.lpszClassName = kWindowClassName;
|
||||||
|
|
||||||
|
m_windowClassAtom = RegisterClassExW(&windowClass);
|
||||||
|
if (m_windowClassAtom == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hwnd = CreateWindowExW(
|
||||||
|
0,
|
||||||
|
kWindowClassName,
|
||||||
|
kWindowTitle,
|
||||||
|
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
1480,
|
||||||
|
920,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
hInstance,
|
||||||
|
this);
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowWindow(m_hwnd, nCmdShow);
|
||||||
|
UpdateWindow(m_hwnd);
|
||||||
|
|
||||||
|
if (!m_renderer.Initialize(m_hwnd)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_captureRoot =
|
||||||
|
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/number_field_basic/captures";
|
||||||
|
m_autoScreenshot.Initialize(m_captureRoot);
|
||||||
|
|
||||||
|
ResetScenario();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shutdown() {
|
||||||
|
m_autoScreenshot.Shutdown();
|
||||||
|
m_renderer.Shutdown();
|
||||||
|
|
||||||
|
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
|
||||||
|
DestroyWindow(m_hwnd);
|
||||||
|
}
|
||||||
|
m_hwnd = nullptr;
|
||||||
|
|
||||||
|
if (m_windowClassAtom != 0) {
|
||||||
|
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
|
||||||
|
m_windowClassAtom = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScenarioLayout GetLayout() const {
|
||||||
|
RECT clientRect = {};
|
||||||
|
GetClientRect(m_hwnd, &clientRect);
|
||||||
|
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||||
|
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||||
|
return BuildScenarioLayout(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetScenario() {
|
||||||
|
m_spec = {};
|
||||||
|
m_spec.fieldId = "scale";
|
||||||
|
m_spec.label = "Scale";
|
||||||
|
m_spec.value = 1.25;
|
||||||
|
m_spec.step = 0.25;
|
||||||
|
m_spec.minValue = 0.0;
|
||||||
|
m_spec.maxValue = 4.0;
|
||||||
|
m_spec.integerMode = false;
|
||||||
|
m_spec.readOnly = false;
|
||||||
|
m_interactionState = {};
|
||||||
|
m_interactionState.numberFieldState.focused = true;
|
||||||
|
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||||
|
m_hoveredAction = ActionId::Reset;
|
||||||
|
m_hasHoveredAction = false;
|
||||||
|
m_lastResult = "已重置到默认 NumberField 状态";
|
||||||
|
RefreshFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RefreshFrame() {
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
m_frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
m_interactionState,
|
||||||
|
m_spec,
|
||||||
|
layout.fieldRect,
|
||||||
|
{});
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnResize(UINT width, UINT height) {
|
||||||
|
if (width == 0u || height == 0u) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_renderer.Resize(width, height);
|
||||||
|
RefreshFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleMouseMove(float x, float y) {
|
||||||
|
m_mousePosition = UIPoint(x, y);
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
UpdateHoveredAction(layout, x, y);
|
||||||
|
|
||||||
|
TRACKMOUSEEVENT trackEvent = {};
|
||||||
|
trackEvent.cbSize = sizeof(trackEvent);
|
||||||
|
trackEvent.dwFlags = TME_LEAVE;
|
||||||
|
trackEvent.hwndTrack = m_hwnd;
|
||||||
|
TrackMouseEvent(&trackEvent);
|
||||||
|
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) });
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleMouseLeave() {
|
||||||
|
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||||
|
m_hasHoveredAction = false;
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleLeftButtonDown(float x, float y) {
|
||||||
|
m_mousePosition = UIPoint(x, y);
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
if (HitTestAction(layout, x, y) != nullptr) {
|
||||||
|
UpdateHoveredAction(layout, x, y);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) });
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleLeftButtonUp(float x, float y) {
|
||||||
|
m_mousePosition = UIPoint(x, y);
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||||
|
if (button != nullptr) {
|
||||||
|
ExecuteAction(button->action);
|
||||||
|
UpdateHoveredAction(layout, x, y);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIEditorNumberFieldInteractionResult result =
|
||||||
|
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) });
|
||||||
|
UpdateResultText(result);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleKeyDown(std::int32_t keyCode) {
|
||||||
|
const UIEditorNumberFieldInteractionResult result = PumpEvents({ MakeKeyEvent(keyCode) });
|
||||||
|
UpdateResultText(result);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleCharacter(wchar_t character) {
|
||||||
|
if (character < 32) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIEditorNumberFieldInteractionResult result =
|
||||||
|
PumpEvents({ MakeCharacterEvent(character) });
|
||||||
|
UpdateResultText(result);
|
||||||
|
InvalidateRect(m_hwnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
|
||||||
|
const ButtonLayout* button = HitTestAction(layout, x, y);
|
||||||
|
if (button == nullptr) {
|
||||||
|
m_hasHoveredAction = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_hoveredAction = button->action;
|
||||||
|
m_hasHoveredAction = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const {
|
||||||
|
for (const ButtonLayout& button : layout.buttons) {
|
||||||
|
if (ContainsPoint(button.rect, x, y)) {
|
||||||
|
return &button;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIEditorNumberFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
|
||||||
|
const ScenarioLayout layout = GetLayout();
|
||||||
|
m_frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
m_interactionState,
|
||||||
|
m_spec,
|
||||||
|
layout.fieldRect,
|
||||||
|
std::move(events));
|
||||||
|
return m_frame.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateResultText(const UIEditorNumberFieldInteractionResult& result) {
|
||||||
|
if (result.editCommitRejected) {
|
||||||
|
m_lastResult = "提交失败,仍保留在编辑态";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.editCommitted) {
|
||||||
|
m_lastResult = std::string("已提交数值: ") + result.committedText;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.editCanceled) {
|
||||||
|
m_lastResult = "已取消编辑";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.editStarted) {
|
||||||
|
m_lastResult = "已进入编辑态";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.valueChanged || result.stepApplied) {
|
||||||
|
m_lastResult = std::string("数值已更新: ") + FormatUIEditorNumberFieldValue(m_spec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.consumed) {
|
||||||
|
m_lastResult = "控件已消费输入";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_lastResult = "等待交互";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExecuteAction(ActionId action) {
|
||||||
|
switch (action) {
|
||||||
|
case ActionId::Reset:
|
||||||
|
ResetScenario();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ActionId::Capture:
|
||||||
|
m_autoScreenshot.RequestCapture("manual_button");
|
||||||
|
m_lastResult = "已请求截图,输出到 captures/latest.png";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderFrame() {
|
||||||
|
if (m_hwnd == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT clientRect = {};
|
||||||
|
GetClientRect(m_hwnd, &clientRect);
|
||||||
|
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
|
||||||
|
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
|
||||||
|
const ScenarioLayout layout = BuildScenarioLayout(width, height);
|
||||||
|
RefreshFrame();
|
||||||
|
|
||||||
|
const UIEditorNumberFieldHitTarget currentHit =
|
||||||
|
HitTestUIEditorNumberField(m_frame.layout, m_mousePosition);
|
||||||
|
|
||||||
|
UIDrawData drawData = {};
|
||||||
|
UIDrawList& drawList = drawData.EmplaceDrawList("EditorNumberFieldBasic");
|
||||||
|
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowBg);
|
||||||
|
|
||||||
|
DrawCard(
|
||||||
|
drawList,
|
||||||
|
layout.introRect,
|
||||||
|
"这个测试在验证什么功能",
|
||||||
|
"只验证 Editor NumberField 的基础交互契约,不涉及 PropertyGrid 或任何业务 Inspector。");
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
|
||||||
|
"1. 点击 +/- 按钮,检查步进和上下界钳制是否稳定。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
|
||||||
|
"2. 控件获得 focus 后,按 Left / Right / Up / Down / Home / End,检查键盘步进。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
|
||||||
|
"3. 按 Enter 进入编辑态,直接输入字符,Enter commit,Esc cancel。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
|
||||||
|
"4. 检查 Hover / Focus / Editing / Value / Result 是否同步更新。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
|
||||||
|
"5. 按 F12 或点击截图按钮,确认自动截图路径正确。",
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
|
||||||
|
DrawCard(drawList, layout.controlRect, "操作");
|
||||||
|
for (const ButtonLayout& button : layout.buttons) {
|
||||||
|
DrawButton(drawList, button, m_hasHoveredAction && m_hoveredAction == button.action);
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawCard(drawList, layout.stateRect, "状态摘要", "重点检查 hit / focus / editing / value / result。");
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
|
||||||
|
"Hover: " + DescribeHitTarget(currentHit),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
|
||||||
|
std::string("Focused: ") + (m_interactionState.numberFieldState.focused ? "是" : "否"),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
|
||||||
|
std::string("Editing: ") + (m_interactionState.numberFieldState.editing ? "是" : "否"),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
|
||||||
|
"Value: " + FormatUIEditorNumberFieldValue(m_spec),
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
|
||||||
|
"Display: " + m_interactionState.numberFieldState.displayText,
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
|
||||||
|
"Result: " + m_lastResult,
|
||||||
|
kTextPrimary,
|
||||||
|
12.0f);
|
||||||
|
|
||||||
|
const std::string captureSummary =
|
||||||
|
m_autoScreenshot.HasPendingCapture()
|
||||||
|
? "截图排队中..."
|
||||||
|
: (m_autoScreenshot.GetLastCaptureSummary().empty()
|
||||||
|
? std::string("F12 -> tests/UI/Editor/integration/shell/number_field_basic/captures/")
|
||||||
|
: m_autoScreenshot.GetLastCaptureSummary());
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
|
||||||
|
captureSummary,
|
||||||
|
kTextWeak,
|
||||||
|
12.0f);
|
||||||
|
|
||||||
|
DrawCard(drawList, layout.previewRect, "NumberField 预览", "这里只放一个 NumberField。");
|
||||||
|
AppendUIEditorNumberField(
|
||||||
|
drawList,
|
||||||
|
layout.fieldRect,
|
||||||
|
m_spec,
|
||||||
|
m_interactionState.numberFieldState);
|
||||||
|
|
||||||
|
const bool framePresented = m_renderer.Render(drawData);
|
||||||
|
m_autoScreenshot.CaptureIfRequested(
|
||||||
|
m_renderer,
|
||||||
|
drawData,
|
||||||
|
static_cast<unsigned int>(width),
|
||||||
|
static_cast<unsigned int>(height),
|
||||||
|
framePresented);
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND m_hwnd = nullptr;
|
||||||
|
ATOM m_windowClassAtom = 0;
|
||||||
|
NativeRenderer m_renderer = {};
|
||||||
|
AutoScreenshotController m_autoScreenshot = {};
|
||||||
|
std::filesystem::path m_captureRoot = {};
|
||||||
|
UIEditorNumberFieldSpec m_spec = {};
|
||||||
|
UIEditorNumberFieldInteractionState m_interactionState = {};
|
||||||
|
UIEditorNumberFieldInteractionFrame m_frame = {};
|
||||||
|
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
|
||||||
|
ActionId m_hoveredAction = ActionId::Reset;
|
||||||
|
bool m_hasHoveredAction = false;
|
||||||
|
std::string m_lastResult = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
|
||||||
|
return ScenarioApp().Run(hInstance, nCmdShow);
|
||||||
|
}
|
||||||
@@ -17,11 +17,17 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
|
|||||||
test_ui_editor_shell_compose.cpp
|
test_ui_editor_shell_compose.cpp
|
||||||
test_ui_editor_shell_interaction.cpp
|
test_ui_editor_shell_interaction.cpp
|
||||||
test_ui_editor_collection_primitives.cpp
|
test_ui_editor_collection_primitives.cpp
|
||||||
|
test_ui_editor_bool_field.cpp
|
||||||
|
test_ui_editor_bool_field_interaction.cpp
|
||||||
test_ui_editor_dock_host.cpp
|
test_ui_editor_dock_host.cpp
|
||||||
test_ui_editor_list_view.cpp
|
test_ui_editor_list_view.cpp
|
||||||
test_ui_editor_list_view_interaction.cpp
|
test_ui_editor_list_view_interaction.cpp
|
||||||
test_ui_editor_panel_chrome.cpp
|
test_ui_editor_panel_chrome.cpp
|
||||||
test_ui_editor_panel_frame.cpp
|
test_ui_editor_panel_frame.cpp
|
||||||
|
test_ui_editor_enum_field.cpp
|
||||||
|
test_ui_editor_enum_field_interaction.cpp
|
||||||
|
test_ui_editor_number_field.cpp
|
||||||
|
test_ui_editor_number_field_interaction.cpp
|
||||||
test_ui_editor_scroll_view.cpp
|
test_ui_editor_scroll_view.cpp
|
||||||
test_ui_editor_scroll_view_interaction.cpp
|
test_ui_editor_scroll_view_interaction.cpp
|
||||||
test_ui_editor_status_bar.cpp
|
test_ui_editor_status_bar.cpp
|
||||||
@@ -63,4 +69,6 @@ if(MSVC)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
include(GoogleTest)
|
include(GoogleTest)
|
||||||
gtest_discover_tests(editor_ui_tests)
|
gtest_discover_tests(editor_ui_tests
|
||||||
|
DISCOVERY_MODE PRE_TEST
|
||||||
|
)
|
||||||
|
|||||||
54
tests/UI/Editor/unit/test_ui_editor_bool_field.cpp
Normal file
54
tests/UI/Editor/unit/test_ui_editor_bool_field.cpp
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEditor/Widgets/UIEditorBoolField.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolFieldBackground;
|
||||||
|
using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolFieldForeground;
|
||||||
|
using XCEngine::UI::Editor::Widgets::BuildUIEditorBoolFieldLayout;
|
||||||
|
using XCEngine::UI::Editor::Widgets::HitTestUIEditorBoolField;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldState;
|
||||||
|
|
||||||
|
TEST(UIEditorBoolFieldTest, LayoutBuildsLabelAndToggleRects) {
|
||||||
|
UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false };
|
||||||
|
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||||
|
|
||||||
|
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||||
|
EXPECT_FLOAT_EQ(layout.toggleRect.width, 42.0f);
|
||||||
|
EXPECT_GT(layout.knobRect.x, layout.toggleRect.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorBoolFieldTest, HitTestResolvesToggleAndRow) {
|
||||||
|
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
|
||||||
|
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||||
|
|
||||||
|
const auto toggleHit = HitTestUIEditorBoolField(
|
||||||
|
layout,
|
||||||
|
UIPoint(layout.toggleRect.x + 4.0f, layout.toggleRect.y + 4.0f));
|
||||||
|
EXPECT_EQ(toggleHit.kind, UIEditorBoolFieldHitTargetKind::Toggle);
|
||||||
|
|
||||||
|
const auto rowHit = HitTestUIEditorBoolField(layout, UIPoint(20.0f, 16.0f));
|
||||||
|
EXPECT_EQ(rowHit.kind, UIEditorBoolFieldHitTargetKind::Row);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorBoolFieldTest, BackgroundAndForegroundEmitStableCommands) {
|
||||||
|
UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false };
|
||||||
|
UIEditorBoolFieldState state = {};
|
||||||
|
state.focused = true;
|
||||||
|
state.hoveredTarget = UIEditorBoolFieldHitTargetKind::Toggle;
|
||||||
|
|
||||||
|
XCEngine::UI::UIDrawData drawData = {};
|
||||||
|
auto& drawList = drawData.EmplaceDrawList("BoolField");
|
||||||
|
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||||
|
AppendUIEditorBoolFieldBackground(drawList, layout, spec, state);
|
||||||
|
AppendUIEditorBoolFieldForeground(drawList, layout, spec);
|
||||||
|
|
||||||
|
ASSERT_GE(drawList.GetCommands().size(), 6u);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
109
tests/UI/Editor/unit/test_ui_editor_bool_field_interaction.cpp
Normal file
109
tests/UI/Editor/unit/test_ui_editor_bool_field_interaction.cpp
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEditor/Core/UIEditorBoolFieldInteraction.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Input::KeyCode;
|
||||||
|
using XCEngine::UI::UIInputEvent;
|
||||||
|
using XCEngine::UI::UIInputEventType;
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIPointerButton;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::UIEditorBoolFieldInteractionState;
|
||||||
|
using XCEngine::UI::Editor::UpdateUIEditorBoolFieldInteraction;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
|
||||||
|
|
||||||
|
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
|
event.position = UIPoint(x, y);
|
||||||
|
event.pointerButton = button;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::KeyDown;
|
||||||
|
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(UIEditorBoolFieldInteractionTest, ClickToggleFlipsValue) {
|
||||||
|
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
|
||||||
|
UIEditorBoolFieldInteractionState state = {};
|
||||||
|
bool value = false;
|
||||||
|
|
||||||
|
auto frame = UpdateUIEditorBoolFieldInteraction(
|
||||||
|
state,
|
||||||
|
value,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
spec,
|
||||||
|
{});
|
||||||
|
const auto toggle = frame.layout.toggleRect;
|
||||||
|
|
||||||
|
frame = UpdateUIEditorBoolFieldInteraction(
|
||||||
|
state,
|
||||||
|
value,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
spec,
|
||||||
|
{
|
||||||
|
MakePointer(UIInputEventType::PointerButtonDown, toggle.x + 4.0f, toggle.y + 4.0f, UIPointerButton::Left),
|
||||||
|
MakePointer(UIInputEventType::PointerButtonUp, toggle.x + 4.0f, toggle.y + 4.0f, UIPointerButton::Left)
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_TRUE(frame.result.valueChanged);
|
||||||
|
EXPECT_TRUE(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorBoolFieldInteractionTest, SpaceAndEnterToggleWhenFocused) {
|
||||||
|
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
|
||||||
|
UIEditorBoolFieldInteractionState state = {};
|
||||||
|
state.fieldState.focused = true;
|
||||||
|
bool value = false;
|
||||||
|
|
||||||
|
auto frame = UpdateUIEditorBoolFieldInteraction(
|
||||||
|
state,
|
||||||
|
value,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
spec,
|
||||||
|
{ MakeKey(KeyCode::Space) });
|
||||||
|
EXPECT_TRUE(frame.result.valueChanged);
|
||||||
|
EXPECT_TRUE(value);
|
||||||
|
|
||||||
|
frame = UpdateUIEditorBoolFieldInteraction(
|
||||||
|
state,
|
||||||
|
value,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
spec,
|
||||||
|
{ MakeKey(KeyCode::Enter) });
|
||||||
|
EXPECT_TRUE(frame.result.valueChanged);
|
||||||
|
EXPECT_FALSE(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorBoolFieldInteractionTest, HoverTracksToggleHitTarget) {
|
||||||
|
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
|
||||||
|
UIEditorBoolFieldInteractionState state = {};
|
||||||
|
bool value = false;
|
||||||
|
|
||||||
|
auto frame = UpdateUIEditorBoolFieldInteraction(
|
||||||
|
state,
|
||||||
|
value,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
spec,
|
||||||
|
{});
|
||||||
|
const auto toggle = frame.layout.toggleRect;
|
||||||
|
|
||||||
|
frame = UpdateUIEditorBoolFieldInteraction(
|
||||||
|
state,
|
||||||
|
value,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
spec,
|
||||||
|
{ MakePointer(UIInputEventType::PointerMove, toggle.x + 4.0f, toggle.y + 4.0f) });
|
||||||
|
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorBoolFieldHitTargetKind::Toggle);
|
||||||
|
}
|
||||||
35
tests/UI/Editor/unit/test_ui_editor_enum_field.cpp
Normal file
35
tests/UI/Editor/unit/test_ui_editor_enum_field.cpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEditor/Widgets/UIEditorEnumField.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::Widgets::BuildUIEditorEnumFieldLayout;
|
||||||
|
using XCEngine::UI::Editor::Widgets::HitTestUIEditorEnumField;
|
||||||
|
using XCEngine::UI::Editor::Widgets::ResolveUIEditorEnumFieldValueText;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
|
||||||
|
|
||||||
|
TEST(UIEditorEnumFieldTest, ValueTextUsesSelectedOption) {
|
||||||
|
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||||
|
EXPECT_EQ(ResolveUIEditorEnumFieldValueText(spec), "Cutout");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorEnumFieldTest, HitTestResolvesPreviousNextAndValueBox) {
|
||||||
|
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||||
|
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
HitTestUIEditorEnumField(layout, UIPoint(layout.previousRect.x + 2.0f, layout.previousRect.y + 2.0f)).kind,
|
||||||
|
UIEditorEnumFieldHitTargetKind::PreviousButton);
|
||||||
|
EXPECT_EQ(
|
||||||
|
HitTestUIEditorEnumField(layout, UIPoint(layout.nextRect.x + 2.0f, layout.nextRect.y + 2.0f)).kind,
|
||||||
|
UIEditorEnumFieldHitTargetKind::NextButton);
|
||||||
|
EXPECT_EQ(
|
||||||
|
HitTestUIEditorEnumField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
|
||||||
|
UIEditorEnumFieldHitTargetKind::ValueBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEditor/Core/UIEditorEnumFieldInteraction.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Input::KeyCode;
|
||||||
|
using XCEngine::UI::UIInputEvent;
|
||||||
|
using XCEngine::UI::UIInputEventType;
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIPointerButton;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::UIEditorEnumFieldInteractionState;
|
||||||
|
using XCEngine::UI::Editor::UpdateUIEditorEnumFieldInteraction;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
|
||||||
|
|
||||||
|
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
|
event.position = UIPoint(x, y);
|
||||||
|
event.pointerButton = button;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::KeyDown;
|
||||||
|
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(UIEditorEnumFieldInteractionTest, ClickButtonsAdjustSelection) {
|
||||||
|
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||||
|
UIEditorEnumFieldInteractionState state = {};
|
||||||
|
std::size_t selectedIndex = 1u;
|
||||||
|
|
||||||
|
auto frame = UpdateUIEditorEnumFieldInteraction(
|
||||||
|
state,
|
||||||
|
selectedIndex,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
spec,
|
||||||
|
{});
|
||||||
|
|
||||||
|
frame = UpdateUIEditorEnumFieldInteraction(
|
||||||
|
state,
|
||||||
|
selectedIndex,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
spec,
|
||||||
|
{
|
||||||
|
MakePointer(UIInputEventType::PointerButtonDown, frame.layout.nextRect.x + 2.0f, frame.layout.nextRect.y + 2.0f, UIPointerButton::Left),
|
||||||
|
MakePointer(UIInputEventType::PointerButtonUp, frame.layout.nextRect.x + 2.0f, frame.layout.nextRect.y + 2.0f, UIPointerButton::Left)
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(frame.result.selectionChanged);
|
||||||
|
EXPECT_EQ(selectedIndex, 2u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorEnumFieldInteractionTest, KeyboardControlsMoveToEnds) {
|
||||||
|
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
|
||||||
|
UIEditorEnumFieldInteractionState state = {};
|
||||||
|
state.fieldState.focused = true;
|
||||||
|
std::size_t selectedIndex = 1u;
|
||||||
|
|
||||||
|
auto frame = UpdateUIEditorEnumFieldInteraction(
|
||||||
|
state,
|
||||||
|
selectedIndex,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
spec,
|
||||||
|
{ MakeKey(KeyCode::Home) });
|
||||||
|
EXPECT_TRUE(frame.result.selectionChanged);
|
||||||
|
EXPECT_EQ(selectedIndex, 0u);
|
||||||
|
|
||||||
|
frame = UpdateUIEditorEnumFieldInteraction(
|
||||||
|
state,
|
||||||
|
selectedIndex,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
spec,
|
||||||
|
{ MakeKey(KeyCode::End) });
|
||||||
|
EXPECT_TRUE(frame.result.selectionChanged);
|
||||||
|
EXPECT_EQ(selectedIndex, 2u);
|
||||||
|
}
|
||||||
49
tests/UI/Editor/unit/test_ui_editor_number_field.cpp
Normal file
49
tests/UI/Editor/unit/test_ui_editor_number_field.cpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEditor/Widgets/UIEditorNumberField.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::Widgets::BuildUIEditorNumberFieldLayout;
|
||||||
|
using XCEngine::UI::Editor::Widgets::FormatUIEditorNumberFieldValue;
|
||||||
|
using XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTargetKind;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
|
||||||
|
|
||||||
|
TEST(UIEditorNumberFieldTest, FormatSupportsIntegerAndFloatMode) {
|
||||||
|
UIEditorNumberFieldSpec integerSpec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
|
||||||
|
UIEditorNumberFieldSpec floatSpec = { "scale", "Scale", 1.25, 0.25, 0.0, 2.0, false, false };
|
||||||
|
|
||||||
|
EXPECT_EQ(FormatUIEditorNumberFieldValue(integerSpec), "7");
|
||||||
|
EXPECT_EQ(FormatUIEditorNumberFieldValue(floatSpec), "1.25");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorNumberFieldTest, LayoutBuildsValueAndStepperRects) {
|
||||||
|
UIEditorNumberFieldSpec spec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
|
||||||
|
const auto layout = BuildUIEditorNumberFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||||
|
|
||||||
|
EXPECT_GT(layout.labelRect.width, 0.0f);
|
||||||
|
EXPECT_GT(layout.controlRect.width, 0.0f);
|
||||||
|
EXPECT_GT(layout.valueRect.width, 0.0f);
|
||||||
|
EXPECT_FLOAT_EQ(layout.decrementRect.width, 22.0f);
|
||||||
|
EXPECT_FLOAT_EQ(layout.incrementRect.width, 22.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorNumberFieldTest, HitTestResolvesButtonsAndValueBox) {
|
||||||
|
UIEditorNumberFieldSpec spec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
|
||||||
|
const auto layout = BuildUIEditorNumberFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
HitTestUIEditorNumberField(layout, UIPoint(layout.decrementRect.x + 2.0f, layout.decrementRect.y + 2.0f)).kind,
|
||||||
|
UIEditorNumberFieldHitTargetKind::DecrementButton);
|
||||||
|
EXPECT_EQ(
|
||||||
|
HitTestUIEditorNumberField(layout, UIPoint(layout.incrementRect.x + 2.0f, layout.incrementRect.y + 2.0f)).kind,
|
||||||
|
UIEditorNumberFieldHitTargetKind::IncrementButton);
|
||||||
|
EXPECT_EQ(
|
||||||
|
HitTestUIEditorNumberField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
|
||||||
|
UIEditorNumberFieldHitTargetKind::ValueBox);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
155
tests/UI/Editor/unit/test_ui_editor_number_field_interaction.cpp
Normal file
155
tests/UI/Editor/unit/test_ui_editor_number_field_interaction.cpp
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEditor/Core/UIEditorNumberFieldInteraction.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Input/InputTypes.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using XCEngine::Input::KeyCode;
|
||||||
|
using XCEngine::UI::UIInputEvent;
|
||||||
|
using XCEngine::UI::UIInputEventType;
|
||||||
|
using XCEngine::UI::UIPoint;
|
||||||
|
using XCEngine::UI::UIPointerButton;
|
||||||
|
using XCEngine::UI::UIRect;
|
||||||
|
using XCEngine::UI::Editor::UIEditorNumberFieldInteractionState;
|
||||||
|
using XCEngine::UI::Editor::UpdateUIEditorNumberFieldInteraction;
|
||||||
|
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
|
||||||
|
|
||||||
|
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = type;
|
||||||
|
event.position = UIPoint(x, y);
|
||||||
|
event.pointerButton = button;
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakeKey(KeyCode keyCode) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::KeyDown;
|
||||||
|
event.keyCode = static_cast<std::int32_t>(keyCode);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIInputEvent MakeCharacter(char character) {
|
||||||
|
UIInputEvent event = {};
|
||||||
|
event.type = UIInputEventType::Character;
|
||||||
|
event.character = static_cast<std::uint32_t>(character);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(UIEditorNumberFieldInteractionTest, ClickStepperButtonsAdjustValue) {
|
||||||
|
UIEditorNumberFieldSpec spec = { "queue", "Queue", 2.0, 1.0, 0.0, 5.0, true, false };
|
||||||
|
UIEditorNumberFieldInteractionState state = {};
|
||||||
|
|
||||||
|
auto frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
state,
|
||||||
|
spec,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
{});
|
||||||
|
|
||||||
|
frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
state,
|
||||||
|
spec,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
{
|
||||||
|
MakePointer(
|
||||||
|
UIInputEventType::PointerButtonDown,
|
||||||
|
frame.layout.incrementRect.x + 2.0f,
|
||||||
|
frame.layout.incrementRect.y + 2.0f,
|
||||||
|
UIPointerButton::Left),
|
||||||
|
MakePointer(
|
||||||
|
UIInputEventType::PointerButtonUp,
|
||||||
|
frame.layout.incrementRect.x + 2.0f,
|
||||||
|
frame.layout.incrementRect.y + 2.0f,
|
||||||
|
UIPointerButton::Left)
|
||||||
|
});
|
||||||
|
|
||||||
|
EXPECT_TRUE(frame.result.stepApplied);
|
||||||
|
EXPECT_TRUE(frame.result.valueChanged);
|
||||||
|
EXPECT_DOUBLE_EQ(spec.value, 3.0);
|
||||||
|
EXPECT_DOUBLE_EQ(frame.result.valueAfter, 3.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorNumberFieldInteractionTest, KeyboardStepAndBoundsWorkWhenFocused) {
|
||||||
|
UIEditorNumberFieldSpec spec = { "queue", "Queue", 2.0, 1.0, 0.0, 5.0, true, false };
|
||||||
|
UIEditorNumberFieldInteractionState state = {};
|
||||||
|
state.numberFieldState.focused = true;
|
||||||
|
|
||||||
|
auto frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
state,
|
||||||
|
spec,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
{ MakeKey(KeyCode::Right) });
|
||||||
|
EXPECT_TRUE(frame.result.stepApplied);
|
||||||
|
EXPECT_DOUBLE_EQ(spec.value, 3.0);
|
||||||
|
|
||||||
|
frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
state,
|
||||||
|
spec,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
{ MakeKey(KeyCode::Home) });
|
||||||
|
EXPECT_TRUE(frame.result.stepApplied);
|
||||||
|
EXPECT_DOUBLE_EQ(spec.value, 0.0);
|
||||||
|
|
||||||
|
frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
state,
|
||||||
|
spec,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
{ MakeKey(KeyCode::End) });
|
||||||
|
EXPECT_TRUE(frame.result.stepApplied);
|
||||||
|
EXPECT_DOUBLE_EQ(spec.value, 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorNumberFieldInteractionTest, EnterStartsEditingAndCharacterInputCommitsValue) {
|
||||||
|
UIEditorNumberFieldSpec spec = { "count", "Count", 7.0, 1.0, 0.0, 1000.0, true, false };
|
||||||
|
UIEditorNumberFieldInteractionState state = {};
|
||||||
|
state.numberFieldState.focused = true;
|
||||||
|
|
||||||
|
auto frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
state,
|
||||||
|
spec,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
{ MakeKey(KeyCode::Enter) });
|
||||||
|
EXPECT_TRUE(frame.result.editStarted);
|
||||||
|
EXPECT_TRUE(state.numberFieldState.editing);
|
||||||
|
|
||||||
|
frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
state,
|
||||||
|
spec,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
{ MakeCharacter('4'), MakeCharacter('2'), MakeKey(KeyCode::Enter) });
|
||||||
|
|
||||||
|
EXPECT_TRUE(frame.result.editCommitted);
|
||||||
|
EXPECT_TRUE(frame.result.valueChanged);
|
||||||
|
EXPECT_FALSE(state.numberFieldState.editing);
|
||||||
|
EXPECT_EQ(frame.result.committedText, "742");
|
||||||
|
EXPECT_DOUBLE_EQ(spec.value, 742.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(UIEditorNumberFieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
|
||||||
|
UIEditorNumberFieldSpec spec = { "count", "Count", 7.0, 1.0, 0.0, 200.0, true, false };
|
||||||
|
UIEditorNumberFieldInteractionState state = {};
|
||||||
|
state.numberFieldState.focused = true;
|
||||||
|
|
||||||
|
auto frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
state,
|
||||||
|
spec,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
{ MakeCharacter('9') });
|
||||||
|
EXPECT_TRUE(frame.result.editStarted);
|
||||||
|
EXPECT_TRUE(state.numberFieldState.editing);
|
||||||
|
EXPECT_EQ(state.numberFieldState.displayText, "9");
|
||||||
|
|
||||||
|
frame = UpdateUIEditorNumberFieldInteraction(
|
||||||
|
state,
|
||||||
|
spec,
|
||||||
|
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
|
||||||
|
{ MakeKey(KeyCode::Escape) });
|
||||||
|
EXPECT_TRUE(frame.result.editCanceled);
|
||||||
|
EXPECT_FALSE(state.numberFieldState.editing);
|
||||||
|
EXPECT_DOUBLE_EQ(spec.value, 7.0);
|
||||||
|
EXPECT_EQ(state.numberFieldState.displayText, "7");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user