engine: sync editor rendering and ui changes

This commit is contained in:
2026-04-08 16:09:15 +08:00
parent 31756847ab
commit 162f1cc12e
153 changed files with 4454 additions and 2990 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {};

View File

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

View File

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

View File

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