Add editor typed field widgets and validation scenarios

This commit is contained in:
2026-04-07 17:55:42 +08:00
parent e22ce763c2
commit 5a3232c099
32 changed files with 4635 additions and 1 deletions

View File

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

View File

@@ -0,0 +1,54 @@
#include <gtest/gtest.h>
#include <XCEditor/Widgets/UIEditorBoolField.h>
namespace {
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolFieldBackground;
using XCEngine::UI::Editor::Widgets::AppendUIEditorBoolFieldForeground;
using XCEngine::UI::Editor::Widgets::BuildUIEditorBoolFieldLayout;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorBoolField;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldState;
TEST(UIEditorBoolFieldTest, LayoutBuildsLabelAndToggleRects) {
UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false };
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
EXPECT_GT(layout.labelRect.width, 0.0f);
EXPECT_FLOAT_EQ(layout.toggleRect.width, 42.0f);
EXPECT_GT(layout.knobRect.x, layout.toggleRect.x);
}
TEST(UIEditorBoolFieldTest, HitTestResolvesToggleAndRow) {
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
const auto toggleHit = HitTestUIEditorBoolField(
layout,
UIPoint(layout.toggleRect.x + 4.0f, layout.toggleRect.y + 4.0f));
EXPECT_EQ(toggleHit.kind, UIEditorBoolFieldHitTargetKind::Toggle);
const auto rowHit = HitTestUIEditorBoolField(layout, UIPoint(20.0f, 16.0f));
EXPECT_EQ(rowHit.kind, UIEditorBoolFieldHitTargetKind::Row);
}
TEST(UIEditorBoolFieldTest, BackgroundAndForegroundEmitStableCommands) {
UIEditorBoolFieldSpec spec = { "visible", "Visible", true, false };
UIEditorBoolFieldState state = {};
state.focused = true;
state.hoveredTarget = UIEditorBoolFieldHitTargetKind::Toggle;
XCEngine::UI::UIDrawData drawData = {};
auto& drawList = drawData.EmplaceDrawList("BoolField");
const auto layout = BuildUIEditorBoolFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
AppendUIEditorBoolFieldBackground(drawList, layout, spec, state);
AppendUIEditorBoolFieldForeground(drawList, layout, spec);
ASSERT_GE(drawList.GetCommands().size(), 6u);
}
} // namespace

View File

@@ -0,0 +1,109 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorBoolFieldInteraction.h>
#include <XCEngine/Input/InputTypes.h>
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::UIEditorBoolFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorBoolFieldInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorBoolFieldSpec;
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = UIPoint(x, y);
event.pointerButton = button;
return event;
}
UIInputEvent MakeKey(KeyCode keyCode) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(keyCode);
return event;
}
} // namespace
TEST(UIEditorBoolFieldInteractionTest, ClickToggleFlipsValue) {
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
UIEditorBoolFieldInteractionState state = {};
bool value = false;
auto frame = UpdateUIEditorBoolFieldInteraction(
state,
value,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{});
const auto toggle = frame.layout.toggleRect;
frame = UpdateUIEditorBoolFieldInteraction(
state,
value,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{
MakePointer(UIInputEventType::PointerButtonDown, toggle.x + 4.0f, toggle.y + 4.0f, UIPointerButton::Left),
MakePointer(UIInputEventType::PointerButtonUp, toggle.x + 4.0f, toggle.y + 4.0f, UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.valueChanged);
EXPECT_TRUE(value);
}
TEST(UIEditorBoolFieldInteractionTest, SpaceAndEnterToggleWhenFocused) {
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
UIEditorBoolFieldInteractionState state = {};
state.fieldState.focused = true;
bool value = false;
auto frame = UpdateUIEditorBoolFieldInteraction(
state,
value,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{ MakeKey(KeyCode::Space) });
EXPECT_TRUE(frame.result.valueChanged);
EXPECT_TRUE(value);
frame = UpdateUIEditorBoolFieldInteraction(
state,
value,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{ MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.valueChanged);
EXPECT_FALSE(value);
}
TEST(UIEditorBoolFieldInteractionTest, HoverTracksToggleHitTarget) {
UIEditorBoolFieldSpec spec = { "visible", "Visible", false, false };
UIEditorBoolFieldInteractionState state = {};
bool value = false;
auto frame = UpdateUIEditorBoolFieldInteraction(
state,
value,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{});
const auto toggle = frame.layout.toggleRect;
frame = UpdateUIEditorBoolFieldInteraction(
state,
value,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{ MakePointer(UIInputEventType::PointerMove, toggle.x + 4.0f, toggle.y + 4.0f) });
EXPECT_EQ(frame.result.hitTarget.kind, UIEditorBoolFieldHitTargetKind::Toggle);
}

View File

@@ -0,0 +1,35 @@
#include <gtest/gtest.h>
#include <XCEditor/Widgets/UIEditorEnumField.h>
namespace {
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Widgets::BuildUIEditorEnumFieldLayout;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorEnumField;
using XCEngine::UI::Editor::Widgets::ResolveUIEditorEnumFieldValueText;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
TEST(UIEditorEnumFieldTest, ValueTextUsesSelectedOption) {
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
EXPECT_EQ(ResolveUIEditorEnumFieldValueText(spec), "Cutout");
}
TEST(UIEditorEnumFieldTest, HitTestResolvesPreviousNextAndValueBox) {
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
const auto layout = BuildUIEditorEnumFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
EXPECT_EQ(
HitTestUIEditorEnumField(layout, UIPoint(layout.previousRect.x + 2.0f, layout.previousRect.y + 2.0f)).kind,
UIEditorEnumFieldHitTargetKind::PreviousButton);
EXPECT_EQ(
HitTestUIEditorEnumField(layout, UIPoint(layout.nextRect.x + 2.0f, layout.nextRect.y + 2.0f)).kind,
UIEditorEnumFieldHitTargetKind::NextButton);
EXPECT_EQ(
HitTestUIEditorEnumField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
UIEditorEnumFieldHitTargetKind::ValueBox);
}
} // namespace

View File

@@ -0,0 +1,84 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorEnumFieldInteraction.h>
#include <XCEngine/Input/InputTypes.h>
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::UIEditorEnumFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorEnumFieldInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorEnumFieldSpec;
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = UIPoint(x, y);
event.pointerButton = button;
return event;
}
UIInputEvent MakeKey(KeyCode keyCode) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(keyCode);
return event;
}
} // namespace
TEST(UIEditorEnumFieldInteractionTest, ClickButtonsAdjustSelection) {
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
UIEditorEnumFieldInteractionState state = {};
std::size_t selectedIndex = 1u;
auto frame = UpdateUIEditorEnumFieldInteraction(
state,
selectedIndex,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{});
frame = UpdateUIEditorEnumFieldInteraction(
state,
selectedIndex,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{
MakePointer(UIInputEventType::PointerButtonDown, frame.layout.nextRect.x + 2.0f, frame.layout.nextRect.y + 2.0f, UIPointerButton::Left),
MakePointer(UIInputEventType::PointerButtonUp, frame.layout.nextRect.x + 2.0f, frame.layout.nextRect.y + 2.0f, UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.selectionChanged);
EXPECT_EQ(selectedIndex, 2u);
}
TEST(UIEditorEnumFieldInteractionTest, KeyboardControlsMoveToEnds) {
UIEditorEnumFieldSpec spec = { "blend", "Blend", { "Opaque", "Cutout", "Fade" }, 1u, false };
UIEditorEnumFieldInteractionState state = {};
state.fieldState.focused = true;
std::size_t selectedIndex = 1u;
auto frame = UpdateUIEditorEnumFieldInteraction(
state,
selectedIndex,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{ MakeKey(KeyCode::Home) });
EXPECT_TRUE(frame.result.selectionChanged);
EXPECT_EQ(selectedIndex, 0u);
frame = UpdateUIEditorEnumFieldInteraction(
state,
selectedIndex,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
spec,
{ MakeKey(KeyCode::End) });
EXPECT_TRUE(frame.result.selectionChanged);
EXPECT_EQ(selectedIndex, 2u);
}

View File

@@ -0,0 +1,49 @@
#include <gtest/gtest.h>
#include <XCEditor/Widgets/UIEditorNumberField.h>
namespace {
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Widgets::BuildUIEditorNumberFieldLayout;
using XCEngine::UI::Editor::Widgets::FormatUIEditorNumberFieldValue;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorNumberField;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
TEST(UIEditorNumberFieldTest, FormatSupportsIntegerAndFloatMode) {
UIEditorNumberFieldSpec integerSpec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
UIEditorNumberFieldSpec floatSpec = { "scale", "Scale", 1.25, 0.25, 0.0, 2.0, false, false };
EXPECT_EQ(FormatUIEditorNumberFieldValue(integerSpec), "7");
EXPECT_EQ(FormatUIEditorNumberFieldValue(floatSpec), "1.25");
}
TEST(UIEditorNumberFieldTest, LayoutBuildsValueAndStepperRects) {
UIEditorNumberFieldSpec spec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
const auto layout = BuildUIEditorNumberFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
EXPECT_GT(layout.labelRect.width, 0.0f);
EXPECT_GT(layout.controlRect.width, 0.0f);
EXPECT_GT(layout.valueRect.width, 0.0f);
EXPECT_FLOAT_EQ(layout.decrementRect.width, 22.0f);
EXPECT_FLOAT_EQ(layout.incrementRect.width, 22.0f);
}
TEST(UIEditorNumberFieldTest, HitTestResolvesButtonsAndValueBox) {
UIEditorNumberFieldSpec spec = { "queue", "Queue", 7.0, 1.0, 0.0, 10.0, true, false };
const auto layout = BuildUIEditorNumberFieldLayout(UIRect(0.0f, 0.0f, 360.0f, 32.0f), spec);
EXPECT_EQ(
HitTestUIEditorNumberField(layout, UIPoint(layout.decrementRect.x + 2.0f, layout.decrementRect.y + 2.0f)).kind,
UIEditorNumberFieldHitTargetKind::DecrementButton);
EXPECT_EQ(
HitTestUIEditorNumberField(layout, UIPoint(layout.incrementRect.x + 2.0f, layout.incrementRect.y + 2.0f)).kind,
UIEditorNumberFieldHitTargetKind::IncrementButton);
EXPECT_EQ(
HitTestUIEditorNumberField(layout, UIPoint(layout.valueRect.x + 4.0f, layout.valueRect.y + 4.0f)).kind,
UIEditorNumberFieldHitTargetKind::ValueBox);
}
} // namespace

View File

@@ -0,0 +1,155 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorNumberFieldInteraction.h>
#include <XCEngine/Input/InputTypes.h>
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::UIEditorNumberFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorNumberFieldInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorNumberFieldSpec;
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = UIPoint(x, y);
event.pointerButton = button;
return event;
}
UIInputEvent MakeKey(KeyCode keyCode) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(keyCode);
return event;
}
UIInputEvent MakeCharacter(char character) {
UIInputEvent event = {};
event.type = UIInputEventType::Character;
event.character = static_cast<std::uint32_t>(character);
return event;
}
} // namespace
TEST(UIEditorNumberFieldInteractionTest, ClickStepperButtonsAdjustValue) {
UIEditorNumberFieldSpec spec = { "queue", "Queue", 2.0, 1.0, 0.0, 5.0, true, false };
UIEditorNumberFieldInteractionState state = {};
auto frame = UpdateUIEditorNumberFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{});
frame = UpdateUIEditorNumberFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{
MakePointer(
UIInputEventType::PointerButtonDown,
frame.layout.incrementRect.x + 2.0f,
frame.layout.incrementRect.y + 2.0f,
UIPointerButton::Left),
MakePointer(
UIInputEventType::PointerButtonUp,
frame.layout.incrementRect.x + 2.0f,
frame.layout.incrementRect.y + 2.0f,
UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.stepApplied);
EXPECT_TRUE(frame.result.valueChanged);
EXPECT_DOUBLE_EQ(spec.value, 3.0);
EXPECT_DOUBLE_EQ(frame.result.valueAfter, 3.0);
}
TEST(UIEditorNumberFieldInteractionTest, KeyboardStepAndBoundsWorkWhenFocused) {
UIEditorNumberFieldSpec spec = { "queue", "Queue", 2.0, 1.0, 0.0, 5.0, true, false };
UIEditorNumberFieldInteractionState state = {};
state.numberFieldState.focused = true;
auto frame = UpdateUIEditorNumberFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeKey(KeyCode::Right) });
EXPECT_TRUE(frame.result.stepApplied);
EXPECT_DOUBLE_EQ(spec.value, 3.0);
frame = UpdateUIEditorNumberFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeKey(KeyCode::Home) });
EXPECT_TRUE(frame.result.stepApplied);
EXPECT_DOUBLE_EQ(spec.value, 0.0);
frame = UpdateUIEditorNumberFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeKey(KeyCode::End) });
EXPECT_TRUE(frame.result.stepApplied);
EXPECT_DOUBLE_EQ(spec.value, 5.0);
}
TEST(UIEditorNumberFieldInteractionTest, EnterStartsEditingAndCharacterInputCommitsValue) {
UIEditorNumberFieldSpec spec = { "count", "Count", 7.0, 1.0, 0.0, 1000.0, true, false };
UIEditorNumberFieldInteractionState state = {};
state.numberFieldState.focused = true;
auto frame = UpdateUIEditorNumberFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.numberFieldState.editing);
frame = UpdateUIEditorNumberFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeCharacter('4'), MakeCharacter('2'), MakeKey(KeyCode::Enter) });
EXPECT_TRUE(frame.result.editCommitted);
EXPECT_TRUE(frame.result.valueChanged);
EXPECT_FALSE(state.numberFieldState.editing);
EXPECT_EQ(frame.result.committedText, "742");
EXPECT_DOUBLE_EQ(spec.value, 742.0);
}
TEST(UIEditorNumberFieldInteractionTest, CharacterInputCanStartEditingAndEscapeCancels) {
UIEditorNumberFieldSpec spec = { "count", "Count", 7.0, 1.0, 0.0, 200.0, true, false };
UIEditorNumberFieldInteractionState state = {};
state.numberFieldState.focused = true;
auto frame = UpdateUIEditorNumberFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeCharacter('9') });
EXPECT_TRUE(frame.result.editStarted);
EXPECT_TRUE(state.numberFieldState.editing);
EXPECT_EQ(state.numberFieldState.displayText, "9");
frame = UpdateUIEditorNumberFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeKey(KeyCode::Escape) });
EXPECT_TRUE(frame.result.editCanceled);
EXPECT_FALSE(state.numberFieldState.editing);
EXPECT_DOUBLE_EQ(spec.value, 7.0);
EXPECT_EQ(state.numberFieldState.displayText, "7");
}