Add XCUI command routing and widget state models

This commit is contained in:
2026-04-05 12:10:55 +08:00
parent 511e94fd30
commit 68c4c80b06
18 changed files with 1329 additions and 9 deletions

View File

@@ -6,6 +6,8 @@ set(UI_TEST_SOURCES
test_ui_core.cpp
test_ui_editor_collection_primitives.cpp
test_ui_expansion_model.cpp
test_ui_keyboard_navigation_model.cpp
test_ui_property_edit_model.cpp
test_layout_engine.cpp
test_ui_selection_model.cpp
test_ui_runtime.cpp

View File

@@ -0,0 +1,101 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Widgets/UIKeyboardNavigationModel.h>
namespace {
using XCEngine::UI::Widgets::UIKeyboardNavigationModel;
TEST(UIKeyboardNavigationModelTest, EmptyModelStartsWithoutCurrentIndexOrAnchor) {
UIKeyboardNavigationModel navigation = {};
EXPECT_EQ(navigation.GetItemCount(), 0u);
EXPECT_FALSE(navigation.HasCurrentIndex());
EXPECT_EQ(navigation.GetCurrentIndex(), UIKeyboardNavigationModel::InvalidIndex);
EXPECT_FALSE(navigation.HasSelectionAnchor());
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), UIKeyboardNavigationModel::InvalidIndex);
EXPECT_FALSE(navigation.MoveNext());
EXPECT_FALSE(navigation.MovePrevious());
EXPECT_FALSE(navigation.MoveHome());
EXPECT_FALSE(navigation.MoveEnd());
}
TEST(UIKeyboardNavigationModelTest, SetCurrentIndexAndDirectionalMovesTrackCurrentIndexAndAnchor) {
UIKeyboardNavigationModel navigation = {};
ASSERT_TRUE(navigation.SetItemCount(4u));
EXPECT_TRUE(navigation.SetCurrentIndex(1u));
EXPECT_EQ(navigation.GetCurrentIndex(), 1u);
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 1u);
EXPECT_TRUE(navigation.MoveNext());
EXPECT_EQ(navigation.GetCurrentIndex(), 2u);
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 2u);
EXPECT_TRUE(navigation.MoveEnd());
EXPECT_EQ(navigation.GetCurrentIndex(), 3u);
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 3u);
EXPECT_FALSE(navigation.MoveNext());
EXPECT_TRUE(navigation.MoveHome());
EXPECT_EQ(navigation.GetCurrentIndex(), 0u);
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 0u);
EXPECT_FALSE(navigation.MovePrevious());
}
TEST(UIKeyboardNavigationModelTest, MovePreviousAndEndSeedNavigationWhenCurrentIndexIsUnset) {
UIKeyboardNavigationModel navigation = {};
ASSERT_TRUE(navigation.SetItemCount(5u));
EXPECT_TRUE(navigation.MovePrevious());
EXPECT_EQ(navigation.GetCurrentIndex(), 4u);
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 4u);
EXPECT_TRUE(navigation.ClearCurrentIndex());
EXPECT_TRUE(navigation.ClearSelectionAnchor());
EXPECT_FALSE(navigation.HasCurrentIndex());
EXPECT_FALSE(navigation.HasSelectionAnchor());
EXPECT_TRUE(navigation.MoveEnd());
EXPECT_EQ(navigation.GetCurrentIndex(), 4u);
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 4u);
}
TEST(UIKeyboardNavigationModelTest, ExplicitAnchorCanBePreservedUntilNavigationCollapsesIt) {
UIKeyboardNavigationModel navigation = {};
ASSERT_TRUE(navigation.SetItemCount(6u));
EXPECT_TRUE(navigation.SetSelectionAnchorIndex(1u));
EXPECT_TRUE(navigation.SetCurrentIndex(4u, false));
EXPECT_EQ(navigation.GetCurrentIndex(), 4u);
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 1u);
EXPECT_TRUE(navigation.MovePrevious());
EXPECT_EQ(navigation.GetCurrentIndex(), 3u);
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 3u);
}
TEST(UIKeyboardNavigationModelTest, ItemCountChangesClampCurrentIndexAndSelectionAnchor) {
UIKeyboardNavigationModel navigation = {};
ASSERT_TRUE(navigation.SetItemCount(5u));
ASSERT_TRUE(navigation.SetSelectionAnchorIndex(3u));
ASSERT_TRUE(navigation.SetCurrentIndex(4u, false));
EXPECT_TRUE(navigation.SetItemCount(4u));
EXPECT_EQ(navigation.GetCurrentIndex(), 3u);
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 3u);
EXPECT_FALSE(navigation.SetCurrentIndex(3u, false));
EXPECT_TRUE(navigation.SetSelectionAnchorIndex(2u));
EXPECT_TRUE(navigation.SetItemCount(2u));
EXPECT_EQ(navigation.GetCurrentIndex(), 1u);
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), 1u);
EXPECT_TRUE(navigation.SetItemCount(0u));
EXPECT_FALSE(navigation.HasCurrentIndex());
EXPECT_EQ(navigation.GetCurrentIndex(), UIKeyboardNavigationModel::InvalidIndex);
EXPECT_FALSE(navigation.HasSelectionAnchor());
EXPECT_EQ(navigation.GetSelectionAnchorIndex(), UIKeyboardNavigationModel::InvalidIndex);
}
} // namespace

View File

@@ -0,0 +1,80 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Widgets/UIPropertyEditModel.h>
namespace {
using XCEngine::UI::Widgets::UIPropertyEditModel;
TEST(UIPropertyEditModelTest, BeginEditTracksActiveFieldAndInitialValue) {
UIPropertyEditModel model = {};
EXPECT_FALSE(model.HasActiveEdit());
EXPECT_TRUE(model.GetActiveFieldId().empty());
EXPECT_TRUE(model.GetStagedValue().empty());
EXPECT_FALSE(model.IsDirty());
EXPECT_FALSE(model.BeginEdit("", "12.0"));
EXPECT_TRUE(model.BeginEdit("transform.position.x", "12.0"));
EXPECT_TRUE(model.HasActiveEdit());
EXPECT_EQ(model.GetActiveFieldId(), "transform.position.x");
EXPECT_EQ(model.GetStagedValue(), "12.0");
EXPECT_FALSE(model.IsDirty());
EXPECT_FALSE(model.BeginEdit("transform.position.x", "12.0"));
}
TEST(UIPropertyEditModelTest, UpdateStagedValueTracksDirtyAgainstBaseline) {
UIPropertyEditModel model = {};
EXPECT_FALSE(model.UpdateStagedValue("3.5"));
ASSERT_TRUE(model.BeginEdit("light.intensity", "1.0"));
EXPECT_TRUE(model.UpdateStagedValue("3.5"));
EXPECT_EQ(model.GetStagedValue(), "3.5");
EXPECT_TRUE(model.IsDirty());
EXPECT_FALSE(model.UpdateStagedValue("3.5"));
EXPECT_TRUE(model.UpdateStagedValue("1.0"));
EXPECT_EQ(model.GetStagedValue(), "1.0");
EXPECT_FALSE(model.IsDirty());
}
TEST(UIPropertyEditModelTest, CommitEditReturnsPayloadAndClearsState) {
UIPropertyEditModel model = {};
ASSERT_TRUE(model.BeginEdit("material.albedo", "#ffffff"));
ASSERT_TRUE(model.UpdateStagedValue("#ffcc00"));
std::string committedFieldId = {};
std::string committedValue = {};
EXPECT_TRUE(model.CommitEdit(&committedFieldId, &committedValue));
EXPECT_EQ(committedFieldId, "material.albedo");
EXPECT_EQ(committedValue, "#ffcc00");
EXPECT_FALSE(model.HasActiveEdit());
EXPECT_TRUE(model.GetActiveFieldId().empty());
EXPECT_TRUE(model.GetStagedValue().empty());
EXPECT_FALSE(model.IsDirty());
EXPECT_FALSE(model.CommitEdit(&committedFieldId, &committedValue));
}
TEST(UIPropertyEditModelTest, CancelEditDropsStagedChangesAndResetsSession) {
UIPropertyEditModel model = {};
ASSERT_TRUE(model.BeginEdit("camera.fov", "60"));
ASSERT_TRUE(model.UpdateStagedValue("75"));
ASSERT_TRUE(model.IsDirty());
EXPECT_TRUE(model.CancelEdit());
EXPECT_FALSE(model.HasActiveEdit());
EXPECT_TRUE(model.GetActiveFieldId().empty());
EXPECT_TRUE(model.GetStagedValue().empty());
EXPECT_FALSE(model.IsDirty());
EXPECT_FALSE(model.CancelEdit());
EXPECT_TRUE(model.BeginEdit("camera.nearClip", "0.3"));
EXPECT_EQ(model.GetActiveFieldId(), "camera.nearClip");
EXPECT_EQ(model.GetStagedValue(), "0.3");
EXPECT_FALSE(model.IsDirty());
}
} // namespace