engine: sync editor rendering and ui changes
This commit is contained in:
@@ -191,4 +191,16 @@ TEST(ResourceHandle, EqualityOperators) {
|
||||
handle2.Reset();
|
||||
}
|
||||
|
||||
TEST(ResourceHandle, ResetDoesNotDereferenceDestroyedResourcePointer) {
|
||||
TestResource* resource = new TestResource();
|
||||
resource->Initialize({ "Test", "test.png", ResourceGUID(321), 100 });
|
||||
|
||||
ResourceHandle<TestResource> handle(resource);
|
||||
delete resource;
|
||||
|
||||
handle.Reset();
|
||||
EXPECT_EQ(handle.Get(), nullptr);
|
||||
EXPECT_EQ(handle.GetGUID().value, 0u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Layout/LayoutEngine.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::UISize;
|
||||
using XCEngine::UI::Layout::ArrangeOverlayLayout;
|
||||
using XCEngine::UI::Layout::ArrangeStackLayout;
|
||||
using XCEngine::UI::Layout::MeasureOverlayLayout;
|
||||
using XCEngine::UI::Layout::MeasureStackLayout;
|
||||
using XCEngine::UI::Layout::UILayoutAlignment;
|
||||
using XCEngine::UI::Layout::UILayoutAxis;
|
||||
using XCEngine::UI::Layout::UILayoutConstraints;
|
||||
using XCEngine::UI::Layout::UILayoutItem;
|
||||
using XCEngine::UI::Layout::UILayoutLength;
|
||||
using XCEngine::UI::Layout::UILayoutThickness;
|
||||
using XCEngine::UI::Layout::UIOverlayLayoutOptions;
|
||||
using XCEngine::UI::Layout::UIStackLayoutOptions;
|
||||
|
||||
void ExpectRect(
|
||||
const UIRect& rect,
|
||||
float x,
|
||||
float y,
|
||||
float width,
|
||||
float height) {
|
||||
EXPECT_FLOAT_EQ(rect.x, x);
|
||||
EXPECT_FLOAT_EQ(rect.y, y);
|
||||
EXPECT_FLOAT_EQ(rect.width, width);
|
||||
EXPECT_FLOAT_EQ(rect.height, height);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UI_Layout, MeasureHorizontalStackAccumulatesSpacingPaddingAndCrossExtent) {
|
||||
UIStackLayoutOptions options = {};
|
||||
options.axis = UILayoutAxis::Horizontal;
|
||||
options.spacing = 5.0f;
|
||||
options.padding = UILayoutThickness::Symmetric(10.0f, 6.0f);
|
||||
|
||||
std::vector<UILayoutItem> items(2);
|
||||
items[0].desiredContentSize = UISize(40.0f, 20.0f);
|
||||
items[1].desiredContentSize = UISize(60.0f, 30.0f);
|
||||
|
||||
const auto result = MeasureStackLayout(options, items);
|
||||
|
||||
EXPECT_FLOAT_EQ(result.desiredSize.width, 125.0f);
|
||||
EXPECT_FLOAT_EQ(result.desiredSize.height, 42.0f);
|
||||
}
|
||||
|
||||
TEST(UI_Layout, ArrangeHorizontalStackDistributesRemainingSpaceToStretchChildren) {
|
||||
UIStackLayoutOptions options = {};
|
||||
options.axis = UILayoutAxis::Horizontal;
|
||||
options.spacing = 5.0f;
|
||||
options.padding = UILayoutThickness::Uniform(10.0f);
|
||||
|
||||
std::vector<UILayoutItem> items(3);
|
||||
items[0].width = UILayoutLength::Pixels(100.0f);
|
||||
items[0].desiredContentSize = UISize(10.0f, 20.0f);
|
||||
|
||||
items[1].width = UILayoutLength::Stretch(1.0f);
|
||||
items[1].desiredContentSize = UISize(30.0f, 20.0f);
|
||||
|
||||
items[2].width = UILayoutLength::Pixels(50.0f);
|
||||
items[2].desiredContentSize = UISize(10.0f, 20.0f);
|
||||
|
||||
const auto result = ArrangeStackLayout(options, items, UIRect(0.0f, 0.0f, 300.0f, 80.0f));
|
||||
|
||||
ExpectRect(result.children[0].arrangedRect, 10.0f, 10.0f, 100.0f, 20.0f);
|
||||
ExpectRect(result.children[1].arrangedRect, 115.0f, 10.0f, 120.0f, 20.0f);
|
||||
ExpectRect(result.children[2].arrangedRect, 240.0f, 10.0f, 50.0f, 20.0f);
|
||||
}
|
||||
|
||||
TEST(UI_Layout, ArrangeVerticalStackSupportsCrossAxisStretch) {
|
||||
UIStackLayoutOptions options = {};
|
||||
options.axis = UILayoutAxis::Vertical;
|
||||
options.spacing = 4.0f;
|
||||
options.padding = UILayoutThickness::Symmetric(8.0f, 6.0f);
|
||||
|
||||
std::vector<UILayoutItem> items(2);
|
||||
items[0].desiredContentSize = UISize(40.0f, 10.0f);
|
||||
items[0].horizontalAlignment = UILayoutAlignment::Stretch;
|
||||
items[1].desiredContentSize = UISize(60.0f, 20.0f);
|
||||
|
||||
const auto result = ArrangeStackLayout(options, items, UIRect(0.0f, 0.0f, 200.0f, 100.0f));
|
||||
|
||||
ExpectRect(result.children[0].arrangedRect, 8.0f, 6.0f, 184.0f, 10.0f);
|
||||
ExpectRect(result.children[1].arrangedRect, 8.0f, 20.0f, 60.0f, 20.0f);
|
||||
}
|
||||
|
||||
TEST(UI_Layout, ArrangeOverlaySupportsCenterAndStretch) {
|
||||
UIOverlayLayoutOptions options = {};
|
||||
options.padding = UILayoutThickness::Uniform(10.0f);
|
||||
|
||||
std::vector<UILayoutItem> items(2);
|
||||
items[0].desiredContentSize = UISize(40.0f, 20.0f);
|
||||
items[0].horizontalAlignment = UILayoutAlignment::Center;
|
||||
items[0].verticalAlignment = UILayoutAlignment::Center;
|
||||
|
||||
items[1].desiredContentSize = UISize(10.0f, 10.0f);
|
||||
items[1].width = UILayoutLength::Stretch();
|
||||
items[1].height = UILayoutLength::Stretch();
|
||||
items[1].margin = UILayoutThickness::Uniform(5.0f);
|
||||
|
||||
const auto result = ArrangeOverlayLayout(options, items, UIRect(0.0f, 0.0f, 100.0f, 60.0f));
|
||||
|
||||
ExpectRect(result.children[0].arrangedRect, 30.0f, 20.0f, 40.0f, 20.0f);
|
||||
ExpectRect(result.children[1].arrangedRect, 15.0f, 15.0f, 70.0f, 30.0f);
|
||||
}
|
||||
|
||||
TEST(UI_Layout, MeasureOverlayRespectsItemMinMaxAndAvailableConstraints) {
|
||||
UIOverlayLayoutOptions options = {};
|
||||
|
||||
std::vector<UILayoutItem> items(1);
|
||||
items[0].width = UILayoutLength::Pixels(500.0f);
|
||||
items[0].desiredContentSize = UISize(10.0f, 10.0f);
|
||||
items[0].minSize = UISize(0.0f, 50.0f);
|
||||
items[0].maxSize = UISize(200.0f, 120.0f);
|
||||
|
||||
const auto result = MeasureOverlayLayout(
|
||||
options,
|
||||
items,
|
||||
UILayoutConstraints::Bounded(150.0f, 100.0f));
|
||||
|
||||
EXPECT_FLOAT_EQ(result.children[0].measuredSize.width, 150.0f);
|
||||
EXPECT_FLOAT_EQ(result.children[0].measuredSize.height, 50.0f);
|
||||
EXPECT_FLOAT_EQ(result.desiredSize.width, 150.0f);
|
||||
EXPECT_FLOAT_EQ(result.desiredSize.height, 50.0f);
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/IResource.h>
|
||||
#include <XCEngine/Resources/UI/UIDocuments.h>
|
||||
#include <XCEngine/UI/Core/UIContext.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::HasAnyDirtyFlags;
|
||||
using XCEngine::UI::IUIViewModel;
|
||||
using XCEngine::UI::RevisionedViewModelBase;
|
||||
using XCEngine::UI::UIBuildContext;
|
||||
using XCEngine::UI::UIBuildElementDesc;
|
||||
using XCEngine::UI::UIContext;
|
||||
using XCEngine::UI::UIDirtyFlags;
|
||||
using XCEngine::UI::UIElementChangeKind;
|
||||
using XCEngine::UI::UIElementId;
|
||||
using XCEngine::UI::UIElementNode;
|
||||
using XCEngine::UI::UIElementTree;
|
||||
using XCEngine::Resources::UIDocumentKind;
|
||||
using XCEngine::Resources::UIDocumentModel;
|
||||
using XCEngine::Resources::UISchema;
|
||||
using XCEngine::Resources::UIView;
|
||||
|
||||
class TestViewModel : public RevisionedViewModelBase {
|
||||
public:
|
||||
void Touch() {
|
||||
MarkViewModelChanged();
|
||||
}
|
||||
};
|
||||
|
||||
UIBuildElementDesc MakeElement(
|
||||
UIElementId id,
|
||||
const char* typeName,
|
||||
std::uint64_t localStateRevision = 0,
|
||||
const IUIViewModel* viewModel = nullptr,
|
||||
std::uint64_t structuralRevision = 0) {
|
||||
UIBuildElementDesc desc = {};
|
||||
desc.id = id;
|
||||
desc.typeName = typeName;
|
||||
desc.localStateRevision = localStateRevision;
|
||||
desc.viewModel = viewModel;
|
||||
desc.structuralRevision = structuralRevision;
|
||||
return desc;
|
||||
}
|
||||
|
||||
void BuildBasicTree(UIBuildContext& buildContext) {
|
||||
auto root = buildContext.PushElement(MakeElement(1, "Root"));
|
||||
EXPECT_TRUE(static_cast<bool>(root));
|
||||
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(2, "Label")));
|
||||
auto panel = buildContext.PushElement(MakeElement(3, "Panel"));
|
||||
EXPECT_TRUE(static_cast<bool>(panel));
|
||||
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(4, "Button")));
|
||||
}
|
||||
|
||||
const UIElementNode& RequireNode(const UIElementTree& tree, UIElementId id) {
|
||||
const UIElementNode* node = tree.FindNode(id);
|
||||
EXPECT_NE(node, nullptr);
|
||||
return *node;
|
||||
}
|
||||
|
||||
TEST(UICoreTest, RebuildCreatesStableParentChildTree) {
|
||||
UIContext context = {};
|
||||
const auto result = context.Rebuild(BuildBasicTree);
|
||||
|
||||
EXPECT_TRUE(result.succeeded);
|
||||
EXPECT_TRUE(result.treeChanged);
|
||||
EXPECT_EQ(result.generation, 1u);
|
||||
EXPECT_EQ(context.GetElementTree().GetRootId(), 1u);
|
||||
EXPECT_EQ(context.GetElementTree().GetNodeCount(), 4u);
|
||||
EXPECT_TRUE(result.HasChange(1));
|
||||
EXPECT_TRUE(result.HasChange(4));
|
||||
|
||||
const UIElementNode& root = RequireNode(context.GetElementTree(), 1);
|
||||
ASSERT_EQ(root.childIds.size(), 2u);
|
||||
EXPECT_EQ(root.childIds[0], 2u);
|
||||
EXPECT_EQ(root.childIds[1], 3u);
|
||||
EXPECT_EQ(root.depth, 0u);
|
||||
|
||||
const UIElementNode& panel = RequireNode(context.GetElementTree(), 3);
|
||||
ASSERT_EQ(panel.childIds.size(), 1u);
|
||||
EXPECT_EQ(panel.childIds[0], 4u);
|
||||
EXPECT_EQ(panel.depth, 1u);
|
||||
|
||||
ASSERT_EQ(result.dirtyRootIds.size(), 1u);
|
||||
EXPECT_EQ(result.dirtyRootIds[0], 1u);
|
||||
}
|
||||
|
||||
TEST(UICoreTest, RebuildSkipsUnchangedTreeAfterDirtyFlagsAreCleared) {
|
||||
UIContext context = {};
|
||||
const auto initial = context.Rebuild(BuildBasicTree);
|
||||
ASSERT_TRUE(initial.succeeded);
|
||||
context.GetElementTree().ClearAllDirtyFlags();
|
||||
|
||||
const auto result = context.Rebuild(BuildBasicTree);
|
||||
|
||||
EXPECT_TRUE(result.succeeded);
|
||||
EXPECT_FALSE(result.treeChanged);
|
||||
EXPECT_TRUE(result.changes.empty());
|
||||
EXPECT_TRUE(result.dirtyRootIds.empty());
|
||||
}
|
||||
|
||||
TEST(UICoreTest, LocalStateChangeOnlyInvalidatesTheChangedLeaf) {
|
||||
UIContext context = {};
|
||||
ASSERT_TRUE(context.Rebuild(BuildBasicTree).succeeded);
|
||||
context.GetElementTree().ClearAllDirtyFlags();
|
||||
|
||||
const auto result = context.Rebuild([](UIBuildContext& buildContext) {
|
||||
auto root = buildContext.PushElement(MakeElement(1, "Root"));
|
||||
EXPECT_TRUE(static_cast<bool>(root));
|
||||
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(2, "Label", 1)));
|
||||
auto panel = buildContext.PushElement(MakeElement(3, "Panel"));
|
||||
EXPECT_TRUE(static_cast<bool>(panel));
|
||||
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(4, "Button")));
|
||||
});
|
||||
|
||||
EXPECT_TRUE(result.succeeded);
|
||||
EXPECT_TRUE(result.treeChanged);
|
||||
ASSERT_EQ(result.changes.size(), 1u);
|
||||
ASSERT_NE(result.FindChange(2), nullptr);
|
||||
EXPECT_EQ(result.FindChange(2)->kind, UIElementChangeKind::Updated);
|
||||
EXPECT_TRUE(HasAnyDirtyFlags(result.FindChange(2)->dirtyFlags, UIDirtyFlags::LocalState));
|
||||
EXPECT_TRUE(HasAnyDirtyFlags(result.FindChange(2)->dirtyFlags, UIDirtyFlags::Paint));
|
||||
|
||||
ASSERT_EQ(result.dirtyRootIds.size(), 1u);
|
||||
EXPECT_EQ(result.dirtyRootIds[0], 2u);
|
||||
|
||||
const UIElementNode& leaf = RequireNode(context.GetElementTree(), 2);
|
||||
EXPECT_TRUE(HasAnyDirtyFlags(leaf.dirtyFlags, UIDirtyFlags::LocalState));
|
||||
EXPECT_FALSE(RequireNode(context.GetElementTree(), 1).IsDirty());
|
||||
}
|
||||
|
||||
TEST(UICoreTest, ViewModelRevisionChangeInvalidatesBoundElement) {
|
||||
TestViewModel viewModel = {};
|
||||
UIContext context = {};
|
||||
ASSERT_TRUE(context.Rebuild([&](UIBuildContext& buildContext) {
|
||||
auto root = buildContext.PushElement(MakeElement(1, "Root"));
|
||||
EXPECT_TRUE(static_cast<bool>(root));
|
||||
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(2, "Inspector", 0, &viewModel)));
|
||||
}).succeeded);
|
||||
context.GetElementTree().ClearAllDirtyFlags();
|
||||
|
||||
viewModel.Touch();
|
||||
const auto result = context.Rebuild([&](UIBuildContext& buildContext) {
|
||||
auto root = buildContext.PushElement(MakeElement(1, "Root"));
|
||||
EXPECT_TRUE(static_cast<bool>(root));
|
||||
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(2, "Inspector", 0, &viewModel)));
|
||||
});
|
||||
|
||||
EXPECT_TRUE(result.succeeded);
|
||||
ASSERT_NE(result.FindChange(2), nullptr);
|
||||
EXPECT_TRUE(HasAnyDirtyFlags(result.FindChange(2)->dirtyFlags, UIDirtyFlags::ViewModel));
|
||||
EXPECT_TRUE(HasAnyDirtyFlags(result.FindChange(2)->dirtyFlags, UIDirtyFlags::Paint));
|
||||
ASSERT_EQ(result.dirtyRootIds.size(), 1u);
|
||||
EXPECT_EQ(result.dirtyRootIds[0], 2u);
|
||||
}
|
||||
|
||||
TEST(UICoreTest, StructuralChangesBubbleLayoutInvalidationToAncestors) {
|
||||
UIContext context = {};
|
||||
ASSERT_TRUE(context.Rebuild([](UIBuildContext& buildContext) {
|
||||
auto root = buildContext.PushElement(MakeElement(1, "Root"));
|
||||
EXPECT_TRUE(static_cast<bool>(root));
|
||||
auto panel = buildContext.PushElement(MakeElement(2, "Panel"));
|
||||
EXPECT_TRUE(static_cast<bool>(panel));
|
||||
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(3, "Text")));
|
||||
}).succeeded);
|
||||
context.GetElementTree().ClearAllDirtyFlags();
|
||||
|
||||
const auto result = context.Rebuild([](UIBuildContext& buildContext) {
|
||||
auto root = buildContext.PushElement(MakeElement(1, "Root"));
|
||||
EXPECT_TRUE(static_cast<bool>(root));
|
||||
auto panel = buildContext.PushElement(MakeElement(2, "Panel"));
|
||||
EXPECT_TRUE(static_cast<bool>(panel));
|
||||
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(3, "Text")));
|
||||
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(4, "Icon")));
|
||||
});
|
||||
|
||||
EXPECT_TRUE(result.succeeded);
|
||||
EXPECT_TRUE(result.HasChange(4));
|
||||
ASSERT_NE(result.FindChange(2), nullptr);
|
||||
EXPECT_TRUE(HasAnyDirtyFlags(result.FindChange(2)->dirtyFlags, UIDirtyFlags::Structure));
|
||||
|
||||
const UIElementNode& root = RequireNode(context.GetElementTree(), 1);
|
||||
const UIElementNode& panel = RequireNode(context.GetElementTree(), 2);
|
||||
EXPECT_TRUE(HasAnyDirtyFlags(root.dirtyFlags, UIDirtyFlags::Layout));
|
||||
EXPECT_TRUE(HasAnyDirtyFlags(panel.dirtyFlags, UIDirtyFlags::Structure));
|
||||
|
||||
ASSERT_EQ(result.dirtyRootIds.size(), 1u);
|
||||
EXPECT_EQ(result.dirtyRootIds[0], 1u);
|
||||
}
|
||||
|
||||
TEST(UICoreTest, RebuildFailsWhenElementScopesRemainOpen) {
|
||||
UIContext context = {};
|
||||
const auto result = context.Rebuild([](UIBuildContext& buildContext) {
|
||||
EXPECT_TRUE(buildContext.BeginElement(MakeElement(1, "Root")));
|
||||
});
|
||||
|
||||
EXPECT_FALSE(result.succeeded);
|
||||
EXPECT_FALSE(result.errorMessage.empty());
|
||||
EXPECT_EQ(context.GetElementTree().GetNodeCount(), 0u);
|
||||
}
|
||||
|
||||
TEST(UICoreTest, UIDocumentResourcesAcceptMovedDocumentModels) {
|
||||
UIDocumentModel viewDocument = {};
|
||||
viewDocument.kind = UIDocumentKind::View;
|
||||
viewDocument.sourcePath = "Assets/UI/Test.xcui";
|
||||
viewDocument.displayName = "TestView";
|
||||
viewDocument.rootNode.tagName = "View";
|
||||
viewDocument.valid = true;
|
||||
|
||||
UIView view = {};
|
||||
XCEngine::Resources::IResource::ConstructParams params = {};
|
||||
params.name = "TestView";
|
||||
params.path = viewDocument.sourcePath;
|
||||
params.guid = XCEngine::Resources::ResourceGUID::Generate(params.path);
|
||||
view.Initialize(params);
|
||||
view.SetDocumentModel(std::move(viewDocument));
|
||||
EXPECT_EQ(view.GetRootNode().tagName, "View");
|
||||
EXPECT_EQ(view.GetSourcePath(), "Assets/UI/Test.xcui");
|
||||
|
||||
UIDocumentModel schemaDocument = {};
|
||||
schemaDocument.kind = UIDocumentKind::Schema;
|
||||
schemaDocument.sourcePath = "Assets/UI/Test.xcschema";
|
||||
schemaDocument.displayName = "TestSchema";
|
||||
schemaDocument.rootNode.tagName = "Schema";
|
||||
schemaDocument.schemaDefinition.name = "TestSchema";
|
||||
schemaDocument.schemaDefinition.valid = true;
|
||||
schemaDocument.valid = true;
|
||||
|
||||
UISchema schema = {};
|
||||
params.name = "TestSchema";
|
||||
params.path = schemaDocument.sourcePath;
|
||||
params.guid = XCEngine::Resources::ResourceGUID::Generate(params.path);
|
||||
schema.Initialize(params);
|
||||
schema.SetDocumentModel(std::move(schemaDocument));
|
||||
EXPECT_TRUE(schema.GetSchemaDefinition().valid);
|
||||
EXPECT_EQ(schema.GetSchemaDefinition().name, "TestSchema");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,84 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Style/Theme.h>
|
||||
#include <XCEngine/UI/Style/StyleTypes.h>
|
||||
#include <XCEngine/UI/Widgets/UIEditorCollectionPrimitives.h>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace Style = XCEngine::UI::Style;
|
||||
namespace UIWidgets = XCEngine::UI::Widgets;
|
||||
|
||||
Style::UITheme BuildEditorPrimitiveTheme() {
|
||||
Style::UIThemeDefinition definition = {};
|
||||
definition.SetToken("space.cardInset", Style::UIStyleValue(14.0f));
|
||||
definition.SetToken("size.treeItemHeight", Style::UIStyleValue(30.0f));
|
||||
definition.SetToken("size.listItemHeight", Style::UIStyleValue(64.0f));
|
||||
definition.SetToken("size.fieldRowHeight", Style::UIStyleValue(36.0f));
|
||||
definition.SetToken("size.propertySectionHeight", Style::UIStyleValue(156.0f));
|
||||
definition.SetToken("size.treeIndent", Style::UIStyleValue(20.0f));
|
||||
return Style::BuildTheme(definition);
|
||||
}
|
||||
|
||||
TEST(UIEditorCollectionPrimitivesTest, ClassifyAndFlagsMatchEditorCollectionTags) {
|
||||
using Kind = UIWidgets::UIEditorCollectionPrimitiveKind;
|
||||
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("ScrollView"), Kind::ScrollView);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("TreeView"), Kind::TreeView);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("TreeItem"), Kind::TreeItem);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("ListView"), Kind::ListView);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("ListItem"), Kind::ListItem);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("PropertySection"), Kind::PropertySection);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("FieldRow"), Kind::FieldRow);
|
||||
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("Column"), Kind::None);
|
||||
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::ScrollView));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::TreeView));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::ListView));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::PropertySection));
|
||||
EXPECT_FALSE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::TreeItem));
|
||||
|
||||
EXPECT_TRUE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::TreeView));
|
||||
EXPECT_TRUE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::ListView));
|
||||
EXPECT_TRUE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::PropertySection));
|
||||
EXPECT_FALSE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::ScrollView));
|
||||
|
||||
EXPECT_TRUE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::ScrollView));
|
||||
EXPECT_TRUE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::TreeView));
|
||||
EXPECT_TRUE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::ListView));
|
||||
EXPECT_FALSE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::PropertySection));
|
||||
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::TreeItem));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::ListItem));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::PropertySection));
|
||||
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::FieldRow));
|
||||
EXPECT_FALSE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::TreeView));
|
||||
}
|
||||
|
||||
TEST(UIEditorCollectionPrimitivesTest, ResolveMetricsUseThemeTokensAndFallbacks) {
|
||||
using Kind = UIWidgets::UIEditorCollectionPrimitiveKind;
|
||||
|
||||
const Style::UITheme themed = BuildEditorPrimitiveTheme();
|
||||
const Style::UITheme fallback = Style::UITheme();
|
||||
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::TreeView, themed), 14.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::ListView, themed), 14.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::PropertySection, themed), 14.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::ScrollView, themed), 0.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::TreeItem, themed), 30.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::ListItem, themed), 64.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::FieldRow, themed), 36.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::PropertySection, themed), 156.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::TreeItem, fallback), 28.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::ListItem, fallback), 60.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::FieldRow, fallback), 32.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::PropertySection, fallback), 148.0f);
|
||||
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveIndent(Kind::TreeItem, themed, 2.0f), 40.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveIndent(Kind::TreeItem, fallback, 2.0f), 36.0f);
|
||||
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveIndent(Kind::ListItem, themed, 2.0f), 0.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,126 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/DrawData.h>
|
||||
#include <XCEngine/UI/Widgets/UIEditorPanelChrome.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIColor;
|
||||
using XCEngine::UI::UIDrawCommandType;
|
||||
using XCEngine::UI::UIDrawList;
|
||||
using XCEngine::UI::UIRect;
|
||||
using XCEngine::UI::Widgets::AppendUIEditorPanelChromeBackground;
|
||||
using XCEngine::UI::Widgets::AppendUIEditorPanelChromeForeground;
|
||||
using XCEngine::UI::Widgets::BuildUIEditorPanelChromeHeaderRect;
|
||||
using XCEngine::UI::Widgets::ResolveUIEditorPanelChromeBorderColor;
|
||||
using XCEngine::UI::Widgets::ResolveUIEditorPanelChromeBorderThickness;
|
||||
using XCEngine::UI::Widgets::UIEditorPanelChromePalette;
|
||||
using XCEngine::UI::Widgets::UIEditorPanelChromeState;
|
||||
using XCEngine::UI::Widgets::UIEditorPanelChromeText;
|
||||
|
||||
void ExpectColorEq(
|
||||
const UIColor& actual,
|
||||
const UIColor& expected) {
|
||||
EXPECT_FLOAT_EQ(actual.r, expected.r);
|
||||
EXPECT_FLOAT_EQ(actual.g, expected.g);
|
||||
EXPECT_FLOAT_EQ(actual.b, expected.b);
|
||||
EXPECT_FLOAT_EQ(actual.a, expected.a);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelChromeTest, HeaderRectAndBorderPolicyMatchNativeShellCardChrome) {
|
||||
const UIRect panelRect(100.0f, 200.0f, 320.0f, 180.0f);
|
||||
const UIEditorPanelChromePalette palette = {};
|
||||
|
||||
const auto headerRect = BuildUIEditorPanelChromeHeaderRect(panelRect);
|
||||
EXPECT_FLOAT_EQ(headerRect.x, 100.0f);
|
||||
EXPECT_FLOAT_EQ(headerRect.y, 200.0f);
|
||||
EXPECT_FLOAT_EQ(headerRect.width, 320.0f);
|
||||
EXPECT_FLOAT_EQ(headerRect.height, 42.0f);
|
||||
|
||||
ExpectColorEq(
|
||||
ResolveUIEditorPanelChromeBorderColor(UIEditorPanelChromeState(), palette),
|
||||
palette.borderColor);
|
||||
ExpectColorEq(
|
||||
ResolveUIEditorPanelChromeBorderColor(UIEditorPanelChromeState{ false, true }, palette),
|
||||
palette.hoveredAccentColor);
|
||||
ExpectColorEq(
|
||||
ResolveUIEditorPanelChromeBorderColor(UIEditorPanelChromeState{ true, true }, palette),
|
||||
palette.accentColor);
|
||||
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorPanelChromeBorderThickness(UIEditorPanelChromeState()), 1.0f);
|
||||
EXPECT_FLOAT_EQ(ResolveUIEditorPanelChromeBorderThickness(UIEditorPanelChromeState{ true, false }), 2.0f);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelChromeTest, BackgroundAppendEmitsSurfaceOutlineAndHeaderFill) {
|
||||
UIDrawList drawList("PanelChrome");
|
||||
const UIRect panelRect(40.0f, 60.0f, 400.0f, 280.0f);
|
||||
const UIEditorPanelChromeState state{ true, false };
|
||||
const UIEditorPanelChromePalette palette = {};
|
||||
|
||||
AppendUIEditorPanelChromeBackground(drawList, panelRect, state, palette);
|
||||
|
||||
ASSERT_EQ(drawList.GetCommandCount(), 3u);
|
||||
const auto& commands = drawList.GetCommands();
|
||||
EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRect);
|
||||
EXPECT_EQ(commands[1].type, UIDrawCommandType::RectOutline);
|
||||
EXPECT_EQ(commands[2].type, UIDrawCommandType::FilledRect);
|
||||
|
||||
EXPECT_FLOAT_EQ(commands[0].rect.x, 40.0f);
|
||||
EXPECT_FLOAT_EQ(commands[0].rounding, 18.0f);
|
||||
ExpectColorEq(commands[0].color, palette.surfaceColor);
|
||||
|
||||
EXPECT_FLOAT_EQ(commands[1].thickness, 2.0f);
|
||||
EXPECT_FLOAT_EQ(commands[1].rounding, 18.0f);
|
||||
ExpectColorEq(commands[1].color, palette.accentColor);
|
||||
|
||||
EXPECT_FLOAT_EQ(commands[2].rect.height, 42.0f);
|
||||
EXPECT_FLOAT_EQ(commands[2].rounding, 18.0f);
|
||||
ExpectColorEq(commands[2].color, palette.headerColor);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelChromeTest, ForegroundAppendPlacesTitleSubtitleAndFooterAtCurrentOffsets) {
|
||||
UIDrawList drawList("PanelChromeText");
|
||||
const UIRect panelRect(100.0f, 200.0f, 320.0f, 180.0f);
|
||||
const UIEditorPanelChromePalette palette = {};
|
||||
const UIEditorPanelChromeText text{
|
||||
"XCUI Demo",
|
||||
"native queued offscreen surface",
|
||||
"Active | 42 elements | 9 cmds"
|
||||
};
|
||||
|
||||
AppendUIEditorPanelChromeForeground(drawList, panelRect, text, palette);
|
||||
|
||||
ASSERT_EQ(drawList.GetCommandCount(), 3u);
|
||||
const auto& commands = drawList.GetCommands();
|
||||
EXPECT_EQ(commands[0].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(commands[1].type, UIDrawCommandType::Text);
|
||||
EXPECT_EQ(commands[2].type, UIDrawCommandType::Text);
|
||||
|
||||
EXPECT_EQ(commands[0].text, "XCUI Demo");
|
||||
EXPECT_FLOAT_EQ(commands[0].position.x, 116.0f);
|
||||
EXPECT_FLOAT_EQ(commands[0].position.y, 212.0f);
|
||||
ExpectColorEq(commands[0].color, palette.textPrimary);
|
||||
|
||||
EXPECT_EQ(commands[1].text, "native queued offscreen surface");
|
||||
EXPECT_FLOAT_EQ(commands[1].position.x, 116.0f);
|
||||
EXPECT_FLOAT_EQ(commands[1].position.y, 228.0f);
|
||||
ExpectColorEq(commands[1].color, palette.textSecondary);
|
||||
|
||||
EXPECT_EQ(commands[2].text, "Active | 42 elements | 9 cmds");
|
||||
EXPECT_FLOAT_EQ(commands[2].position.x, 116.0f);
|
||||
EXPECT_FLOAT_EQ(commands[2].position.y, 362.0f);
|
||||
ExpectColorEq(commands[2].color, palette.textMuted);
|
||||
}
|
||||
|
||||
TEST(UIEditorPanelChromeTest, ForegroundAppendSkipsEmptyStrings) {
|
||||
UIDrawList drawList("PanelChromeEmptyText");
|
||||
|
||||
AppendUIEditorPanelChromeForeground(
|
||||
drawList,
|
||||
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
|
||||
UIEditorPanelChromeText{});
|
||||
|
||||
EXPECT_EQ(drawList.GetCommandCount(), 0u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,45 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Widgets::UIExpansionModel;
|
||||
|
||||
TEST(UIExpansionModelTest, ExpandCollapseAndClearTrackExpandedItems) {
|
||||
UIExpansionModel expansion = {};
|
||||
|
||||
EXPECT_FALSE(expansion.HasExpandedItems());
|
||||
EXPECT_EQ(expansion.GetExpandedCount(), 0u);
|
||||
|
||||
EXPECT_TRUE(expansion.Expand("treeAssetsRoot"));
|
||||
EXPECT_TRUE(expansion.IsExpanded("treeAssetsRoot"));
|
||||
EXPECT_TRUE(expansion.HasExpandedItems());
|
||||
EXPECT_EQ(expansion.GetExpandedCount(), 1u);
|
||||
|
||||
EXPECT_FALSE(expansion.Expand("treeAssetsRoot"));
|
||||
EXPECT_TRUE(expansion.Collapse("treeAssetsRoot"));
|
||||
EXPECT_FALSE(expansion.IsExpanded("treeAssetsRoot"));
|
||||
EXPECT_EQ(expansion.GetExpandedCount(), 0u);
|
||||
EXPECT_FALSE(expansion.Collapse("treeAssetsRoot"));
|
||||
EXPECT_FALSE(expansion.Clear());
|
||||
}
|
||||
|
||||
TEST(UIExpansionModelTest, SetAndToggleExpandedReplaceStateForMatchingItem) {
|
||||
UIExpansionModel expansion = {};
|
||||
|
||||
EXPECT_TRUE(expansion.SetExpanded("inspectorTransform", true));
|
||||
EXPECT_TRUE(expansion.IsExpanded("inspectorTransform"));
|
||||
EXPECT_EQ(expansion.GetExpandedCount(), 1u);
|
||||
|
||||
EXPECT_FALSE(expansion.SetExpanded("inspectorTransform", true));
|
||||
EXPECT_TRUE(expansion.ToggleExpanded("inspectorTransform"));
|
||||
EXPECT_FALSE(expansion.IsExpanded("inspectorTransform"));
|
||||
|
||||
EXPECT_TRUE(expansion.ToggleExpanded("inspectorMesh"));
|
||||
EXPECT_TRUE(expansion.IsExpanded("inspectorMesh"));
|
||||
EXPECT_TRUE(expansion.SetExpanded("inspectorMesh", false));
|
||||
EXPECT_FALSE(expansion.IsExpanded("inspectorMesh"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,145 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UIFlatHierarchyHelpers.h>
|
||||
|
||||
#include <array>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Widgets::kInvalidUIFlatHierarchyItemOffset;
|
||||
using XCEngine::UI::Widgets::UIFlatHierarchyFindFirstVisibleChildOffset;
|
||||
using XCEngine::UI::Widgets::UIFlatHierarchyFindParentOffset;
|
||||
using XCEngine::UI::Widgets::UIFlatHierarchyHasChildren;
|
||||
using XCEngine::UI::Widgets::UIFlatHierarchyIsVisible;
|
||||
|
||||
struct FlatHierarchyItem {
|
||||
float depth = 0.0f;
|
||||
};
|
||||
|
||||
TEST(UIFlatHierarchyHelpersTest, HasChildrenAndParentResolutionTrackIndentedBranches) {
|
||||
const std::array<FlatHierarchyItem, 5> items = {{
|
||||
{ 0.0f },
|
||||
{ 1.0f },
|
||||
{ 2.0f },
|
||||
{ 1.0f },
|
||||
{ 0.0f },
|
||||
}};
|
||||
const std::vector<std::size_t> itemIndices = { 0u, 1u, 2u, 3u, 4u };
|
||||
|
||||
const auto resolveDepth = [&items](std::size_t itemIndex) {
|
||||
return items[itemIndex].depth;
|
||||
};
|
||||
|
||||
EXPECT_TRUE(UIFlatHierarchyHasChildren(itemIndices, 0u, resolveDepth));
|
||||
EXPECT_TRUE(UIFlatHierarchyHasChildren(itemIndices, 1u, resolveDepth));
|
||||
EXPECT_FALSE(UIFlatHierarchyHasChildren(itemIndices, 2u, resolveDepth));
|
||||
EXPECT_FALSE(UIFlatHierarchyHasChildren(itemIndices, 3u, resolveDepth));
|
||||
EXPECT_FALSE(UIFlatHierarchyHasChildren(itemIndices, 4u, resolveDepth));
|
||||
|
||||
EXPECT_EQ(
|
||||
UIFlatHierarchyFindParentOffset(itemIndices, 0u, resolveDepth),
|
||||
kInvalidUIFlatHierarchyItemOffset);
|
||||
EXPECT_EQ(UIFlatHierarchyFindParentOffset(itemIndices, 1u, resolveDepth), 0u);
|
||||
EXPECT_EQ(UIFlatHierarchyFindParentOffset(itemIndices, 2u, resolveDepth), 1u);
|
||||
EXPECT_EQ(UIFlatHierarchyFindParentOffset(itemIndices, 3u, resolveDepth), 0u);
|
||||
EXPECT_EQ(
|
||||
UIFlatHierarchyFindParentOffset(itemIndices, 99u, resolveDepth),
|
||||
kInvalidUIFlatHierarchyItemOffset);
|
||||
}
|
||||
|
||||
TEST(UIFlatHierarchyHelpersTest, VisibilityFollowsCollapsedAncestorStateAcrossDepthTransitions) {
|
||||
const std::array<FlatHierarchyItem, 4> items = {{
|
||||
{ 0.0f },
|
||||
{ 1.0f },
|
||||
{ 2.0f },
|
||||
{ 1.0f },
|
||||
}};
|
||||
const std::vector<std::size_t> itemIndices = { 0u, 1u, 2u, 3u };
|
||||
|
||||
const auto resolveDepth = [&items](std::size_t itemIndex) {
|
||||
return items[itemIndex].depth;
|
||||
};
|
||||
|
||||
std::unordered_set<std::size_t> expandedItems = { 0u };
|
||||
const auto isExpanded = [&expandedItems](std::size_t itemIndex) {
|
||||
return expandedItems.contains(itemIndex);
|
||||
};
|
||||
|
||||
EXPECT_TRUE(UIFlatHierarchyIsVisible(itemIndices, 0u, resolveDepth, isExpanded));
|
||||
EXPECT_TRUE(UIFlatHierarchyIsVisible(itemIndices, 1u, resolveDepth, isExpanded));
|
||||
EXPECT_FALSE(UIFlatHierarchyIsVisible(itemIndices, 2u, resolveDepth, isExpanded));
|
||||
EXPECT_TRUE(UIFlatHierarchyIsVisible(itemIndices, 3u, resolveDepth, isExpanded));
|
||||
|
||||
expandedItems.insert(1u);
|
||||
EXPECT_TRUE(UIFlatHierarchyIsVisible(itemIndices, 2u, resolveDepth, isExpanded));
|
||||
|
||||
expandedItems.clear();
|
||||
EXPECT_FALSE(UIFlatHierarchyIsVisible(itemIndices, 1u, resolveDepth, isExpanded));
|
||||
EXPECT_FALSE(UIFlatHierarchyIsVisible(itemIndices, 2u, resolveDepth, isExpanded));
|
||||
}
|
||||
|
||||
TEST(UIFlatHierarchyHelpersTest, FirstVisibleChildSkipsCollapsedDescendantsUntilExpanded) {
|
||||
const std::array<FlatHierarchyItem, 4> items = {{
|
||||
{ 0.0f },
|
||||
{ 1.0f },
|
||||
{ 2.0f },
|
||||
{ 1.0f },
|
||||
}};
|
||||
const std::vector<std::size_t> itemIndices = { 0u, 1u, 2u, 3u };
|
||||
|
||||
const auto resolveDepth = [&items](std::size_t itemIndex) {
|
||||
return items[itemIndex].depth;
|
||||
};
|
||||
|
||||
std::unordered_set<std::size_t> expandedItems = { 0u };
|
||||
const auto isExpanded = [&expandedItems](std::size_t itemIndex) {
|
||||
return expandedItems.contains(itemIndex);
|
||||
};
|
||||
const auto isVisible = [&itemIndices, &resolveDepth, &isExpanded](std::size_t itemIndex) {
|
||||
for (std::size_t itemOffset = 0; itemOffset < itemIndices.size(); ++itemOffset) {
|
||||
if (itemIndices[itemOffset] == itemIndex) {
|
||||
return UIFlatHierarchyIsVisible(itemIndices, itemOffset, resolveDepth, isExpanded);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
EXPECT_EQ(
|
||||
UIFlatHierarchyFindFirstVisibleChildOffset(itemIndices, 0u, resolveDepth, isVisible),
|
||||
1u);
|
||||
EXPECT_EQ(
|
||||
UIFlatHierarchyFindFirstVisibleChildOffset(itemIndices, 1u, resolveDepth, isVisible),
|
||||
kInvalidUIFlatHierarchyItemOffset);
|
||||
|
||||
expandedItems.insert(1u);
|
||||
EXPECT_EQ(
|
||||
UIFlatHierarchyFindFirstVisibleChildOffset(itemIndices, 1u, resolveDepth, isVisible),
|
||||
2u);
|
||||
}
|
||||
|
||||
TEST(UIFlatHierarchyHelpersTest, NegativeDepthsClampToRootsForHierarchyQueries) {
|
||||
const std::array<FlatHierarchyItem, 3> items = {{
|
||||
{ -3.0f },
|
||||
{ 1.0f },
|
||||
{ -1.0f },
|
||||
}};
|
||||
const std::vector<std::size_t> itemIndices = { 0u, 1u, 2u };
|
||||
|
||||
const auto resolveDepth = [&items](std::size_t itemIndex) {
|
||||
return items[itemIndex].depth;
|
||||
};
|
||||
const auto isExpanded = [](std::size_t itemIndex) {
|
||||
return itemIndex == 0u;
|
||||
};
|
||||
|
||||
EXPECT_TRUE(UIFlatHierarchyHasChildren(itemIndices, 0u, resolveDepth));
|
||||
EXPECT_EQ(UIFlatHierarchyFindParentOffset(itemIndices, 1u, resolveDepth), 0u);
|
||||
EXPECT_EQ(
|
||||
UIFlatHierarchyFindParentOffset(itemIndices, 2u, resolveDepth),
|
||||
kInvalidUIFlatHierarchyItemOffset);
|
||||
EXPECT_TRUE(UIFlatHierarchyIsVisible(itemIndices, 2u, resolveDepth, isExpanded));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,110 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Input/UIInputDispatcher.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::UIInputDispatchRequest;
|
||||
using XCEngine::UI::UIInputDispatcher;
|
||||
using XCEngine::UI::UIInputEvent;
|
||||
using XCEngine::UI::UIInputEventType;
|
||||
using XCEngine::UI::UIInputPath;
|
||||
using XCEngine::UI::UIPointerButton;
|
||||
|
||||
UIInputEvent MakePointerEvent(
|
||||
UIInputEventType type,
|
||||
UIPointerButton button = UIPointerButton::None) {
|
||||
UIInputEvent event = {};
|
||||
event.type = type;
|
||||
event.pointerButton = button;
|
||||
return event;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(UIInputDispatcherTest, PointerDownTransfersFocusAndStartsActivePath) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath hoveredPath = { 10u, 20u, 30u };
|
||||
std::vector<UIInputDispatchRequest> routedRequests = {};
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakePointerEvent(UIInputEventType::PointerButtonDown, UIPointerButton::Left),
|
||||
hoveredPath,
|
||||
[&](const UIInputDispatchRequest& request) {
|
||||
routedRequests.push_back(request);
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_TRUE(summary.focusChange.Changed());
|
||||
EXPECT_EQ(summary.focusChange.previousPath, UIInputPath());
|
||||
EXPECT_EQ(summary.focusChange.currentPath, hoveredPath);
|
||||
EXPECT_EQ(dispatcher.GetFocusController().GetFocusedPath(), hoveredPath);
|
||||
EXPECT_EQ(dispatcher.GetFocusController().GetActivePath(), hoveredPath);
|
||||
ASSERT_FALSE(routedRequests.empty());
|
||||
EXPECT_EQ(summary.routing.plan.targetPath, hoveredPath);
|
||||
EXPECT_EQ(summary.routing.plan.targetKind, XCEngine::UI::UIInputTargetKind::Hovered);
|
||||
const auto targetIt = std::find_if(
|
||||
routedRequests.begin(),
|
||||
routedRequests.end(),
|
||||
[](const UIInputDispatchRequest& request) {
|
||||
return request.isTargetElement;
|
||||
});
|
||||
ASSERT_NE(targetIt, routedRequests.end());
|
||||
EXPECT_EQ(targetIt->elementId, hoveredPath.Target());
|
||||
}
|
||||
|
||||
TEST(UIInputDispatcherTest, PointerCaptureOverridesHoveredRouteForPointerEvents) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath hoveredPath = { 41u, 42u };
|
||||
const UIInputPath capturePath = { 7u, 8u, 9u };
|
||||
dispatcher.GetFocusController().SetPointerCapturePath(capturePath);
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakePointerEvent(UIInputEventType::PointerMove),
|
||||
hoveredPath,
|
||||
[](const UIInputDispatchRequest&) {
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_EQ(summary.routing.plan.targetKind, XCEngine::UI::UIInputTargetKind::Captured);
|
||||
EXPECT_EQ(summary.routing.plan.targetPath, capturePath);
|
||||
}
|
||||
|
||||
TEST(UIInputDispatcherTest, PointerButtonUpClearsActivePathAfterDispatch) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath activePath = { 2u, 4u, 6u };
|
||||
dispatcher.GetFocusController().SetActivePath(activePath);
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
MakePointerEvent(UIInputEventType::PointerButtonUp, UIPointerButton::Left),
|
||||
{},
|
||||
[](const UIInputDispatchRequest&) {
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_FALSE(summary.routing.handled);
|
||||
EXPECT_FALSE(dispatcher.GetFocusController().HasActivePath());
|
||||
}
|
||||
|
||||
TEST(UIInputDispatcherTest, KeyboardEventsRouteToFocusedPath) {
|
||||
UIInputDispatcher dispatcher{};
|
||||
const UIInputPath focusedPath = { 101u, 202u };
|
||||
dispatcher.GetFocusController().SetFocusedPath(focusedPath);
|
||||
|
||||
UIInputEvent event = {};
|
||||
event.type = UIInputEventType::KeyDown;
|
||||
event.keyCode = 'F';
|
||||
|
||||
const auto summary = dispatcher.Dispatch(
|
||||
event,
|
||||
{},
|
||||
[](const UIInputDispatchRequest&) {
|
||||
return XCEngine::UI::UIInputDispatchDecision{};
|
||||
});
|
||||
|
||||
EXPECT_EQ(summary.routing.plan.targetKind, XCEngine::UI::UIInputTargetKind::Focused);
|
||||
EXPECT_EQ(summary.routing.plan.targetPath, focusedPath);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
#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
|
||||
@@ -1,80 +0,0 @@
|
||||
#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
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
|
||||
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
|
||||
#include <XCEngine/UI/Runtime/UISceneRuntimeContext.h>
|
||||
@@ -22,6 +23,7 @@ using XCEngine::UI::Runtime::UISceneRuntimeContext;
|
||||
using XCEngine::UI::Runtime::UIDocumentScreenHost;
|
||||
using XCEngine::UI::Runtime::UIScreenStackController;
|
||||
using XCEngine::UI::Runtime::UISystem;
|
||||
using XCEngine::Input::KeyCode;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -515,6 +517,79 @@ TEST(UIRuntimeTest, DocumentHostTracksHoverFocusAndPointerCaptureAcrossFrames) {
|
||||
EXPECT_NE(afterRelease.hoveredStateKey.find("/input-route"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, DocumentHostTraversesKeyboardFocusAndKeyboardActivationAcrossFrames) {
|
||||
TempFileScope viewFile(
|
||||
"xcui_runtime_keyboard_focus",
|
||||
".xcui",
|
||||
"<View name=\"Keyboard Focus Test\">\n"
|
||||
" <Column padding=\"18\" gap=\"10\">\n"
|
||||
" <Button id=\"focus-first\" text=\"First Focus\" />\n"
|
||||
" <Button id=\"focus-second\" text=\"Second Focus\" />\n"
|
||||
" <Button id=\"focus-third\" text=\"Third Focus\" />\n"
|
||||
" </Column>\n"
|
||||
"</View>\n");
|
||||
UIDocumentScreenHost host = {};
|
||||
UIScreenPlayer player(host);
|
||||
|
||||
ASSERT_TRUE(player.Load(BuildScreenAsset(viewFile.Path(), "runtime.keyboard.focus")));
|
||||
|
||||
UIScreenFrameInput firstInput = BuildInputState(1u);
|
||||
firstInput.viewportRect = XCEngine::UI::UIRect(0.0f, 0.0f, 520.0f, 260.0f);
|
||||
player.Update(firstInput);
|
||||
const auto& initialSnapshot = host.GetInputDebugSnapshot();
|
||||
EXPECT_TRUE(initialSnapshot.focusedStateKey.empty());
|
||||
|
||||
UIScreenFrameInput tabInput = BuildInputState(2u);
|
||||
tabInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent tabEvent = {};
|
||||
tabEvent.type = XCEngine::UI::UIInputEventType::KeyDown;
|
||||
tabEvent.keyCode = static_cast<std::int32_t>(KeyCode::Tab);
|
||||
tabInput.events.push_back(tabEvent);
|
||||
player.Update(tabInput);
|
||||
const auto& afterFirstTab = host.GetInputDebugSnapshot();
|
||||
EXPECT_NE(afterFirstTab.focusedStateKey.find("/focus-first"), std::string::npos);
|
||||
EXPECT_EQ(afterFirstTab.lastResult, "Focus traversed");
|
||||
|
||||
UIScreenFrameInput secondTabInput = BuildInputState(3u);
|
||||
secondTabInput.viewportRect = firstInput.viewportRect;
|
||||
secondTabInput.events.push_back(tabEvent);
|
||||
player.Update(secondTabInput);
|
||||
const auto& afterSecondTab = host.GetInputDebugSnapshot();
|
||||
EXPECT_NE(afterSecondTab.focusedStateKey.find("/focus-second"), std::string::npos);
|
||||
|
||||
UIScreenFrameInput reverseTabInput = BuildInputState(4u);
|
||||
reverseTabInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent reverseTabEvent = tabEvent;
|
||||
reverseTabEvent.modifiers.shift = true;
|
||||
reverseTabInput.events.push_back(reverseTabEvent);
|
||||
player.Update(reverseTabInput);
|
||||
const auto& afterReverseTab = host.GetInputDebugSnapshot();
|
||||
EXPECT_NE(afterReverseTab.focusedStateKey.find("/focus-first"), std::string::npos);
|
||||
|
||||
UIScreenFrameInput enterDownInput = BuildInputState(5u);
|
||||
enterDownInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent enterDownEvent = {};
|
||||
enterDownEvent.type = XCEngine::UI::UIInputEventType::KeyDown;
|
||||
enterDownEvent.keyCode = static_cast<std::int32_t>(KeyCode::Enter);
|
||||
enterDownInput.events.push_back(enterDownEvent);
|
||||
player.Update(enterDownInput);
|
||||
const auto& afterEnterDown = host.GetInputDebugSnapshot();
|
||||
EXPECT_NE(afterEnterDown.focusedStateKey.find("/focus-first"), std::string::npos);
|
||||
EXPECT_NE(afterEnterDown.activeStateKey.find("/focus-first"), std::string::npos);
|
||||
EXPECT_EQ(afterEnterDown.lastTargetKind, "Focused");
|
||||
|
||||
UIScreenFrameInput enterUpInput = BuildInputState(6u);
|
||||
enterUpInput.viewportRect = firstInput.viewportRect;
|
||||
XCEngine::UI::UIInputEvent enterUpEvent = {};
|
||||
enterUpEvent.type = XCEngine::UI::UIInputEventType::KeyUp;
|
||||
enterUpEvent.keyCode = static_cast<std::int32_t>(KeyCode::Enter);
|
||||
enterUpInput.events.push_back(enterUpEvent);
|
||||
player.Update(enterUpInput);
|
||||
const auto& afterEnterUp = host.GetInputDebugSnapshot();
|
||||
EXPECT_NE(afterEnterUp.focusedStateKey.find("/focus-first"), std::string::npos);
|
||||
EXPECT_TRUE(afterEnterUp.activeStateKey.empty());
|
||||
}
|
||||
|
||||
TEST(UIRuntimeTest, ScreenPlayerConsumeLastFrameReturnsDetachedPacketAndClearsBorrowedState) {
|
||||
TempFileScope viewFile("xcui_runtime_consume_player", ".xcui", BuildViewMarkup("Runtime Consume"));
|
||||
UIDocumentScreenHost host = {};
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||
|
||||
namespace {
|
||||
|
||||
using XCEngine::UI::Widgets::UISelectionModel;
|
||||
|
||||
TEST(UISelectionModelTest, SetAndClearSelectionTrackCurrentId) {
|
||||
UISelectionModel selection = {};
|
||||
|
||||
EXPECT_FALSE(selection.HasSelection());
|
||||
EXPECT_TRUE(selection.GetSelectedId().empty());
|
||||
|
||||
EXPECT_TRUE(selection.SetSelection("assetLighting"));
|
||||
EXPECT_TRUE(selection.HasSelection());
|
||||
EXPECT_TRUE(selection.IsSelected("assetLighting"));
|
||||
EXPECT_EQ(selection.GetSelectedId(), "assetLighting");
|
||||
|
||||
EXPECT_FALSE(selection.SetSelection("assetLighting"));
|
||||
EXPECT_TRUE(selection.ClearSelection());
|
||||
EXPECT_FALSE(selection.HasSelection());
|
||||
EXPECT_TRUE(selection.GetSelectedId().empty());
|
||||
EXPECT_FALSE(selection.ClearSelection());
|
||||
}
|
||||
|
||||
TEST(UISelectionModelTest, ToggleSelectionSelectsAndDeselectsMatchingId) {
|
||||
UISelectionModel selection = {};
|
||||
|
||||
EXPECT_TRUE(selection.ToggleSelection("treeScenes"));
|
||||
EXPECT_EQ(selection.GetSelectedId(), "treeScenes");
|
||||
|
||||
EXPECT_TRUE(selection.ToggleSelection("treeScenes"));
|
||||
EXPECT_TRUE(selection.GetSelectedId().empty());
|
||||
|
||||
EXPECT_TRUE(selection.ToggleSelection("treeMaterials"));
|
||||
EXPECT_EQ(selection.GetSelectedId(), "treeMaterials");
|
||||
EXPECT_TRUE(selection.ToggleSelection("treeUi"));
|
||||
EXPECT_EQ(selection.GetSelectedId(), "treeUi");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,59 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/UI/Text/UITextEditing.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace UIText = XCEngine::UI::Text;
|
||||
|
||||
TEST(UITextEditingTest, Utf8CountingAndCaretOffsetsRespectCodepointBoundaries) {
|
||||
const std::string text = std::string("A") + "\xE4\xBD\xA0" + "B";
|
||||
|
||||
EXPECT_EQ(UIText::CountUtf8Codepoints(text), 3u);
|
||||
EXPECT_EQ(UIText::AdvanceUtf8Offset(text, 0u), 1u);
|
||||
EXPECT_EQ(UIText::AdvanceUtf8Offset(text, 1u), 4u);
|
||||
EXPECT_EQ(UIText::AdvanceUtf8Offset(text, 4u), 5u);
|
||||
EXPECT_EQ(UIText::RetreatUtf8Offset(text, text.size()), 4u);
|
||||
EXPECT_EQ(UIText::RetreatUtf8Offset(text, 4u), 1u);
|
||||
EXPECT_EQ(UIText::RetreatUtf8Offset(text, 1u), 0u);
|
||||
}
|
||||
|
||||
TEST(UITextEditingTest, AppendUtf8CodepointEncodesCharactersAndSkipsInvalidSurrogates) {
|
||||
std::string text = {};
|
||||
UIText::AppendUtf8Codepoint(text, 'A');
|
||||
UIText::AppendUtf8Codepoint(text, 0x4F60u);
|
||||
UIText::AppendUtf8Codepoint(text, 0x1F642u);
|
||||
UIText::AppendUtf8Codepoint(text, 0xD800u);
|
||||
|
||||
EXPECT_EQ(text, std::string("A") + "\xE4\xBD\xA0" + "\xF0\x9F\x99\x82");
|
||||
EXPECT_EQ(UIText::CountUtf8Codepoints(text), 3u);
|
||||
}
|
||||
|
||||
TEST(UITextEditingTest, SplitLinesAndLineHelpersTrackMultilineRanges) {
|
||||
const std::string text = "alpha\nbeta\n";
|
||||
|
||||
const auto lines = UIText::SplitLines(text);
|
||||
ASSERT_EQ(lines.size(), 3u);
|
||||
EXPECT_EQ(lines[0], "alpha");
|
||||
EXPECT_EQ(lines[1], "beta");
|
||||
EXPECT_EQ(lines[2], "");
|
||||
EXPECT_EQ(UIText::CountTextLines(text), 3u);
|
||||
EXPECT_EQ(UIText::CountUtf8CodepointsInRange(text, 6u, 10u), 4u);
|
||||
EXPECT_EQ(UIText::FindLineStartOffset(text, 7u), 6u);
|
||||
EXPECT_EQ(UIText::FindLineEndOffset(text, 7u), 10u);
|
||||
}
|
||||
|
||||
TEST(UITextEditingTest, MoveCaretVerticallyPreservesUtf8ColumnWhenPossible) {
|
||||
const std::string text = std::string("A") + "\xE4\xBD\xA0" + "Z\nBC\n";
|
||||
const std::size_t secondColumnCaret = UIText::AdvanceUtf8Offset(text, 1u);
|
||||
|
||||
const std::size_t movedDown = UIText::MoveCaretVertically(text, secondColumnCaret, 1);
|
||||
const std::size_t movedBackUp = UIText::MoveCaretVertically(text, movedDown, -1);
|
||||
|
||||
EXPECT_EQ(movedDown, 8u);
|
||||
EXPECT_EQ(movedBackUp, secondColumnCaret);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,287 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/UI/Text/UITextInputController.h>
|
||||
|
||||
namespace {
|
||||
|
||||
namespace UIText = XCEngine::UI::Text;
|
||||
using XCEngine::Input::KeyCode;
|
||||
|
||||
TEST(UITextInputControllerTest, InsertCharacterTracksUtf8CaretMovement) {
|
||||
UIText::UITextInputState state = {};
|
||||
|
||||
EXPECT_TRUE(UIText::InsertCharacter(state, 'A'));
|
||||
EXPECT_TRUE(UIText::InsertCharacter(state, 0x4F60u));
|
||||
EXPECT_EQ(state.value, std::string("A") + "\xE4\xBD\xA0");
|
||||
EXPECT_EQ(state.caret, state.value.size());
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, BackspaceAndArrowKeysUseUtf8Boundaries) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = std::string("A") + "\xE4\xBD\xA0" + "B";
|
||||
state.caret = state.value.size();
|
||||
|
||||
const auto moveLeft = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Left),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(moveLeft.handled);
|
||||
EXPECT_EQ(state.caret, 4u);
|
||||
|
||||
const auto backspace = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Backspace),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(backspace.handled);
|
||||
EXPECT_TRUE(backspace.valueChanged);
|
||||
EXPECT_EQ(state.value, "AB");
|
||||
EXPECT_EQ(state.caret, 1u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, DeleteUsesUtf8BoundariesAndLeavesCaretAtDeletePoint) {
|
||||
if (static_cast<std::int32_t>(KeyCode::Delete) ==
|
||||
static_cast<std::int32_t>(KeyCode::Backspace)) {
|
||||
GTEST_SKIP() << "KeyCode::Delete currently aliases Backspace.";
|
||||
}
|
||||
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = std::string("A") + "\xE4\xBD\xA0" + "B";
|
||||
state.caret = 1u;
|
||||
|
||||
const auto result = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Delete),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(result.handled);
|
||||
EXPECT_TRUE(result.valueChanged);
|
||||
EXPECT_FALSE(result.submitRequested);
|
||||
EXPECT_EQ(state.value, "AB");
|
||||
EXPECT_EQ(state.caret, 1u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, DeleteClampsOversizedCaretAndDoesNotMutateAtDocumentEnd) {
|
||||
if (static_cast<std::int32_t>(KeyCode::Delete) ==
|
||||
static_cast<std::int32_t>(KeyCode::Backspace)) {
|
||||
GTEST_SKIP() << "KeyCode::Delete currently aliases Backspace.";
|
||||
}
|
||||
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "AB";
|
||||
state.caret = 99u;
|
||||
|
||||
const auto result = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Delete),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(result.handled);
|
||||
EXPECT_FALSE(result.valueChanged);
|
||||
EXPECT_FALSE(result.submitRequested);
|
||||
EXPECT_EQ(state.value, "AB");
|
||||
EXPECT_EQ(state.caret, state.value.size());
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, SingleLineEnterRequestsSubmitWithoutMutatingValue) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "prompt";
|
||||
state.caret = state.value.size();
|
||||
|
||||
const auto result = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Enter),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(result.handled);
|
||||
EXPECT_FALSE(result.valueChanged);
|
||||
EXPECT_TRUE(result.submitRequested);
|
||||
EXPECT_EQ(state.value, "prompt");
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, MultilineEnterAndVerticalMovementStayInController) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = std::string("A") + "\xE4\xBD\xA0" + "Z\nBC";
|
||||
state.caret = 4u;
|
||||
|
||||
const UIText::UITextInputOptions options = { true, 4u };
|
||||
|
||||
const auto moveDown = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Down),
|
||||
{},
|
||||
options);
|
||||
EXPECT_TRUE(moveDown.handled);
|
||||
EXPECT_EQ(state.caret, 8u);
|
||||
|
||||
const auto enter = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Enter),
|
||||
{},
|
||||
options);
|
||||
EXPECT_TRUE(enter.handled);
|
||||
EXPECT_TRUE(enter.valueChanged);
|
||||
EXPECT_EQ(state.value, std::string("A") + "\xE4\xBD\xA0" + "Z\nBC\n");
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, HomeAndEndRespectSingleLineAndMultilineBounds) {
|
||||
UIText::UITextInputState singleLine = {};
|
||||
singleLine.value = "prompt";
|
||||
singleLine.caret = 2u;
|
||||
|
||||
const auto singleHome = UIText::HandleKeyDown(
|
||||
singleLine,
|
||||
static_cast<std::int32_t>(KeyCode::Home),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(singleHome.handled);
|
||||
EXPECT_EQ(singleLine.caret, 0u);
|
||||
|
||||
const auto singleEnd = UIText::HandleKeyDown(
|
||||
singleLine,
|
||||
static_cast<std::int32_t>(KeyCode::End),
|
||||
{},
|
||||
{});
|
||||
EXPECT_TRUE(singleEnd.handled);
|
||||
EXPECT_EQ(singleLine.caret, singleLine.value.size());
|
||||
|
||||
UIText::UITextInputState multiline = {};
|
||||
multiline.value = "root\nleaf\nend";
|
||||
multiline.caret = 7u;
|
||||
const UIText::UITextInputOptions options = { true, 4u };
|
||||
|
||||
const auto multilineHome = UIText::HandleKeyDown(
|
||||
multiline,
|
||||
static_cast<std::int32_t>(KeyCode::Home),
|
||||
{},
|
||||
options);
|
||||
EXPECT_TRUE(multilineHome.handled);
|
||||
EXPECT_EQ(multiline.caret, 5u);
|
||||
|
||||
multiline.caret = 7u;
|
||||
const auto multilineEnd = UIText::HandleKeyDown(
|
||||
multiline,
|
||||
static_cast<std::int32_t>(KeyCode::End),
|
||||
{},
|
||||
options);
|
||||
EXPECT_TRUE(multilineEnd.handled);
|
||||
EXPECT_EQ(multiline.caret, 9u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, ClampCaretAndInsertCharacterRecoverFromOversizedCaret) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "go";
|
||||
state.caret = 42u;
|
||||
|
||||
UIText::ClampCaret(state);
|
||||
EXPECT_EQ(state.caret, state.value.size());
|
||||
|
||||
state.caret = 42u;
|
||||
EXPECT_TRUE(UIText::InsertCharacter(state, '!'));
|
||||
EXPECT_EQ(state.value, "go!");
|
||||
EXPECT_EQ(state.caret, state.value.size());
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, MultilineTabAndShiftTabIndentAndOutdentCurrentLine) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "root\nnode";
|
||||
state.caret = 5u;
|
||||
|
||||
const UIText::UITextInputOptions options = { true, 4u };
|
||||
|
||||
const auto indent = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
{},
|
||||
options);
|
||||
EXPECT_TRUE(indent.handled);
|
||||
EXPECT_TRUE(indent.valueChanged);
|
||||
EXPECT_EQ(state.value, "root\n node");
|
||||
EXPECT_EQ(state.caret, 9u);
|
||||
|
||||
XCEngine::UI::UIInputModifiers modifiers = {};
|
||||
modifiers.shift = true;
|
||||
const auto outdent = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
modifiers,
|
||||
options);
|
||||
EXPECT_TRUE(outdent.handled);
|
||||
EXPECT_TRUE(outdent.valueChanged);
|
||||
EXPECT_EQ(state.value, "root\nnode");
|
||||
EXPECT_EQ(state.caret, 5u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, ShiftTabWithoutLeadingSpacesIsHandledWithoutMutatingText) {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "root\nnode";
|
||||
state.caret = 5u;
|
||||
|
||||
XCEngine::UI::UIInputModifiers modifiers = {};
|
||||
modifiers.shift = true;
|
||||
|
||||
const auto result = UIText::HandleKeyDown(
|
||||
state,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
modifiers,
|
||||
{ true, 4u });
|
||||
EXPECT_TRUE(result.handled);
|
||||
EXPECT_FALSE(result.valueChanged);
|
||||
EXPECT_FALSE(result.submitRequested);
|
||||
EXPECT_EQ(state.value, "root\nnode");
|
||||
EXPECT_EQ(state.caret, 5u);
|
||||
}
|
||||
|
||||
TEST(UITextInputControllerTest, MultilineTabIgnoresSystemModifiers) {
|
||||
const auto buildState = []() {
|
||||
UIText::UITextInputState state = {};
|
||||
state.value = "root\nnode";
|
||||
state.caret = 5u;
|
||||
return state;
|
||||
};
|
||||
|
||||
const UIText::UITextInputOptions options = { true, 4u };
|
||||
|
||||
XCEngine::UI::UIInputModifiers control = {};
|
||||
control.control = true;
|
||||
auto controlState = buildState();
|
||||
const auto controlResult = UIText::HandleKeyDown(
|
||||
controlState,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
control,
|
||||
options);
|
||||
EXPECT_FALSE(controlResult.handled);
|
||||
EXPECT_FALSE(controlResult.valueChanged);
|
||||
EXPECT_EQ(controlState.value, "root\nnode");
|
||||
EXPECT_EQ(controlState.caret, 5u);
|
||||
|
||||
XCEngine::UI::UIInputModifiers alt = {};
|
||||
alt.alt = true;
|
||||
auto altState = buildState();
|
||||
const auto altResult = UIText::HandleKeyDown(
|
||||
altState,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
alt,
|
||||
options);
|
||||
EXPECT_FALSE(altResult.handled);
|
||||
EXPECT_FALSE(altResult.valueChanged);
|
||||
EXPECT_EQ(altState.value, "root\nnode");
|
||||
EXPECT_EQ(altState.caret, 5u);
|
||||
|
||||
XCEngine::UI::UIInputModifiers superModifier = {};
|
||||
superModifier.super = true;
|
||||
auto superState = buildState();
|
||||
const auto superResult = UIText::HandleKeyDown(
|
||||
superState,
|
||||
static_cast<std::int32_t>(KeyCode::Tab),
|
||||
superModifier,
|
||||
options);
|
||||
EXPECT_FALSE(superResult.handled);
|
||||
EXPECT_FALSE(superResult.valueChanged);
|
||||
EXPECT_EQ(superState.value, "root\nnode");
|
||||
EXPECT_EQ(superState.caret, 5u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user