diff --git a/new_editor/CMakeLists.txt b/new_editor/CMakeLists.txt index b422200a..dfc82e6f 100644 --- a/new_editor/CMakeLists.txt +++ b/new_editor/CMakeLists.txt @@ -15,10 +15,13 @@ add_library(XCUIEditorLib STATIC src/Core/EditorShellAsset.cpp src/Core/UIEditorCommandDispatcher.cpp src/Core/UIEditorCommandRegistry.cpp + src/Core/UIEditorBoolFieldInteraction.cpp src/Core/UIEditorDockHostInteraction.cpp + src/Core/UIEditorEnumFieldInteraction.cpp src/Core/UIEditorListViewInteraction.cpp src/Core/UIEditorMenuModel.cpp src/Core/UIEditorMenuSession.cpp + src/Core/UIEditorNumberFieldInteraction.cpp src/Core/UIEditorPanelContentHost.cpp src/Core/UIEditorPanelHostLifecycle.cpp src/Core/UIEditorPanelRegistry.cpp @@ -37,10 +40,13 @@ add_library(XCUIEditorLib STATIC src/Core/UIEditorWorkspaceModel.cpp src/Core/UIEditorWorkspaceSession.cpp src/Widgets/UIEditorCollectionPrimitives.cpp + src/Widgets/UIEditorBoolField.cpp src/Widgets/UIEditorDockHost.cpp + src/Widgets/UIEditorEnumField.cpp src/Widgets/UIEditorListView.cpp src/Widgets/UIEditorMenuBar.cpp src/Widgets/UIEditorMenuPopup.cpp + src/Widgets/UIEditorNumberField.cpp src/Widgets/UIEditorPanelFrame.cpp src/Widgets/UIEditorPropertyGrid.cpp src/Widgets/UIEditorScrollView.cpp diff --git a/new_editor/include/XCEditor/Core/UIEditorBoolFieldInteraction.h b/new_editor/include/XCEditor/Core/UIEditorBoolFieldInteraction.h new file mode 100644 index 00000000..a6167195 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorBoolFieldInteraction.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +#include + +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 diff --git a/new_editor/include/XCEditor/Core/UIEditorEnumFieldInteraction.h b/new_editor/include/XCEditor/Core/UIEditorEnumFieldInteraction.h new file mode 100644 index 00000000..d1789158 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorEnumFieldInteraction.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +#include + +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 diff --git a/new_editor/include/XCEditor/Core/UIEditorNumberFieldInteraction.h b/new_editor/include/XCEditor/Core/UIEditorNumberFieldInteraction.h new file mode 100644 index 00000000..172de981 --- /dev/null +++ b/new_editor/include/XCEditor/Core/UIEditorNumberFieldInteraction.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include + +#include +#include + +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 diff --git a/new_editor/include/XCEditor/Widgets/UIEditorBoolField.h b/new_editor/include/XCEditor/Widgets/UIEditorBoolField.h new file mode 100644 index 00000000..b26b8a07 --- /dev/null +++ b/new_editor/include/XCEditor/Widgets/UIEditorBoolField.h @@ -0,0 +1,113 @@ +#pragma once + +#include + +#include +#include + +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 diff --git a/new_editor/include/XCEditor/Widgets/UIEditorEnumField.h b/new_editor/include/XCEditor/Widgets/UIEditorEnumField.h new file mode 100644 index 00000000..2dd15b17 --- /dev/null +++ b/new_editor/include/XCEditor/Widgets/UIEditorEnumField.h @@ -0,0 +1,127 @@ +#pragma once + +#include + +#include +#include +#include +#include + +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 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 diff --git a/new_editor/include/XCEditor/Widgets/UIEditorNumberField.h b/new_editor/include/XCEditor/Widgets/UIEditorNumberField.h new file mode 100644 index 00000000..4d237c55 --- /dev/null +++ b/new_editor/include/XCEditor/Widgets/UIEditorNumberField.h @@ -0,0 +1,146 @@ +#pragma once + +#include + +#include +#include +#include + +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 diff --git a/new_editor/src/Core/UIEditorBoolFieldInteraction.cpp b/new_editor/src/Core/UIEditorBoolFieldInteraction.cpp new file mode 100644 index 00000000..3988fc1a --- /dev/null +++ b/new_editor/src/Core/UIEditorBoolFieldInteraction.cpp @@ -0,0 +1,143 @@ +#include + +#include + +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& 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(event.keyCode) == KeyCode::Space || + static_cast(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 diff --git a/new_editor/src/Core/UIEditorEnumFieldInteraction.cpp b/new_editor/src/Core/UIEditorEnumFieldInteraction.cpp new file mode 100644 index 00000000..d8773a54 --- /dev/null +++ b/new_editor/src/Core/UIEditorEnumFieldInteraction.cpp @@ -0,0 +1,190 @@ +#include + +#include + +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& 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(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 diff --git a/new_editor/src/Core/UIEditorNumberFieldInteraction.cpp b/new_editor/src/Core/UIEditorNumberFieldInteraction.cpp new file mode 100644 index 00000000..26a92bc8 --- /dev/null +++ b/new_editor/src/Core/UIEditorNumberFieldInteraction.cpp @@ -0,0 +1,477 @@ +#include + +#include + +#include +#include +#include + +namespace XCEngine::UI::Editor { + +namespace { + +using ::XCEngine::Input::KeyCode; +using ::XCEngine::UI::UIInputEvent; +using ::XCEngine::UI::UIInputEventType; +using ::XCEngine::UI::UIPointerButton; +using ::XCEngine::UI::Text::HandleKeyDown; +using ::XCEngine::UI::Text::InsertCharacter; +using ::XCEngine::UI::Editor::Widgets::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('0') && + character <= static_cast('9')) { + return true; + } + + if (character == static_cast('-') || + character == static_cast('+')) { + return true; + } + + return !spec.integerMode && character == static_cast('.'); +} + +double NormalizeValue(const UIEditorNumberFieldSpec& spec, double value) { + const double minValue = (std::min)(spec.minValue, spec.maxValue); + const double maxValue = (std::max)(spec.minValue, spec.maxValue); + value = (std::clamp)(value, minValue, maxValue); + if (spec.integerMode) { + value = static_cast(std::llround(value)); + } + return value; +} + +bool TryParseValue( + const UIEditorNumberFieldSpec& spec, + const std::string& text, + double& outValue) { + if (text.empty()) { + return false; + } + + try { + std::size_t consumed = 0u; + const double parsed = std::stod(text, &consumed); + if (consumed != text.size()) { + return false; + } + + outValue = NormalizeValue(spec, parsed); + return true; + } catch (...) { + return false; + } +} + +void SyncDisplayText( + UIEditorNumberFieldInteractionState& state, + const UIEditorNumberFieldSpec& spec) { + 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& 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(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(KeyCode::Enter)) { + eventResult.editStarted = BeginEdit(state, spec, false); + eventResult.consumed = eventResult.editStarted; + } else { + ApplyKeyboardStep( + state, + spec, + static_cast(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 diff --git a/new_editor/src/Widgets/UIEditorBoolField.cpp b/new_editor/src/Widgets/UIEditorBoolField.cpp new file mode 100644 index 00000000..79887e3a --- /dev/null +++ b/new_editor/src/Widgets/UIEditorBoolField.cpp @@ -0,0 +1,133 @@ +#include + +#include + +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 diff --git a/new_editor/src/Widgets/UIEditorEnumField.cpp b/new_editor/src/Widgets/UIEditorEnumField.cpp new file mode 100644 index 00000000..df59f4bc --- /dev/null +++ b/new_editor/src/Widgets/UIEditorEnumField.cpp @@ -0,0 +1,184 @@ +#include + +#include + +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 diff --git a/new_editor/src/Widgets/UIEditorNumberField.cpp b/new_editor/src/Widgets/UIEditorNumberField.cpp new file mode 100644 index 00000000..5047da17 --- /dev/null +++ b/new_editor/src/Widgets/UIEditorNumberField.cpp @@ -0,0 +1,284 @@ +#include + +#include +#include +#include +#include + +namespace XCEngine::UI::Editor::Widgets { + +namespace { + +using ::XCEngine::UI::UIDrawList; +using ::XCEngine::UI::UIPoint; +using ::XCEngine::UI::UIRect; + +float ClampNonNegative(float value) { + return (std::max)(0.0f, value); +} + +double NormalizeRangeValue(const UIEditorNumberFieldSpec& spec, double value) { + const double minValue = (std::min)(spec.minValue, spec.maxValue); + const double maxValue = (std::max)(spec.minValue, spec.maxValue); + value = (std::clamp)(value, minValue, maxValue); + if (spec.integerMode) { + value = static_cast(std::llround(value)); + } + return value; +} + +std::string FormatNumberValue(double value, bool integerMode) { + std::ostringstream stream = {}; + if (integerMode) { + stream << static_cast(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 diff --git a/tests/UI/Editor/integration/CMakeLists.txt b/tests/UI/Editor/integration/CMakeLists.txt index 7f192dae..70a491fd 100644 --- a/tests/UI/Editor/integration/CMakeLists.txt +++ b/tests/UI/Editor/integration/CMakeLists.txt @@ -58,6 +58,21 @@ if(TARGET editor_ui_property_grid_basic_validation) editor_ui_property_grid_basic_validation) 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) list(APPEND EDITOR_UI_INTEGRATION_TARGETS editor_ui_list_view_basic_validation) diff --git a/tests/UI/Editor/integration/README.md b/tests/UI/Editor/integration/README.md index 2ccc07bf..fd875d4b 100644 --- a/tests/UI/Editor/integration/README.md +++ b/tests/UI/Editor/integration/README.md @@ -24,6 +24,9 @@ Layout: - `shell/panel_frame_basic/`: panel frame layout/state/hit-test 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/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/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 @@ -90,6 +93,21 @@ Scenarios: Executable: `XCUIEditorPropertyGridBasicValidation.exe` 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` Build target: `editor_ui_status_bar_basic_validation` Executable: `XCUIEditorStatusBarBasicValidation.exe` @@ -195,6 +213,12 @@ Selected controls: - `shell/scroll_view_basic/` 把鼠标移到右侧日志区内滚轮滚动,拖拽 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/` Move the mouse across leading/trailing segments, click interactive segments, toggle focus/active, press `F12`. diff --git a/tests/UI/Editor/integration/shell/CMakeLists.txt b/tests/UI/Editor/integration/shell/CMakeLists.txt index 39f9a7f1..294b0e85 100644 --- a/tests/UI/Editor/integration/shell/CMakeLists.txt +++ b/tests/UI/Editor/integration/shell/CMakeLists.txt @@ -19,6 +19,15 @@ endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/property_grid_basic/CMakeLists.txt") add_subdirectory(property_grid_basic) 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") add_subdirectory(tree_view_basic) endif() diff --git a/tests/UI/Editor/integration/shell/bool_field_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/bool_field_basic/CMakeLists.txt new file mode 100644 index 00000000..055feab7 --- /dev/null +++ b/tests/UI/Editor/integration/shell/bool_field_basic/CMakeLists.txt @@ -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$<$: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" +) diff --git a/tests/UI/Editor/integration/shell/bool_field_basic/captures/.gitkeep b/tests/UI/Editor/integration/shell/bool_field_basic/captures/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/UI/Editor/integration/shell/bool_field_basic/captures/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/UI/Editor/integration/shell/bool_field_basic/main.cpp b/tests/UI/Editor/integration/shell/bool_field_basic/main.cpp new file mode 100644 index 00000000..0f986a8c --- /dev/null +++ b/tests/UI/Editor/integration/shell/bool_field_basic/main.cpp @@ -0,0 +1,670 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include "Host/AutoScreenshot.h" +#include "Host/NativeRenderer.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT +#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "." +#endif + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::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 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(KeyCode::Space); + case VK_RETURN: + return static_cast(KeyCode::Enter); + default: + return static_cast(KeyCode::None); + } +} + +ScenarioLayout BuildScenarioLayout(float width, float height) { + constexpr float margin = 20.0f; + constexpr float leftWidth = 430.0f; + constexpr float gap = 16.0f; + + ScenarioLayout 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(message.wParam); + } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->OnResize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + + case WM_MOUSEMOVE: + if (app != nullptr) { + app->HandleMouseMove( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_MOUSELEAVE: + if (app != nullptr) { + app->HandleMouseLeave(); + return 0; + } + break; + + case WM_LBUTTONDOWN: + if (app != nullptr) { + app->HandleLeftButtonDown( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandleLeftButtonUp( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr) { + if (wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + app->m_lastResult = "已请求截图,输出到 captures/latest.png"; + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + + const std::int32_t keyCode = MapBoolFieldKey(static_cast(wParam)); + if (keyCode != static_cast(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((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((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 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((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + const ScenarioLayout layout = BuildScenarioLayout(width, height); + 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(width), + static_cast(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); +} diff --git a/tests/UI/Editor/integration/shell/enum_field_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/enum_field_basic/CMakeLists.txt new file mode 100644 index 00000000..afe19f80 --- /dev/null +++ b/tests/UI/Editor/integration/shell/enum_field_basic/CMakeLists.txt @@ -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$<$: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" +) diff --git a/tests/UI/Editor/integration/shell/enum_field_basic/captures/.gitkeep b/tests/UI/Editor/integration/shell/enum_field_basic/captures/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/UI/Editor/integration/shell/enum_field_basic/captures/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/UI/Editor/integration/shell/enum_field_basic/main.cpp b/tests/UI/Editor/integration/shell/enum_field_basic/main.cpp new file mode 100644 index 00000000..5621981a --- /dev/null +++ b/tests/UI/Editor/integration/shell/enum_field_basic/main.cpp @@ -0,0 +1,673 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include "Host/AutoScreenshot.h" +#include "Host/NativeRenderer.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT +#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "." +#endif + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::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 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(KeyCode::Left); + case VK_RIGHT: + return static_cast(KeyCode::Right); + case VK_HOME: + return static_cast(KeyCode::Home); + case VK_END: + return static_cast(KeyCode::End); + case VK_RETURN: + return static_cast(KeyCode::Enter); + default: + return static_cast(KeyCode::None); + } +} + +ScenarioLayout BuildScenarioLayout(float width, float height) { + constexpr float margin = 20.0f; + constexpr float leftWidth = 430.0f; + constexpr float gap = 16.0f; + + ScenarioLayout 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(message.wParam); + } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->OnResize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + + case WM_MOUSEMOVE: + if (app != nullptr) { + app->HandleMouseMove( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_MOUSELEAVE: + if (app != nullptr) { + app->HandleMouseLeave(); + return 0; + } + break; + + case WM_LBUTTONDOWN: + if (app != nullptr) { + app->HandleLeftButtonDown( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandleLeftButtonUp( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr) { + if (wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + app->m_lastResult = "已请求截图,输出到 captures/latest.png"; + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + + const std::int32_t keyCode = MapEnumFieldKey(static_cast(wParam)); + if (keyCode != static_cast(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((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((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 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((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + const ScenarioLayout layout = BuildScenarioLayout(width, height); + 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(width), + static_cast(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); +} diff --git a/tests/UI/Editor/integration/shell/number_field_basic/CMakeLists.txt b/tests/UI/Editor/integration/shell/number_field_basic/CMakeLists.txt new file mode 100644 index 00000000..599277b1 --- /dev/null +++ b/tests/UI/Editor/integration/shell/number_field_basic/CMakeLists.txt @@ -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$<$: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" +) diff --git a/tests/UI/Editor/integration/shell/number_field_basic/captures/.gitkeep b/tests/UI/Editor/integration/shell/number_field_basic/captures/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/tests/UI/Editor/integration/shell/number_field_basic/captures/.gitkeep @@ -0,0 +1 @@ + diff --git a/tests/UI/Editor/integration/shell/number_field_basic/main.cpp b/tests/UI/Editor/integration/shell/number_field_basic/main.cpp new file mode 100644 index 00000000..b0d56790 --- /dev/null +++ b/tests/UI/Editor/integration/shell/number_field_basic/main.cpp @@ -0,0 +1,727 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include +#include "Host/AutoScreenshot.h" +#include "Host/NativeRenderer.h" + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT +#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "." +#endif + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::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 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(KeyCode::Left); + case VK_RIGHT: + return static_cast(KeyCode::Right); + case VK_UP: + return static_cast(KeyCode::Up); + case VK_DOWN: + return static_cast(KeyCode::Down); + case VK_HOME: + return static_cast(KeyCode::Home); + case VK_END: + return static_cast(KeyCode::End); + case VK_RETURN: + return static_cast(KeyCode::Enter); + case VK_ESCAPE: + return static_cast(KeyCode::Escape); + default: + return static_cast(KeyCode::None); + } +} + +ScenarioLayout BuildScenarioLayout(float width, float height) { + constexpr float margin = 20.0f; + constexpr float leftWidth = 430.0f; + constexpr float gap = 16.0f; + + ScenarioLayout 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(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(message.wParam); + } + +private: + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { + if (message == WM_NCCREATE) { + const auto* createStruct = reinterpret_cast(lParam); + auto* app = reinterpret_cast(createStruct->lpCreateParams); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast(app)); + return TRUE; + } + + auto* app = reinterpret_cast(GetWindowLongPtrW(hwnd, GWLP_USERDATA)); + switch (message) { + case WM_SIZE: + if (app != nullptr && wParam != SIZE_MINIMIZED) { + app->OnResize(static_cast(LOWORD(lParam)), static_cast(HIWORD(lParam))); + } + return 0; + + case WM_MOUSEMOVE: + if (app != nullptr) { + app->HandleMouseMove( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_MOUSELEAVE: + if (app != nullptr) { + app->HandleMouseLeave(); + return 0; + } + break; + + case WM_LBUTTONDOWN: + if (app != nullptr) { + app->HandleLeftButtonDown( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_LBUTTONUP: + if (app != nullptr) { + app->HandleLeftButtonUp( + static_cast(GET_X_LPARAM(lParam)), + static_cast(GET_Y_LPARAM(lParam))); + return 0; + } + break; + + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + if (app != nullptr) { + if (wParam == VK_F12) { + app->m_autoScreenshot.RequestCapture("manual_f12"); + app->m_lastResult = "已请求截图,输出到 captures/latest.png"; + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + } + + const std::int32_t keyCode = MapNumberFieldKey(static_cast(wParam)); + if (keyCode != static_cast(KeyCode::None)) { + app->HandleKeyDown(keyCode); + return 0; + } + } + break; + + case WM_CHAR: + if (app != nullptr) { + app->HandleCharacter(static_cast(wParam)); + return 0; + } + break; + + case WM_PAINT: + if (app != nullptr) { + PAINTSTRUCT paintStruct = {}; + BeginPaint(hwnd, &paintStruct); + app->RenderFrame(); + EndPaint(hwnd, &paintStruct); + return 0; + } + break; + + case WM_ERASEBKGND: + return 1; + + case WM_DESTROY: + if (app != nullptr) { + app->m_hwnd = nullptr; + } + PostQuitMessage(0); + return 0; + + default: + break; + } + + return DefWindowProcW(hwnd, message, wParam, lParam); + } + + bool Initialize(HINSTANCE hInstance, int nCmdShow) { + WNDCLASSEXW windowClass = {}; + windowClass.cbSize = sizeof(windowClass); + windowClass.style = CS_HREDRAW | CS_VREDRAW; + windowClass.lpfnWndProc = &ScenarioApp::WndProc; + windowClass.hInstance = hInstance; + windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW); + windowClass.lpszClassName = kWindowClassName; + + m_windowClassAtom = RegisterClassExW(&windowClass); + if (m_windowClassAtom == 0) { + return false; + } + + m_hwnd = CreateWindowExW( + 0, + kWindowClassName, + kWindowTitle, + WS_OVERLAPPEDWINDOW | WS_VISIBLE, + CW_USEDEFAULT, + CW_USEDEFAULT, + 1480, + 920, + nullptr, + nullptr, + hInstance, + this); + if (m_hwnd == nullptr) { + return false; + } + + ShowWindow(m_hwnd, nCmdShow); + UpdateWindow(m_hwnd); + + if (!m_renderer.Initialize(m_hwnd)) { + return false; + } + + m_captureRoot = + ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/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((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((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 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((std::max)(1L, clientRect.right - clientRect.left)); + const float height = static_cast((std::max)(1L, clientRect.bottom - clientRect.top)); + const ScenarioLayout layout = BuildScenarioLayout(width, height); + 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(width), + static_cast(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); +} diff --git a/tests/UI/Editor/unit/CMakeLists.txt b/tests/UI/Editor/unit/CMakeLists.txt index be8cdf5e..6e6f24e0 100644 --- a/tests/UI/Editor/unit/CMakeLists.txt +++ b/tests/UI/Editor/unit/CMakeLists.txt @@ -17,11 +17,17 @@ set(EDITOR_UI_UNIT_TEST_SOURCES test_ui_editor_shell_compose.cpp test_ui_editor_shell_interaction.cpp test_ui_editor_collection_primitives.cpp + test_ui_editor_bool_field.cpp + test_ui_editor_bool_field_interaction.cpp test_ui_editor_dock_host.cpp test_ui_editor_list_view.cpp test_ui_editor_list_view_interaction.cpp test_ui_editor_panel_chrome.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_interaction.cpp test_ui_editor_status_bar.cpp @@ -63,4 +69,6 @@ if(MSVC) endif() include(GoogleTest) -gtest_discover_tests(editor_ui_tests) +gtest_discover_tests(editor_ui_tests + DISCOVERY_MODE PRE_TEST +) diff --git a/tests/UI/Editor/unit/test_ui_editor_bool_field.cpp b/tests/UI/Editor/unit/test_ui_editor_bool_field.cpp new file mode 100644 index 00000000..4e02a57c --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_bool_field.cpp @@ -0,0 +1,54 @@ +#include + +#include + +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 diff --git a/tests/UI/Editor/unit/test_ui_editor_bool_field_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_bool_field_interaction.cpp new file mode 100644 index 00000000..99915955 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_bool_field_interaction.cpp @@ -0,0 +1,109 @@ +#include + +#include + +#include + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::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(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); +} diff --git a/tests/UI/Editor/unit/test_ui_editor_enum_field.cpp b/tests/UI/Editor/unit/test_ui_editor_enum_field.cpp new file mode 100644 index 00000000..f4fd0746 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_enum_field.cpp @@ -0,0 +1,35 @@ +#include + +#include + +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 diff --git a/tests/UI/Editor/unit/test_ui_editor_enum_field_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_enum_field_interaction.cpp new file mode 100644 index 00000000..856bad14 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_enum_field_interaction.cpp @@ -0,0 +1,84 @@ +#include + +#include + +#include + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::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(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); +} diff --git a/tests/UI/Editor/unit/test_ui_editor_number_field.cpp b/tests/UI/Editor/unit/test_ui_editor_number_field.cpp new file mode 100644 index 00000000..d78248d9 --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_number_field.cpp @@ -0,0 +1,49 @@ +#include + +#include + +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 diff --git a/tests/UI/Editor/unit/test_ui_editor_number_field_interaction.cpp b/tests/UI/Editor/unit/test_ui_editor_number_field_interaction.cpp new file mode 100644 index 00000000..b502252f --- /dev/null +++ b/tests/UI/Editor/unit/test_ui_editor_number_field_interaction.cpp @@ -0,0 +1,155 @@ +#include + +#include + +#include + +namespace { + +using XCEngine::Input::KeyCode; +using XCEngine::UI::UIInputEvent; +using XCEngine::UI::UIInputEventType; +using XCEngine::UI::UIPoint; +using XCEngine::UI::UIPointerButton; +using XCEngine::UI::UIRect; +using XCEngine::UI::Editor::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(keyCode); + return event; +} + +UIInputEvent MakeCharacter(char character) { + UIInputEvent event = {}; + event.type = UIInputEventType::Character; + event.character = static_cast(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"); +}