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

@@ -106,35 +106,30 @@ TEST(MeshFilterComponent_Test, SetMeshCachesResourceAndPath) {
TEST(MeshFilterComponent_Test, SerializeAndDeserializePreservesPath) {
MeshFilterComponent source;
Mesh* mesh = CreateTestMesh("Quad", "Meshes/serialized.mesh");
source.SetMesh(mesh);
source.SetMeshPath("builtin://meshes/cube");
std::stringstream stream;
source.Serialize(stream);
const std::string serialized = stream.str();
EXPECT_NE(serialized.find("meshRef="), std::string::npos);
EXPECT_NE(serialized.find("meshPath=Meshes/serialized.mesh;"), std::string::npos);
EXPECT_EQ(serialized.find("mesh=Meshes/serialized.mesh;"), std::string::npos);
EXPECT_NE(serialized.find("meshPath=builtin://meshes/cube;"), std::string::npos);
MeshFilterComponent target;
std::stringstream deserializeStream(serialized);
target.Deserialize(deserializeStream);
EXPECT_EQ(target.GetMeshPath(), "Meshes/serialized.mesh");
EXPECT_EQ(target.GetMesh(), nullptr);
EXPECT_EQ(target.GetMeshPath(), "builtin://meshes/cube");
ASSERT_NE(target.GetMesh(), nullptr);
EXPECT_FALSE(target.GetMeshAssetRef().IsValid());
source.ClearMesh();
delete mesh;
}
TEST(MeshFilterComponent_Test, DeserializeSupportsLegacyMeshKey) {
TEST(MeshFilterComponent_Test, DeserializeIgnoresPlainMeshPathWithoutAssetRef) {
MeshFilterComponent target;
std::stringstream stream("mesh=Meshes/legacy.mesh;meshRef=;");
std::stringstream stream("meshPath=Meshes/legacy.mesh;meshRef=;");
target.Deserialize(stream);
EXPECT_EQ(target.GetMeshPath(), "Meshes/legacy.mesh");
EXPECT_TRUE(target.GetMeshPath().empty());
EXPECT_EQ(target.GetMesh(), nullptr);
EXPECT_FALSE(target.GetMeshAssetRef().IsValid());
}
@@ -165,16 +160,16 @@ TEST(MeshFilterComponent_Test, DeferredSceneDeserializeLoadsMeshAsyncByPath) {
{
ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled());
std::stringstream stream("mesh=Meshes/async.mesh;meshRef=;");
std::stringstream stream("meshPath=test://meshes/async.mesh;meshRef=;");
target.Deserialize(stream);
}
EXPECT_EQ(target.GetMeshPath(), "Meshes/async.mesh");
EXPECT_EQ(target.GetMeshPath(), "test://meshes/async.mesh");
EXPECT_EQ(target.GetMesh(), nullptr);
EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize);
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
ASSERT_NE(target.GetMesh(), nullptr);
EXPECT_EQ(target.GetMeshPath(), "Meshes/async.mesh");
EXPECT_EQ(target.GetMeshPath(), "test://meshes/async.mesh");
EXPECT_EQ(target.GetMesh()->GetVertexCount(), 3u);
manager.RegisterLoader(originalLoader);
@@ -209,10 +204,8 @@ TEST(MeshRendererComponent_Test, SetMaterialsKeepsSlotsAndFlags) {
TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesMaterialPathsAndSettings) {
MeshRendererComponent source;
Material* material0 = CreateTestMaterial("M0", "Materials/serialized0.mat");
Material* material1 = CreateTestMaterial("M1", "Materials/serialized1.mat");
source.SetMaterial(0, material0);
source.SetMaterial(1, material1);
source.SetMaterialPath(0, "builtin://materials/default-primitive");
source.SetMaterialPath(1, "builtin://materials/default-primitive");
source.SetCastShadows(false);
source.SetReceiveShadows(true);
source.SetRenderLayer(3);
@@ -221,33 +214,27 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesMaterialPathsAn
source.Serialize(stream);
const std::string serialized = stream.str();
EXPECT_NE(
serialized.find("materialPaths=Materials/serialized0.mat|Materials/serialized1.mat;"),
serialized.find("materialPaths=builtin://materials/default-primitive|builtin://materials/default-primitive;"),
std::string::npos);
EXPECT_NE(serialized.find("materialRefs=|;"), std::string::npos);
EXPECT_EQ(serialized.find("materials="), std::string::npos);
MeshRendererComponent target;
std::stringstream deserializeStream(serialized);
target.Deserialize(deserializeStream);
ASSERT_EQ(target.GetMaterialCount(), 2u);
EXPECT_EQ(target.GetMaterial(0), nullptr);
EXPECT_EQ(target.GetMaterial(1), nullptr);
EXPECT_EQ(target.GetMaterialPaths()[0], "Materials/serialized0.mat");
EXPECT_EQ(target.GetMaterialPaths()[1], "Materials/serialized1.mat");
ASSERT_NE(target.GetMaterial(0), nullptr);
ASSERT_NE(target.GetMaterial(1), nullptr);
EXPECT_EQ(target.GetMaterialPaths()[0], "builtin://materials/default-primitive");
EXPECT_EQ(target.GetMaterialPaths()[1], "builtin://materials/default-primitive");
EXPECT_FALSE(target.GetCastShadows());
EXPECT_TRUE(target.GetReceiveShadows());
EXPECT_EQ(target.GetRenderLayer(), 3u);
source.ClearMaterials();
delete material0;
delete material1;
}
TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesTrailingEmptyMaterialSlots) {
MeshRendererComponent source;
Material* material0 = CreateTestMaterial("M0", "Materials/serialized0.mat");
source.SetMaterial(0, material0);
source.SetMaterialPath(0, "builtin://materials/default-primitive");
source.SetMaterialPath(1, "");
source.SetCastShadows(false);
source.SetReceiveShadows(true);
@@ -256,25 +243,21 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesTrailingEmptyMa
std::stringstream stream;
source.Serialize(stream);
const std::string serialized = stream.str();
EXPECT_NE(serialized.find("materialPaths=Materials/serialized0.mat|;"), std::string::npos);
EXPECT_NE(serialized.find("materialPaths=builtin://materials/default-primitive|;"), std::string::npos);
EXPECT_NE(serialized.find("materialRefs=|;"), std::string::npos);
EXPECT_EQ(serialized.find("materials="), std::string::npos);
MeshRendererComponent target;
std::stringstream deserializeStream(serialized);
target.Deserialize(deserializeStream);
ASSERT_EQ(target.GetMaterialCount(), 2u);
EXPECT_EQ(target.GetMaterial(0), nullptr);
ASSERT_NE(target.GetMaterial(0), nullptr);
EXPECT_EQ(target.GetMaterial(1), nullptr);
EXPECT_EQ(target.GetMaterialPath(0), "Materials/serialized0.mat");
EXPECT_EQ(target.GetMaterialPath(0), "builtin://materials/default-primitive");
EXPECT_EQ(target.GetMaterialPath(1), "");
EXPECT_FALSE(target.GetCastShadows());
EXPECT_TRUE(target.GetReceiveShadows());
EXPECT_EQ(target.GetRenderLayer(), 9u);
source.ClearMaterials();
delete material0;
}
TEST(MeshRendererComponent_Test, SetMaterialPathPreservesPathWithoutLoadedResource) {
@@ -293,15 +276,15 @@ TEST(MeshRendererComponent_Test, SetMaterialPathPreservesPathWithoutLoadedResour
EXPECT_EQ(component.GetMaterial(1), nullptr);
}
TEST(MeshRendererComponent_Test, DeserializeSupportsLegacyMaterialsKey) {
TEST(MeshRendererComponent_Test, DeserializeIgnoresPlainMaterialPathsWithoutAssetRefs) {
MeshRendererComponent target;
std::stringstream stream(
"materials=Materials/legacy0.mat|;materialRefs=|;castShadows=0;receiveShadows=1;renderLayer=5;");
"materialPaths=Materials/legacy0.mat|;materialRefs=|;castShadows=0;receiveShadows=1;renderLayer=5;");
target.Deserialize(stream);
ASSERT_EQ(target.GetMaterialCount(), 2u);
EXPECT_EQ(target.GetMaterialPath(0), "Materials/legacy0.mat");
EXPECT_EQ(target.GetMaterialPath(0), "");
EXPECT_EQ(target.GetMaterialPath(1), "");
EXPECT_EQ(target.GetMaterial(0), nullptr);
EXPECT_EQ(target.GetMaterial(1), nullptr);
@@ -351,7 +334,6 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializeLoadsProjectMaterialByAs
EXPECT_NE(serialized.find("materialPaths=;"), std::string::npos);
EXPECT_NE(serialized.find("materialRefs="), std::string::npos);
EXPECT_EQ(serialized.find("materialRefs=;"), std::string::npos);
EXPECT_EQ(serialized.find("materials="), std::string::npos);
std::stringstream deserializeStream(serialized);
MeshRendererComponent target;

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

View File

@@ -1,6 +1,7 @@
#include "fixtures/OpenGLTestFixture.h"
#include "XCEngine/RHI/OpenGL/OpenGLDescriptorSet.h"
#include "XCEngine/RHI/OpenGL/OpenGLPipelineState.h"
#include "XCEngine/RHI/OpenGL/OpenGLPipelineLayout.h"
#include "XCEngine/RHI/OpenGL/OpenGLResourceView.h"
#include "XCEngine/RHI/OpenGL/OpenGLSampler.h"
@@ -21,6 +22,7 @@
#include <cstring>
#include <algorithm>
#include <filesystem>
#include <iostream>
#include <vector>
using namespace XCEngine::RHI;
@@ -173,6 +175,128 @@ float4 MainPS(PSInput input) : SV_TARGET {
delete pipelineState;
}
TEST_F(OpenGLTestFixture, Device_CreatePipelineState_HlslGraphicsShaders_AssignsCombinedSamplerUniformUnits) {
ASSERT_TRUE(GetDevice()->MakeContextCurrent());
if (!SupportsOpenGLHlslToolchainForTests()) {
GTEST_SKIP() << "glslangValidator.exe or spirv-cross.exe was not found.";
}
static const char* hlslSource = R"(
Texture2D BaseColorTexture : register(t0);
SamplerState LinearClampSampler : register(s0);
Texture2D ShadowMapTexture : register(t1);
SamplerState ShadowMapSampler : register(s1);
struct VSInput {
float4 position : POSITION;
float2 texcoord : TEXCOORD0;
};
struct PSInput {
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
PSInput MainVS(VSInput input) {
PSInput output;
output.position = input.position;
output.texcoord = input.texcoord;
return output;
}
float4 MainPS(PSInput input) : SV_TARGET {
float4 baseColor = BaseColorTexture.Sample(LinearClampSampler, input.texcoord);
float shadow = ShadowMapTexture.Sample(ShadowMapSampler, input.texcoord).r;
return float4(baseColor.rgb * shadow, baseColor.a);
}
)";
GraphicsPipelineDesc pipelineDesc = {};
pipelineDesc.topologyType = static_cast<uint32_t>(PrimitiveTopologyType::Triangle);
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
InputElementDesc position = {};
position.semanticName = "POSITION";
position.semanticIndex = 0;
position.format = static_cast<uint32_t>(Format::R32G32B32A32_Float);
position.inputSlot = 0;
position.alignedByteOffset = 0;
pipelineDesc.inputLayout.elements.push_back(position);
InputElementDesc texcoord = {};
texcoord.semanticName = "TEXCOORD";
texcoord.semanticIndex = 0;
texcoord.format = static_cast<uint32_t>(Format::R32G32_Float);
texcoord.inputSlot = 0;
texcoord.alignedByteOffset = sizeof(float) * 4;
pipelineDesc.inputLayout.elements.push_back(texcoord);
pipelineDesc.vertexShader.source.assign(hlslSource, hlslSource + std::strlen(hlslSource));
pipelineDesc.vertexShader.sourceLanguage = ShaderLanguage::HLSL;
pipelineDesc.vertexShader.entryPoint = L"MainVS";
pipelineDesc.vertexShader.profile = L"vs_5_0";
pipelineDesc.fragmentShader.source.assign(hlslSource, hlslSource + std::strlen(hlslSource));
pipelineDesc.fragmentShader.sourceLanguage = ShaderLanguage::HLSL;
pipelineDesc.fragmentShader.entryPoint = L"MainPS";
pipelineDesc.fragmentShader.profile = L"ps_5_0";
RHIPipelineState* pipelineState = GetDevice()->CreatePipelineState(pipelineDesc);
ASSERT_NE(pipelineState, nullptr);
const GLuint program = static_cast<OpenGLPipelineState*>(pipelineState)->GetProgram();
ASSERT_NE(program, 0u);
const GLint baseColorLocation =
glGetUniformLocation(program, "SPIRV_Cross_CombinedBaseColorTextureLinearClampSampler");
const GLint shadowLocation =
glGetUniformLocation(program, "SPIRV_Cross_CombinedShadowMapTextureShadowMapSampler");
ASSERT_GE(baseColorLocation, 0);
ASSERT_GE(shadowLocation, 0);
GLint baseColorUnit = -1;
GLint shadowUnit = -1;
glGetUniformiv(program, baseColorLocation, &baseColorUnit);
glGetUniformiv(program, shadowLocation, &shadowUnit);
GLint activeUniformCount = 0;
glGetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeUniformCount);
for (GLint uniformIndex = 0; uniformIndex < activeUniformCount; ++uniformIndex) {
GLsizei nameLength = 0;
GLint arraySize = 0;
GLenum type = 0;
char nameBuffer[256] = {};
glGetActiveUniform(
program,
static_cast<GLuint>(uniformIndex),
static_cast<GLsizei>(sizeof(nameBuffer)),
&nameLength,
&arraySize,
&type,
nameBuffer);
if (nameLength <= 0) {
continue;
}
GLint location = glGetUniformLocation(program, nameBuffer);
GLint value = -1;
if (location >= 0) {
glGetUniformiv(program, location, &value);
}
std::cout << "uniform[" << uniformIndex << "] name=" << nameBuffer
<< " location=" << location
<< " value=" << value
<< " type=" << type
<< " arraySize=" << arraySize
<< std::endl;
}
EXPECT_EQ(baseColorUnit, 0);
EXPECT_EQ(shadowUnit, 1);
pipelineState->Shutdown();
delete pipelineState;
}
TEST_F(OpenGLTestFixture, CommandList_SetRenderTargets_BindsColorAndDepthAttachments) {
TextureDesc colorDesc = {};
colorDesc.width = 128;

View File

@@ -1,6 +1,7 @@
#include "fixtures/OpenGLTestFixture.h"
#include "XCEngine/RHI/OpenGL/OpenGLCommandList.h"
#include "XCEngine/RHI/OpenGL/OpenGLBuffer.h"
#include "XCEngine/RHI/OpenGL/OpenGLPipelineState.h"
#include "XCEngine/RHI/OpenGL/OpenGLVertexArray.h"
using namespace XCEngine::RHI;
@@ -160,3 +161,30 @@ TEST_F(OpenGLTestFixture, CommandList_SetBlendFactor) {
EXPECT_FLOAT_EQ(color[2], 0.5f);
EXPECT_FLOAT_EQ(color[3], 1.0f);
}
TEST_F(OpenGLTestFixture, CommandList_SetStencilRef_UpdatesActivePipelineStencilReference) {
OpenGLCommandList cmdList;
OpenGLPipelineState pipeline;
DepthStencilStateDesc state = {};
state.stencilEnable = true;
state.front.func = static_cast<uint32_t>(ComparisonFunc::Equal);
state.back.func = static_cast<uint32_t>(ComparisonFunc::NotEqual);
pipeline.SetDepthStencilState(state);
cmdList.SetPipelineState(&pipeline);
cmdList.SetStencilRef(5);
GLint frontRef = 0;
GLint backRef = 0;
GLint frontFunc = 0;
GLint backFunc = 0;
glGetIntegerv(GL_STENCIL_REF, &frontRef);
glGetIntegerv(GL_STENCIL_BACK_REF, &backRef);
glGetIntegerv(GL_STENCIL_FUNC, &frontFunc);
glGetIntegerv(GL_STENCIL_BACK_FUNC, &backFunc);
EXPECT_EQ(frontRef, 5);
EXPECT_EQ(backRef, 5);
EXPECT_EQ(frontFunc, GL_EQUAL);
EXPECT_EQ(backFunc, GL_NOTEQUAL);
}

View File

@@ -39,6 +39,92 @@ TEST_F(OpenGLTestFixture, PipelineState_SetBlendState) {
EXPECT_EQ(blend, 1);
}
TEST_F(OpenGLTestFixture, PipelineState_ApplyDepthStencil_UsesSeparateStencilFaces) {
OpenGLPipelineState pipeline;
DepthStencilStateDesc state = {};
state.depthTestEnable = true;
state.depthWriteEnable = true;
state.depthFunc = static_cast<uint32_t>(ComparisonFunc::LessEqual);
state.stencilEnable = true;
state.stencilReadMask = 0x3F;
state.stencilWriteMask = 0x1F;
state.front.func = static_cast<uint32_t>(ComparisonFunc::Equal);
state.front.failOp = static_cast<uint32_t>(StencilOp::Replace);
state.front.passOp = static_cast<uint32_t>(StencilOp::Incr);
state.front.depthFailOp = static_cast<uint32_t>(StencilOp::DecrSat);
state.back.func = static_cast<uint32_t>(ComparisonFunc::NotEqual);
state.back.failOp = static_cast<uint32_t>(StencilOp::Invert);
state.back.passOp = static_cast<uint32_t>(StencilOp::Decr);
state.back.depthFailOp = static_cast<uint32_t>(StencilOp::Zero);
pipeline.SetDepthStencilState(state);
pipeline.ApplyDepthStencil();
EXPECT_TRUE(glIsEnabled(GL_STENCIL_TEST));
GLint frontFunc = 0;
GLint backFunc = 0;
GLint frontRef = 0;
GLint backRef = 0;
GLint frontValueMask = 0;
GLint backValueMask = 0;
GLint frontWriteMask = 0;
GLint backWriteMask = 0;
GLint frontFail = 0;
GLint backFail = 0;
GLint frontPassDepthPass = 0;
GLint backPassDepthPass = 0;
glGetIntegerv(GL_STENCIL_FUNC, &frontFunc);
glGetIntegerv(GL_STENCIL_BACK_FUNC, &backFunc);
glGetIntegerv(GL_STENCIL_REF, &frontRef);
glGetIntegerv(GL_STENCIL_BACK_REF, &backRef);
glGetIntegerv(GL_STENCIL_VALUE_MASK, &frontValueMask);
glGetIntegerv(GL_STENCIL_BACK_VALUE_MASK, &backValueMask);
glGetIntegerv(GL_STENCIL_WRITEMASK, &frontWriteMask);
glGetIntegerv(GL_STENCIL_BACK_WRITEMASK, &backWriteMask);
glGetIntegerv(GL_STENCIL_FAIL, &frontFail);
glGetIntegerv(GL_STENCIL_BACK_FAIL, &backFail);
glGetIntegerv(GL_STENCIL_PASS_DEPTH_PASS, &frontPassDepthPass);
glGetIntegerv(GL_STENCIL_BACK_PASS_DEPTH_PASS, &backPassDepthPass);
EXPECT_EQ(frontFunc, GL_EQUAL);
EXPECT_EQ(backFunc, GL_NOTEQUAL);
EXPECT_EQ(frontRef, 0);
EXPECT_EQ(backRef, 0);
EXPECT_EQ(frontValueMask, 0x3F);
EXPECT_EQ(backValueMask, 0x3F);
EXPECT_EQ(frontWriteMask, 0x1F);
EXPECT_EQ(backWriteMask, 0x1F);
EXPECT_EQ(frontFail, GL_REPLACE);
EXPECT_EQ(backFail, GL_INVERT);
EXPECT_EQ(frontPassDepthPass, GL_INCR_WRAP);
EXPECT_EQ(backPassDepthPass, GL_DECR_WRAP);
}
TEST_F(OpenGLTestFixture, PipelineState_ApplyRasterizer_EnablesPolygonOffset) {
OpenGLPipelineState pipeline;
RasterizerDesc state = {};
state.fillMode = static_cast<uint32_t>(FillMode::Solid);
state.cullMode = static_cast<uint32_t>(CullMode::Back);
state.frontFace = static_cast<uint32_t>(FrontFace::CounterClockwise);
state.slopeScaledDepthBias = 1.5f;
state.depthBias = 3;
pipeline.SetRasterizerState(state);
pipeline.ApplyRasterizer();
EXPECT_TRUE(glIsEnabled(GL_POLYGON_OFFSET_FILL));
GLfloat factor = 0.0f;
GLfloat units = 0.0f;
glGetFloatv(GL_POLYGON_OFFSET_FACTOR, &factor);
glGetFloatv(GL_POLYGON_OFFSET_UNITS, &units);
EXPECT_FLOAT_EQ(factor, 1.5f);
EXPECT_FLOAT_EQ(units, 3.0f);
}
TEST_F(OpenGLTestFixture, PipelineState_SetViewport_SetScissor) {
OpenGLPipelineState pipeline;

View File

@@ -90,7 +90,6 @@ TEST(Material, DefaultRenderMetadata) {
EXPECT_TRUE(material.GetRenderState().depthTestEnable);
EXPECT_TRUE(material.GetRenderState().depthWriteEnable);
EXPECT_EQ(material.GetRenderState().depthFunc, MaterialComparisonFunc::Less);
EXPECT_TRUE(material.GetLegacyShaderPassHint().Empty());
EXPECT_EQ(material.GetTagCount(), 0u);
}
@@ -120,6 +119,20 @@ TEST(Material, SetGetRenderState) {
renderState.depthWriteEnable = false;
renderState.depthFunc = MaterialComparisonFunc::LessEqual;
renderState.colorWriteMask = 0x7;
renderState.depthBiasFactor = 1.5f;
renderState.depthBiasUnits = 2;
renderState.stencil.enabled = true;
renderState.stencil.reference = 3;
renderState.stencil.readMask = 0x0F;
renderState.stencil.writeMask = 0xF0;
renderState.stencil.front.func = MaterialComparisonFunc::Equal;
renderState.stencil.front.passOp = MaterialStencilOp::Replace;
renderState.stencil.front.failOp = MaterialStencilOp::Keep;
renderState.stencil.front.depthFailOp = MaterialStencilOp::IncrSat;
renderState.stencil.back.func = MaterialComparisonFunc::NotEqual;
renderState.stencil.back.passOp = MaterialStencilOp::DecrWrap;
renderState.stencil.back.failOp = MaterialStencilOp::Invert;
renderState.stencil.back.depthFailOp = MaterialStencilOp::Zero;
material.SetRenderState(renderState);
@@ -136,18 +149,19 @@ TEST(Material, SetGetRenderState) {
EXPECT_FALSE(result.depthWriteEnable);
EXPECT_EQ(result.depthFunc, MaterialComparisonFunc::LessEqual);
EXPECT_EQ(result.colorWriteMask, 0x7);
}
TEST(Material, SetGetLegacyShaderPassHint) {
Material material;
material.SetLegacyShaderPassHint("ForwardLit");
EXPECT_TRUE(material.HasLegacyShaderPassHint());
EXPECT_EQ(material.GetLegacyShaderPassHint(), "ForwardLit");
material.ClearLegacyShaderPassHint();
EXPECT_FALSE(material.HasLegacyShaderPassHint());
EXPECT_TRUE(material.GetLegacyShaderPassHint().Empty());
EXPECT_FLOAT_EQ(result.depthBiasFactor, 1.5f);
EXPECT_EQ(result.depthBiasUnits, 2);
EXPECT_TRUE(result.stencil.enabled);
EXPECT_EQ(result.stencil.reference, 3u);
EXPECT_EQ(result.stencil.readMask, 0x0Fu);
EXPECT_EQ(result.stencil.writeMask, 0xF0u);
EXPECT_EQ(result.stencil.front.func, MaterialComparisonFunc::Equal);
EXPECT_EQ(result.stencil.front.passOp, MaterialStencilOp::Replace);
EXPECT_EQ(result.stencil.front.depthFailOp, MaterialStencilOp::IncrSat);
EXPECT_EQ(result.stencil.back.func, MaterialComparisonFunc::NotEqual);
EXPECT_EQ(result.stencil.back.passOp, MaterialStencilOp::DecrWrap);
EXPECT_EQ(result.stencil.back.failOp, MaterialStencilOp::Invert);
EXPECT_EQ(result.stencil.back.depthFailOp, MaterialStencilOp::Zero);
}
TEST(Material, SetGetTags) {

View File

@@ -3,6 +3,7 @@
# ============================================================
set(MESH_TEST_SOURCES
test_builtin_primitive_mesh.cpp
test_mesh.cpp
test_mesh_loader.cpp
test_mesh_import_settings.cpp

View File

@@ -0,0 +1,107 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <cmath>
using namespace XCEngine::Math;
using namespace XCEngine::Resources;
namespace {
constexpr float kAlignmentEpsilon = 1e-6f;
XCEngine::Core::uint32 GetIndexValue(const Mesh& mesh, XCEngine::Core::uint32 index) {
if (mesh.IsUse32BitIndex()) {
return static_cast<const XCEngine::Core::uint32*>(mesh.GetIndexData())[index];
}
return static_cast<const XCEngine::Core::uint32>(
static_cast<const XCEngine::Core::uint16*>(mesh.GetIndexData())[index]);
}
float ComputeTriangleNormalAlignment(const Mesh& mesh, XCEngine::Core::uint32 triangleStartIndex) {
const auto* vertices = static_cast<const StaticMeshVertex*>(mesh.GetVertexData());
if (vertices == nullptr || triangleStartIndex + 2 >= mesh.GetIndexCount()) {
return 0.0f;
}
const XCEngine::Core::uint32 i0 = GetIndexValue(mesh, triangleStartIndex);
const XCEngine::Core::uint32 i1 = GetIndexValue(mesh, triangleStartIndex + 1);
const XCEngine::Core::uint32 i2 = GetIndexValue(mesh, triangleStartIndex + 2);
if (i0 >= mesh.GetVertexCount() || i1 >= mesh.GetVertexCount() || i2 >= mesh.GetVertexCount()) {
return 0.0f;
}
const Vector3& p0 = vertices[i0].position;
const Vector3& p1 = vertices[i1].position;
const Vector3& p2 = vertices[i2].position;
const Vector3 faceNormal = Vector3::Cross(p1 - p0, p2 - p0);
if (faceNormal.SqrMagnitude() <= kAlignmentEpsilon) {
return 0.0f;
}
const Vector3 averagedVertexNormal =
(vertices[i0].normal + vertices[i1].normal + vertices[i2].normal) / 3.0f;
if (averagedVertexNormal.SqrMagnitude() <= kAlignmentEpsilon) {
return 0.0f;
}
return Vector3::Dot(faceNormal, averagedVertexNormal);
}
float FindReferenceAlignment(const Mesh& mesh) {
for (XCEngine::Core::uint32 index = 0; index + 2 < mesh.GetIndexCount(); index += 3) {
const float alignment = ComputeTriangleNormalAlignment(mesh, index);
if (std::abs(alignment) > kAlignmentEpsilon) {
return alignment;
}
}
return 0.0f;
}
size_t CountTrianglesWithUnexpectedAlignment(const Mesh& mesh, float expectedSign) {
size_t mismatchCount = 0u;
for (XCEngine::Core::uint32 index = 0; index + 2 < mesh.GetIndexCount(); index += 3) {
const float alignment = ComputeTriangleNormalAlignment(mesh, index);
if (std::abs(alignment) <= kAlignmentEpsilon) {
continue;
}
if (alignment * expectedSign <= 0.0f) {
++mismatchCount;
}
}
return mismatchCount;
}
TEST(BuiltinPrimitiveMesh, SphereUsesSameTriangleWindingConventionAsCube) {
LoadResult cubeResult = CreateBuiltinMeshResource(GetBuiltinPrimitiveMeshPath(BuiltinPrimitiveType::Cube));
ASSERT_TRUE(cubeResult);
ASSERT_NE(cubeResult.resource, nullptr);
LoadResult sphereResult = CreateBuiltinMeshResource(GetBuiltinPrimitiveMeshPath(BuiltinPrimitiveType::Sphere));
ASSERT_TRUE(sphereResult);
ASSERT_NE(sphereResult.resource, nullptr);
auto* cubeMesh = static_cast<Mesh*>(cubeResult.resource);
auto* sphereMesh = static_cast<Mesh*>(sphereResult.resource);
ASSERT_NE(cubeMesh, nullptr);
ASSERT_NE(sphereMesh, nullptr);
const float cubeAlignment = FindReferenceAlignment(*cubeMesh);
ASSERT_GT(std::abs(cubeAlignment), kAlignmentEpsilon);
const float expectedSign = cubeAlignment > 0.0f ? 1.0f : -1.0f;
EXPECT_EQ(CountTrianglesWithUnexpectedAlignment(*cubeMesh, expectedSign), 0u);
EXPECT_EQ(CountTrianglesWithUnexpectedAlignment(*sphereMesh, expectedSign), 0u);
delete cubeMesh;
delete sphereMesh;
}
} // namespace

View File

@@ -2777,6 +2777,15 @@ TEST(ShaderLoader, LoadBuiltinObjectIdOutlineShaderBuildsAuthoringVariants) {
std::string(d3d12Vertex->sourceCode.CStr()).find("XC_BUILTIN_OBJECT_ID_OUTLINE_D3D12_VS"),
std::string::npos);
const ShaderStageVariant* d3d12Fragment = shader->FindVariant(
"ObjectIdOutline",
ShaderType::Fragment,
ShaderBackend::D3D12);
ASSERT_NE(d3d12Fragment, nullptr);
const std::string d3d12FragmentSource = d3d12Fragment->sourceCode.CStr();
EXPECT_EQ(d3d12FragmentSource.find("objectIdColor.a <= 0.0"), std::string::npos);
EXPECT_NE(d3d12FragmentSource.find("all(abs(objectIdColor) <= float4("), std::string::npos);
const ShaderStageVariant* vulkanFragment = shader->FindVariant(
"ObjectIdOutline",
ShaderType::Fragment,

View File

@@ -131,6 +131,19 @@ bool DrawDataContainsText(
return false;
}
const XCEngine::UI::UIDrawCommand* FindFirstFilledRectCommand(
const XCEngine::UI::UIDrawData& drawData) {
for (const XCEngine::UI::UIDrawList& drawList : drawData.GetDrawLists()) {
for (const XCEngine::UI::UIDrawCommand& command : drawList.GetCommands()) {
if (command.type == XCEngine::UI::UIDrawCommandType::FilledRect) {
return &command;
}
}
}
return nullptr;
}
const XCEngine::UI::Runtime::UISystemPresentedLayer* FindPresentedLayerById(
const XCEngine::UI::Runtime::UISystemFrameResult& frame,
XCEngine::UI::Runtime::UIScreenLayerId layerId) {
@@ -442,6 +455,97 @@ TEST_F(SceneRuntimeTest, StopClearsUiRuntimeState) {
EXPECT_TRUE(runtime.GetLastUIFrame().layers.empty());
}
TEST_F(SceneRuntimeTest, ClearQueuedUiInputEventsPreventsPendingDelivery) {
Scene* runtimeScene = CreateScene("RuntimeScene");
runtime.Start(runtimeScene);
runtime.SetUIViewportRect(XCEngine::UI::UIRect(0.0f, 0.0f, 800.0f, 480.0f));
runtime.SetUIFocused(true);
TempFileScope menuView("xcui_scene_runtime_clear_input", ".xcui", BuildViewMarkup("Clear Input Menu"));
const auto layerId = runtime.GetUIScreenStackController().PushMenu(
BuildScreenAsset(menuView.Path(), "runtime.clear.input"),
"clear-input");
ASSERT_NE(layerId, 0u);
XCEngine::UI::UIInputEvent textEvent = {};
textEvent.type = XCEngine::UI::UIInputEventType::Character;
textEvent.character = 'A';
runtime.QueueUIInputEvent(textEvent);
XCEngine::UI::UIInputEvent keyEvent = {};
keyEvent.type = XCEngine::UI::UIInputEventType::KeyDown;
keyEvent.keyCode = 13;
runtime.QueueUIInputEvent(keyEvent);
runtime.ClearQueuedUIInputEvents();
runtime.Update(0.016f);
const auto& clearedFrame = runtime.GetLastUIFrame();
ASSERT_EQ(clearedFrame.presentedLayerCount, 1u);
ASSERT_EQ(clearedFrame.layers.size(), 1u);
EXPECT_EQ(clearedFrame.frameIndex, 1u);
EXPECT_EQ(clearedFrame.layers.front().layerId, layerId);
EXPECT_EQ(clearedFrame.layers.front().stats.inputEventCount, 0u);
runtime.QueueUIInputEvent(textEvent);
runtime.Update(0.016f);
const auto& deliveredFrame = runtime.GetLastUIFrame();
ASSERT_EQ(deliveredFrame.presentedLayerCount, 1u);
ASSERT_EQ(deliveredFrame.layers.size(), 1u);
EXPECT_EQ(deliveredFrame.frameIndex, 2u);
EXPECT_EQ(deliveredFrame.layers.front().stats.inputEventCount, 1u);
}
TEST_F(SceneRuntimeTest, ViewportPersistsAcrossFramesAndResetsAfterStop) {
Scene* runtimeScene = CreateScene("RuntimeScene");
runtime.Start(runtimeScene);
runtime.SetUIViewportRect(XCEngine::UI::UIRect(32.0f, 48.0f, 900.0f, 500.0f));
runtime.SetUIFocused(true);
TempFileScope menuView("xcui_scene_runtime_viewport", ".xcui", BuildViewMarkup("Viewport Menu"));
const UIScreenAsset screenAsset = BuildScreenAsset(menuView.Path(), "runtime.viewport.menu");
ASSERT_NE(runtime.GetUIScreenStackController().PushMenu(screenAsset, "viewport-menu"), 0u);
runtime.Update(0.016f);
const auto& firstFrame = runtime.GetLastUIFrame();
const auto* firstBackground = FindFirstFilledRectCommand(firstFrame.drawData);
ASSERT_NE(firstBackground, nullptr);
EXPECT_EQ(firstFrame.frameIndex, 1u);
EXPECT_FLOAT_EQ(firstBackground->rect.x, 32.0f);
EXPECT_FLOAT_EQ(firstBackground->rect.y, 48.0f);
EXPECT_FLOAT_EQ(firstBackground->rect.width, 900.0f);
EXPECT_FLOAT_EQ(firstBackground->rect.height, 500.0f);
runtime.Update(0.016f);
const auto& secondFrame = runtime.GetLastUIFrame();
const auto* secondBackground = FindFirstFilledRectCommand(secondFrame.drawData);
ASSERT_NE(secondBackground, nullptr);
EXPECT_EQ(secondFrame.frameIndex, 2u);
EXPECT_FLOAT_EQ(secondBackground->rect.x, 32.0f);
EXPECT_FLOAT_EQ(secondBackground->rect.y, 48.0f);
EXPECT_FLOAT_EQ(secondBackground->rect.width, 900.0f);
EXPECT_FLOAT_EQ(secondBackground->rect.height, 500.0f);
runtime.Stop();
runtime.Start(runtimeScene);
runtime.SetUIFocused(true);
ASSERT_NE(runtime.GetUIScreenStackController().PushMenu(screenAsset, "viewport-menu-reset"), 0u);
runtime.Update(0.016f);
const auto& restartedFrame = runtime.GetLastUIFrame();
const auto* restartedBackground = FindFirstFilledRectCommand(restartedFrame.drawData);
ASSERT_NE(restartedBackground, nullptr);
EXPECT_EQ(restartedFrame.frameIndex, 1u);
EXPECT_FLOAT_EQ(restartedBackground->rect.x, 0.0f);
EXPECT_FLOAT_EQ(restartedBackground->rect.y, 0.0f);
EXPECT_FLOAT_EQ(restartedBackground->rect.width, 640.0f);
EXPECT_FLOAT_EQ(restartedBackground->rect.height, 360.0f);
}
TEST_F(SceneRuntimeTest, LayeredSceneUiRoutesInputOnlyToTopInteractivePresentedLayer) {
Scene* runtimeScene = CreateScene("RuntimeScene");
runtime.Start(runtimeScene);

View File

@@ -1,6 +1,7 @@
add_subdirectory(shared)
add_subdirectory(input)
add_subdirectory(layout)
add_subdirectory(render)
add_subdirectory(style)
add_subdirectory(text)
@@ -8,6 +9,7 @@ add_custom_target(core_ui_integration_tests
DEPENDS
core_ui_input_integration_tests
core_ui_layout_integration_tests
core_ui_render_integration_tests
core_ui_style_integration_tests
core_ui_text_integration_tests
)

View File

@@ -0,0 +1,6 @@
add_subdirectory(draw_primitives_basic)
add_custom_target(core_ui_render_integration_tests
DEPENDS
core_ui_render_draw_primitives_basic_validation
)

View File

@@ -0,0 +1,27 @@
add_executable(core_ui_render_draw_primitives_basic_validation WIN32
main.cpp
)
target_include_directories(core_ui_render_draw_primitives_basic_validation PRIVATE
${CMAKE_SOURCE_DIR}/tests/UI/Core/integration/shared/src
${CMAKE_SOURCE_DIR}/engine/include
)
target_compile_definitions(core_ui_render_draw_primitives_basic_validation PRIVATE
UNICODE
_UNICODE
)
if(MSVC)
target_compile_options(core_ui_render_draw_primitives_basic_validation PRIVATE /utf-8 /FS)
set_property(TARGET core_ui_render_draw_primitives_basic_validation PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
target_link_libraries(core_ui_render_draw_primitives_basic_validation PRIVATE
core_ui_integration_host
)
set_target_properties(core_ui_render_draw_primitives_basic_validation PROPERTIES
OUTPUT_NAME "XCUICoreRenderDrawPrimitivesBasicValidation"
)

View File

@@ -0,0 +1,465 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "AutoScreenshot.h"
#include "NativeRenderer.h"
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <string>
#ifndef XCENGINE_CORE_UI_TESTS_REPO_ROOT
#define XCENGINE_CORE_UI_TESTS_REPO_ROOT "."
#endif
namespace {
using XCEngine::Tests::CoreUI::Host::AutoScreenshotController;
using XCEngine::Tests::CoreUI::Host::NativeRenderer;
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UILinearGradientDirection;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
constexpr const wchar_t* kWindowClassName = L"XCUICoreRenderDrawPrimitivesBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Core | Draw Primitives Basic";
constexpr UIColor kWindowColor(0.09f, 0.09f, 0.09f, 1.0f);
constexpr UIColor kCardColor(0.15f, 0.15f, 0.15f, 1.0f);
constexpr UIColor kCardBorderColor(0.26f, 0.26f, 0.26f, 1.0f);
constexpr UIColor kTitleColor(0.95f, 0.95f, 0.95f, 1.0f);
constexpr UIColor kBodyColor(0.82f, 0.82f, 0.82f, 1.0f);
constexpr UIColor kMutedColor(0.64f, 0.64f, 0.64f, 1.0f);
constexpr UIColor kCheckerLight(0.74f, 0.74f, 0.74f, 1.0f);
constexpr UIColor kCheckerDark(0.46f, 0.46f, 0.46f, 1.0f);
struct ScenarioLayout {
UIRect introRect = {};
UIRect stateRect = {};
UIRect previewRect = {};
UIRect gradientRowRect = {};
UIRect svRect = {};
UIRect hueStripRect = {};
UIRect closeButtonRect = {};
UIRect alphaPreviewRect = {};
};
std::filesystem::path ResolveRepoRootPath() {
std::string root = XCENGINE_CORE_UI_TESTS_REPO_ROOT;
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
root = root.substr(1u, root.size() - 2u);
}
return std::filesystem::path(root).lexically_normal();
}
ScenarioLayout BuildScenarioLayout(float width, float height) {
constexpr float margin = 20.0f;
constexpr float gap = 16.0f;
constexpr float leftWidth = 430.0f;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 220.0f);
layout.stateRect = UIRect(
margin,
layout.introRect.y + layout.introRect.height + gap,
leftWidth,
(std::max)(240.0f, height - layout.introRect.height - gap - margin * 2.0f));
layout.previewRect = UIRect(
margin * 2.0f + leftWidth,
margin,
(std::max)(460.0f, width - leftWidth - margin * 3.0f),
height - margin * 2.0f);
const float contentX = layout.previewRect.x + 24.0f;
const float contentY = layout.previewRect.y + 58.0f;
layout.gradientRowRect = UIRect(contentX, contentY, 360.0f, 24.0f);
layout.svRect = UIRect(contentX, contentY + 52.0f, 208.0f, 208.0f);
layout.hueStripRect = UIRect(layout.svRect.x + layout.svRect.width + 18.0f, layout.svRect.y, 28.0f, layout.svRect.height);
layout.alphaPreviewRect = UIRect(layout.hueStripRect.x + layout.hueStripRect.width + 26.0f, layout.svRect.y, 96.0f, 48.0f);
layout.closeButtonRect = UIRect(
layout.previewRect.x + layout.previewRect.width - 42.0f,
layout.previewRect.y + 18.0f,
20.0f,
20.0f);
return layout;
}
bool ShouldAutoCaptureOnStartup() {
const char* value = std::getenv("XCUI_AUTO_CAPTURE_ON_STARTUP");
return value != nullptr && value[0] == '1' && value[1] == '\0';
}
void DrawCard(UIDrawList& drawList, const UIRect& rect) {
drawList.AddFilledRect(rect, kCardColor, 8.0f);
drawList.AddRectOutline(rect, kCardBorderColor, 1.0f, 8.0f);
}
void DrawCheckerboard(UIDrawList& drawList, const UIRect& rect, float cellSize) {
const int columns = static_cast<int>(std::ceil(rect.width / cellSize));
const int rows = static_cast<int>(std::ceil(rect.height / cellSize));
for (int row = 0; row < rows; ++row) {
for (int column = 0; column < columns; ++column) {
const bool light = ((row + column) & 1) == 0;
const float x = rect.x + cellSize * static_cast<float>(column);
const float y = rect.y + cellSize * static_cast<float>(row);
drawList.AddFilledRect(
UIRect(
x,
y,
(std::min)(cellSize, rect.x + rect.width - x),
(std::min)(cellSize, rect.y + rect.height - y)),
light ? kCheckerLight : kCheckerDark);
}
}
}
class ScenarioApp {
public:
int Run(HINSTANCE hInstance, int nCmdShow) {
if (!Initialize(hInstance, nCmdShow)) {
Shutdown();
return 1;
}
MSG message = {};
while (message.message != WM_QUIT) {
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
TranslateMessage(&message);
DispatchMessageW(&message);
continue;
}
RenderFrame();
Sleep(8);
}
Shutdown();
return static_cast<int>(message.wParam);
}
private:
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCCREATE) {
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->m_renderer.Resize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr) {
if (wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastStatus = "已请求截图,输出到 captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
if (wParam == 'R') {
app->ResetScenario();
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
WNDCLASSEXW windowClass = {};
windowClass.cbSize = sizeof(windowClass);
windowClass.style = CS_HREDRAW | CS_VREDRAW;
windowClass.lpfnWndProc = &ScenarioApp::WndProc;
windowClass.hInstance = hInstance;
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
windowClass.lpszClassName = kWindowClassName;
m_windowClassAtom = RegisterClassExW(&windowClass);
if (m_windowClassAtom == 0) {
return false;
}
m_hwnd = CreateWindowExW(
0,
kWindowClassName,
kWindowTitle,
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT,
CW_USEDEFAULT,
1440,
920,
nullptr,
nullptr,
hInstance,
this);
if (m_hwnd == nullptr || !m_renderer.Initialize(m_hwnd)) {
return false;
}
ShowWindow(m_hwnd, nCmdShow);
UpdateWindow(m_hwnd);
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Core/integration/render/draw_primitives_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
ResetScenario();
if (ShouldAutoCaptureOnStartup()) {
m_autoScreenshot.RequestCapture("startup");
m_lastStatus = "已请求启动截图";
}
return true;
}
void Shutdown() {
m_autoScreenshot.Shutdown();
m_renderer.Shutdown();
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
DestroyWindow(m_hwnd);
}
if (m_windowClassAtom != 0) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
}
}
void ResetScenario() {
m_lastStatus = "已重置到默认 Draw Primitives 场景";
}
void RenderFrame() {
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
const ScenarioLayout layout = BuildScenarioLayout(width, height);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("CoreDrawPrimitives");
drawList.AddFilledRect(UIRect(0.0f, 0.0f, width, height), kWindowColor);
DrawCard(drawList, layout.introRect);
DrawCard(drawList, layout.stateRect);
DrawCard(drawList, layout.previewRect);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 14.0f),
"这个测试在验证什么功能?",
kTitleColor,
17.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 40.0f),
"只验证 UI Core 新增的 linear gradient / line / circle primitive 的真实渲染。",
kMutedColor,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 74.0f),
"1. 右侧横向和纵向 gradient 不能出现纯色退化或明显断层。",
kBodyColor,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 96.0f),
"2. SV 方块应由两层 gradient 叠加;白到主色、再由透明到黑。",
kBodyColor,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 118.0f),
"3. 手柄必须是圆形;关闭按钮 X 和标尺都必须是 line不准用文字假冒。",
kBodyColor,
12.0f);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 140.0f),
"4. 按 F12 手动截图;设置 XCUI_AUTO_CAPTURE_ON_STARTUP=1 可自动截图;按 R 重置。",
kBodyColor,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 14.0f),
"状态摘要",
kTitleColor,
17.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 44.0f),
"Primitive: FilledRectLinearGradient / Line / FilledCircle / CircleOutline",
kBodyColor,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 68.0f),
"Preview: Gradient Row + SV Square + Hue Strip + Close Button + Alpha Preview",
kBodyColor,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 92.0f),
"Result: " + m_lastStatus,
kBodyColor,
12.0f);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 116.0f),
m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("Capture: F12 -> tests/UI/Core/integration/render/draw_primitives_basic/captures/")
: ("Capture: " + m_autoScreenshot.GetLastCaptureSummary()),
kMutedColor,
12.0f);
if (!m_autoScreenshot.GetLastCaptureError().empty()) {
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 140.0f),
"Error: " + m_autoScreenshot.GetLastCaptureError(),
UIColor(0.95f, 0.56f, 0.56f, 1.0f),
12.0f);
}
drawList.AddText(
UIPoint(layout.previewRect.x + 16.0f, layout.previewRect.y + 14.0f),
"Draw Primitives 预览",
kTitleColor,
17.0f);
drawList.AddText(
UIPoint(layout.previewRect.x + 16.0f, layout.previewRect.y + 36.0f),
"这里不验证 ColorField 业务,只验证底层绘制 primitive 的像素效果。",
kMutedColor,
12.0f);
drawList.AddFilledRectLinearGradient(
layout.gradientRowRect,
UIColor(0.95f, 0.27f, 0.27f, 1.0f),
UIColor(0.14f, 0.72f, 0.94f, 1.0f),
UILinearGradientDirection::Horizontal,
3.0f);
drawList.AddRectOutline(layout.gradientRowRect, UIColor(0.12f, 0.12f, 0.12f, 1.0f), 1.0f, 3.0f);
const float markerX = layout.gradientRowRect.x + layout.gradientRowRect.width * 0.68f;
drawList.AddLine(
UIPoint(markerX, layout.gradientRowRect.y - 2.0f),
UIPoint(markerX, layout.gradientRowRect.y + layout.gradientRowRect.height + 2.0f),
UIColor(1.0f, 1.0f, 1.0f, 1.0f),
2.0f);
drawList.AddFilledRectLinearGradient(
layout.svRect,
UIColor(1.0f, 1.0f, 1.0f, 1.0f),
UIColor(0.23f, 0.64f, 0.98f, 1.0f),
UILinearGradientDirection::Horizontal,
2.0f);
drawList.AddFilledRectLinearGradient(
layout.svRect,
UIColor(0.0f, 0.0f, 0.0f, 0.0f),
UIColor(0.0f, 0.0f, 0.0f, 1.0f),
UILinearGradientDirection::Vertical,
2.0f);
drawList.AddRectOutline(layout.svRect, UIColor(0.12f, 0.12f, 0.12f, 1.0f), 1.0f, 2.0f);
const UIPoint svHandle(
layout.svRect.x + layout.svRect.width * 0.72f,
layout.svRect.y + layout.svRect.height * 0.34f);
drawList.AddFilledCircle(svHandle, 5.0f, UIColor(1.0f, 1.0f, 1.0f, 0.15f));
drawList.AddCircleOutline(svHandle, 7.0f, UIColor(1.0f, 1.0f, 1.0f, 1.0f), 2.0f);
drawList.AddCircleOutline(svHandle, 8.0f, UIColor(0.0f, 0.0f, 0.0f, 0.35f), 1.0f);
const float segmentHeight = layout.hueStripRect.height / 6.0f;
const UIColor hueStops[7] = {
UIColor(1.0f, 0.0f, 0.0f, 1.0f),
UIColor(1.0f, 1.0f, 0.0f, 1.0f),
UIColor(0.0f, 1.0f, 0.0f, 1.0f),
UIColor(0.0f, 1.0f, 1.0f, 1.0f),
UIColor(0.0f, 0.0f, 1.0f, 1.0f),
UIColor(1.0f, 0.0f, 1.0f, 1.0f),
UIColor(1.0f, 0.0f, 0.0f, 1.0f)
};
for (int index = 0; index < 6; ++index) {
drawList.AddFilledRectLinearGradient(
UIRect(
layout.hueStripRect.x,
layout.hueStripRect.y + segmentHeight * static_cast<float>(index),
layout.hueStripRect.width,
segmentHeight + 1.0f),
hueStops[index],
hueStops[index + 1],
UILinearGradientDirection::Vertical,
0.0f);
}
drawList.AddRectOutline(layout.hueStripRect, UIColor(0.12f, 0.12f, 0.12f, 1.0f), 1.0f, 2.0f);
const float hueMarkerY = layout.hueStripRect.y + layout.hueStripRect.height * 0.44f;
drawList.AddLine(
UIPoint(layout.hueStripRect.x - 4.0f, hueMarkerY),
UIPoint(layout.hueStripRect.x + layout.hueStripRect.width + 4.0f, hueMarkerY),
UIColor(1.0f, 1.0f, 1.0f, 1.0f),
2.0f);
DrawCheckerboard(drawList, layout.alphaPreviewRect, 6.0f);
drawList.AddFilledRect(
UIRect(
layout.alphaPreviewRect.x,
layout.alphaPreviewRect.y,
layout.alphaPreviewRect.width,
layout.alphaPreviewRect.height),
UIColor(0.23f, 0.64f, 0.98f, 0.55f),
2.0f);
drawList.AddRectOutline(layout.alphaPreviewRect, UIColor(0.12f, 0.12f, 0.12f, 1.0f), 1.0f, 2.0f);
drawList.AddRectOutline(layout.closeButtonRect, UIColor(0.38f, 0.38f, 0.38f, 1.0f), 1.0f, 2.0f);
drawList.AddLine(
UIPoint(layout.closeButtonRect.x + 5.0f, layout.closeButtonRect.y + 5.0f),
UIPoint(layout.closeButtonRect.x + layout.closeButtonRect.width - 5.0f, layout.closeButtonRect.y + layout.closeButtonRect.height - 5.0f),
UIColor(0.92f, 0.92f, 0.92f, 1.0f),
1.5f);
drawList.AddLine(
UIPoint(layout.closeButtonRect.x + layout.closeButtonRect.width - 5.0f, layout.closeButtonRect.y + 5.0f),
UIPoint(layout.closeButtonRect.x + 5.0f, layout.closeButtonRect.y + layout.closeButtonRect.height - 5.0f),
UIColor(0.92f, 0.92f, 0.92f, 1.0f),
1.5f);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(width),
static_cast<unsigned int>(height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
std::string m_lastStatus = "等待检查";
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -12,12 +12,57 @@ D2D1_RECT_F ToD2DRect(const ::XCEngine::UI::UIRect& rect) {
return D2D1::RectF(rect.x, rect.y, rect.x + rect.width, rect.y + rect.height);
}
D2D1_COLOR_F ToD2DColorValue(const ::XCEngine::UI::UIColor& color) {
return D2D1::ColorF(color.r, color.g, color.b, color.a);
}
std::string HrToString(const char* operation, HRESULT hr) {
char buffer[128] = {};
sprintf_s(buffer, "%s failed with hr=0x%08X.", operation, static_cast<unsigned int>(hr));
return buffer;
}
void FillLinearGradientRect(
ID2D1RenderTarget& renderTarget,
const ::XCEngine::UI::UIDrawCommand& command) {
const D2D1_RECT_F rect = ToD2DRect(command.rect);
const D2D1_GRADIENT_STOP stops[2] = {
D2D1::GradientStop(0.0f, ToD2DColorValue(command.color)),
D2D1::GradientStop(1.0f, ToD2DColorValue(command.secondaryColor))
};
Microsoft::WRL::ComPtr<ID2D1GradientStopCollection> stopCollection;
if (FAILED(renderTarget.CreateGradientStopCollection(
stops,
2u,
stopCollection.ReleaseAndGetAddressOf()))) {
return;
}
const D2D1_POINT_2F startPoint = D2D1::Point2F(rect.left, rect.top);
const D2D1_POINT_2F endPoint =
command.gradientDirection == ::XCEngine::UI::UILinearGradientDirection::Vertical
? D2D1::Point2F(rect.left, rect.bottom)
: D2D1::Point2F(rect.right, rect.top);
Microsoft::WRL::ComPtr<ID2D1LinearGradientBrush> gradientBrush;
if (FAILED(renderTarget.CreateLinearGradientBrush(
D2D1::LinearGradientBrushProperties(startPoint, endPoint),
stopCollection.Get(),
gradientBrush.ReleaseAndGetAddressOf()))) {
return;
}
if (command.rounding > 0.0f) {
renderTarget.FillRoundedRectangle(
D2D1::RoundedRect(rect, command.rounding, command.rounding),
gradientBrush.Get());
return;
}
renderTarget.FillRectangle(rect, gradientBrush.Get());
}
} // namespace
bool NativeRenderer::Initialize(HWND hwnd) {
@@ -345,6 +390,9 @@ void NativeRenderer::RenderCommand(
}
break;
}
case ::XCEngine::UI::UIDrawCommandType::FilledRectLinearGradient:
FillLinearGradientRect(renderTarget, command);
break;
case ::XCEngine::UI::UIDrawCommandType::RectOutline: {
const D2D1_RECT_F rect = ToD2DRect(command.rect);
const float thickness = command.thickness > 0.0f ? command.thickness : 1.0f;
@@ -358,6 +406,34 @@ void NativeRenderer::RenderCommand(
}
break;
}
case ::XCEngine::UI::UIDrawCommandType::Line:
renderTarget.DrawLine(
D2D1::Point2F(command.position.x, command.position.y),
D2D1::Point2F(command.uvMin.x, command.uvMin.y),
&solidBrush,
command.thickness > 0.0f ? command.thickness : 1.0f);
break;
case ::XCEngine::UI::UIDrawCommandType::FilledCircle:
if (command.radius > 0.0f) {
renderTarget.FillEllipse(
D2D1::Ellipse(
D2D1::Point2F(command.position.x, command.position.y),
command.radius,
command.radius),
&solidBrush);
}
break;
case ::XCEngine::UI::UIDrawCommandType::CircleOutline:
if (command.radius > 0.0f) {
renderTarget.DrawEllipse(
D2D1::Ellipse(
D2D1::Point2F(command.position.x, command.position.y),
command.radius,
command.radius),
&solidBrush,
command.thickness > 0.0f ? command.thickness : 1.0f);
}
break;
case ::XCEngine::UI::UIDrawCommandType::Text: {
if (command.text.empty()) {
break;
@@ -452,7 +528,7 @@ IDWriteTextFormat* NativeRenderer::GetTextFormat(float fontSize) {
}
D2D1_COLOR_F NativeRenderer::ToD2DColor(const ::XCEngine::UI::UIColor& color) {
return D2D1::ColorF(color.r, color.g, color.b, color.a);
return ToD2DColorValue(color);
}
std::wstring NativeRenderer::Utf8ToWide(std::string_view text) {

View File

@@ -3,12 +3,14 @@ set(CORE_UI_TEST_SOURCES
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_input_modifier_tracker.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_layout_engine.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_core.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_draw_data.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_expansion_model.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_flat_hierarchy_helpers.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_input_dispatcher.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_keyboard_navigation_model.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_popup_overlay_model.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_property_edit_model.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_scroll_model.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_selection_model.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_style_system.cpp
${CMAKE_SOURCE_DIR}/tests/UI/Core/unit/test_ui_shortcut_scope.cpp

View File

@@ -0,0 +1,82 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/DrawData.h>
namespace {
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UILinearGradientDirection;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
TEST(UIDrawDataTest, DrawListPreservesGradientLineAndCirclePayload) {
UIDrawList drawList("CorePrimitives");
drawList.AddFilledRectLinearGradient(
UIRect(10.0f, 20.0f, 140.0f, 24.0f),
UIColor(1.0f, 0.0f, 0.0f, 1.0f),
UIColor(0.0f, 0.0f, 1.0f, 1.0f),
UILinearGradientDirection::Vertical,
3.0f);
drawList.AddLine(
UIPoint(8.0f, 12.0f),
UIPoint(28.0f, 36.0f),
UIColor(0.9f, 0.9f, 0.9f, 1.0f),
2.0f);
drawList.AddFilledCircle(
UIPoint(48.0f, 52.0f),
6.0f,
UIColor(0.2f, 0.6f, 0.9f, 1.0f));
drawList.AddCircleOutline(
UIPoint(48.0f, 52.0f),
9.0f,
UIColor(1.0f, 1.0f, 1.0f, 1.0f),
1.5f);
ASSERT_EQ(drawList.GetCommandCount(), 4u);
const auto& commands = drawList.GetCommands();
EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRectLinearGradient);
EXPECT_FLOAT_EQ(commands[0].rect.width, 140.0f);
EXPECT_EQ(commands[0].gradientDirection, UILinearGradientDirection::Vertical);
EXPECT_FLOAT_EQ(commands[0].secondaryColor.b, 1.0f);
EXPECT_FLOAT_EQ(commands[0].rounding, 3.0f);
EXPECT_EQ(commands[1].type, UIDrawCommandType::Line);
EXPECT_FLOAT_EQ(commands[1].position.x, 8.0f);
EXPECT_FLOAT_EQ(commands[1].uvMin.x, 28.0f);
EXPECT_FLOAT_EQ(commands[1].thickness, 2.0f);
EXPECT_EQ(commands[2].type, UIDrawCommandType::FilledCircle);
EXPECT_FLOAT_EQ(commands[2].position.x, 48.0f);
EXPECT_FLOAT_EQ(commands[2].radius, 6.0f);
EXPECT_EQ(commands[3].type, UIDrawCommandType::CircleOutline);
EXPECT_FLOAT_EQ(commands[3].position.y, 52.0f);
EXPECT_FLOAT_EQ(commands[3].radius, 9.0f);
EXPECT_FLOAT_EQ(commands[3].thickness, 1.5f);
}
TEST(UIDrawDataTest, DrawDataAggregatesNewPrimitiveCommands) {
UIDrawData drawData = {};
auto& first = drawData.EmplaceDrawList("Gradients");
first.AddFilledRectLinearGradient(
UIRect(0.0f, 0.0f, 50.0f, 12.0f),
UIColor(0.0f, 0.0f, 0.0f, 1.0f),
UIColor(1.0f, 1.0f, 1.0f, 1.0f));
auto& second = drawData.EmplaceDrawList("Handles");
second.AddFilledCircle(UIPoint(12.0f, 12.0f), 4.0f, UIColor(1.0f, 0.0f, 0.0f, 1.0f));
second.AddCircleOutline(UIPoint(12.0f, 12.0f), 6.0f, UIColor(1.0f, 1.0f, 1.0f, 1.0f));
second.AddLine(
UIPoint(0.0f, 24.0f),
UIPoint(32.0f, 24.0f),
UIColor(0.7f, 0.7f, 0.7f, 1.0f));
EXPECT_EQ(drawData.GetDrawListCount(), 2u);
EXPECT_EQ(drawData.GetTotalCommandCount(), 4u);
}
} // namespace

View File

@@ -0,0 +1,53 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Widgets/UIScrollModel.h>
namespace {
using XCEngine::UI::Widgets::ApplyUIScrollWheel;
using XCEngine::UI::Widgets::ClampUIScrollOffset;
using XCEngine::UI::Widgets::ComputeUIScrollOverflow;
using XCEngine::UI::Widgets::EnsureUIScrollOffsetVisible;
TEST(UIScrollModelTest, OverflowAndClampUseViewportBounds) {
EXPECT_FLOAT_EQ(ComputeUIScrollOverflow(80.0f, 120.0f), 0.0f);
EXPECT_FLOAT_EQ(ComputeUIScrollOverflow(240.0f, 120.0f), 120.0f);
EXPECT_FLOAT_EQ(ClampUIScrollOffset(-20.0f, 240.0f, 120.0f), 0.0f);
EXPECT_FLOAT_EQ(ClampUIScrollOffset(40.0f, 240.0f, 120.0f), 40.0f);
EXPECT_FLOAT_EQ(ClampUIScrollOffset(180.0f, 240.0f, 120.0f), 120.0f);
}
TEST(UIScrollModelTest, WheelMutationUsesDefaultStepAndReportsClampedNoops) {
const auto scrollDown = ApplyUIScrollWheel(48.0f, -120.0f, 360.0f, 120.0f);
EXPECT_TRUE(scrollDown.changed);
EXPECT_FLOAT_EQ(scrollDown.overflow, 240.0f);
EXPECT_FLOAT_EQ(scrollDown.offsetBefore, 48.0f);
EXPECT_FLOAT_EQ(scrollDown.offsetAfter, 96.0f);
const auto clamped = ApplyUIScrollWheel(0.0f, 120.0f, 360.0f, 120.0f);
EXPECT_FALSE(clamped.changed);
EXPECT_FLOAT_EQ(clamped.offsetBefore, 0.0f);
EXPECT_FLOAT_EQ(clamped.offsetAfter, 0.0f);
const auto noOverflow = ApplyUIScrollWheel(0.0f, -120.0f, 80.0f, 120.0f);
EXPECT_FALSE(noOverflow.changed);
EXPECT_FLOAT_EQ(noOverflow.overflow, 0.0f);
}
TEST(UIScrollModelTest, EnsureVisibleKeepsRowsInsideViewport) {
EXPECT_FLOAT_EQ(
EnsureUIScrollOffsetVisible(24.0f, 12.0f, 20.0f, 300.0f, 120.0f),
12.0f);
EXPECT_FLOAT_EQ(
EnsureUIScrollOffsetVisible(24.0f, 96.0f, 36.0f, 300.0f, 120.0f),
24.0f);
EXPECT_FLOAT_EQ(
EnsureUIScrollOffsetVisible(24.0f, 164.0f, 28.0f, 300.0f, 120.0f),
72.0f);
EXPECT_FLOAT_EQ(
EnsureUIScrollOffsetVisible(180.0f, 260.0f, 60.0f, 300.0f, 120.0f),
180.0f);
}
} // namespace

View File

@@ -88,6 +88,11 @@ if(TARGET editor_ui_enum_field_basic_validation)
editor_ui_enum_field_basic_validation)
endif()
if(TARGET editor_ui_color_field_basic_validation)
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
editor_ui_color_field_basic_validation)
endif()
if(TARGET editor_ui_list_view_basic_validation)
list(APPEND EDITOR_UI_INTEGRATION_TARGETS
editor_ui_list_view_basic_validation)

View File

@@ -40,6 +40,9 @@ endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/enum_field_basic/CMakeLists.txt")
add_subdirectory(enum_field_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/color_field_basic/CMakeLists.txt")
add_subdirectory(color_field_basic)
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tree_view_basic/CMakeLists.txt")
add_subdirectory(tree_view_basic)
endif()

View File

@@ -0,0 +1,31 @@
add_executable(editor_ui_color_field_basic_validation WIN32
main.cpp
)
target_include_directories(editor_ui_color_field_basic_validation PRIVATE
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
${CMAKE_SOURCE_DIR}/new_editor/include
${CMAKE_SOURCE_DIR}/new_editor/app
${CMAKE_SOURCE_DIR}/engine/include
)
target_compile_definitions(editor_ui_color_field_basic_validation PRIVATE
UNICODE
_UNICODE
XCENGINE_EDITOR_UI_TESTS_REPO_ROOT="${XCENGINE_EDITOR_UI_TESTS_REPO_ROOT_PATH}"
)
if(MSVC)
target_compile_options(editor_ui_color_field_basic_validation PRIVATE /utf-8 /FS)
set_property(TARGET editor_ui_color_field_basic_validation PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
target_link_libraries(editor_ui_color_field_basic_validation PRIVATE
XCUIEditorLib
XCUIEditorHost
)
set_target_properties(editor_ui_color_field_basic_validation PROPERTIES
OUTPUT_NAME "XCUIEditorColorFieldBasicValidation"
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -0,0 +1,778 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <XCEditor/Core/UIEditorColorFieldInteraction.h>
#include <XCEditor/Core/UIEditorTheme.h>
#include <XCEditor/Widgets/UIEditorColorField.h>
#include "EditorValidationTheme.h"
#include "Host/AutoScreenshot.h"
#include "Host/NativeRenderer.h"
#include <XCEngine/UI/DrawData.h>
#include <windows.h>
#include <windowsx.h>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <cstdio>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#ifndef XCENGINE_EDITOR_UI_TESTS_REPO_ROOT
#define XCENGINE_EDITOR_UI_TESTS_REPO_ROOT "."
#endif
namespace {
using XCEngine::UI::UIDrawData;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Host::AutoScreenshotController;
using XCEngine::UI::Editor::Host::NativeRenderer;
using XCEngine::UI::Editor::UIEditorColorFieldInteractionFrame;
using XCEngine::UI::Editor::UIEditorColorFieldInteractionResult;
using XCEngine::UI::Editor::UIEditorColorFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorColorFieldInteraction;
using XCEngine::UI::Editor::Widgets::AppendUIEditorColorField;
using XCEngine::UI::Editor::Widgets::FormatUIEditorColorFieldHexText;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorColorField;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldHitTarget;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldSpec;
namespace Style = XCEngine::UI::Style;
constexpr const wchar_t* kWindowClassName = L"XCUIEditorColorFieldBasicValidation";
constexpr const wchar_t* kWindowTitle = L"XCUI Editor | ColorField Basic";
enum class ActionId : unsigned char {
Reset = 0,
Capture
};
struct ButtonLayout {
ActionId action = ActionId::Reset;
const char* label = "";
UIRect rect = {};
};
struct ScenarioLayout {
UIRect introRect = {};
UIRect controlRect = {};
UIRect stateRect = {};
UIRect previewRect = {};
UIRect fieldRect = {};
std::vector<ButtonLayout> buttons = {};
};
std::filesystem::path ResolveRepoRootPath() {
std::string root = XCENGINE_EDITOR_UI_TESTS_REPO_ROOT;
if (root.size() >= 2u && root.front() == '"' && root.back() == '"') {
root = root.substr(1u, root.size() - 2u);
}
return std::filesystem::path(root).lexically_normal();
}
std::filesystem::path ResolveValidationThemePath() {
return (ResolveRepoRootPath() / "tests/UI/Editor/integration/shared/themes/editor_validation.xctheme")
.lexically_normal();
}
bool IsTruthyEnvironmentFlag(const char* name) {
const char* value = std::getenv(name);
if (value == nullptr || value[0] == '\0') {
return false;
}
std::string normalized = value;
for (char& character : normalized) {
character = static_cast<char>(std::tolower(static_cast<unsigned char>(character)));
}
return normalized != "0" &&
normalized != "false" &&
normalized != "off" &&
normalized != "no";
}
bool ContainsPoint(const UIRect& rect, float x, float y) {
return x >= rect.x &&
x <= rect.x + rect.width &&
y >= rect.y &&
y <= rect.y + rect.height;
}
ScenarioLayout BuildScenarioLayout(
float width,
float height,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics) {
const float margin = shellMetrics.margin;
constexpr float leftWidth = 456.0f;
const float gap = shellMetrics.gap;
ScenarioLayout layout = {};
layout.introRect = UIRect(margin, margin, leftWidth, 248.0f);
layout.controlRect = UIRect(margin, layout.introRect.y + layout.introRect.height + gap, leftWidth, 84.0f);
layout.stateRect = UIRect(
margin,
layout.controlRect.y + layout.controlRect.height + gap,
leftWidth,
(std::max)(250.0f, height - (layout.controlRect.y + layout.controlRect.height + gap) - margin));
layout.previewRect = UIRect(
leftWidth + margin * 2.0f,
margin,
(std::max)(520.0f, width - leftWidth - margin * 3.0f),
height - margin * 2.0f);
layout.fieldRect = UIRect(
layout.previewRect.x + 28.0f,
layout.previewRect.y + 72.0f,
360.0f,
32.0f);
const float buttonWidth = (layout.controlRect.width - 44.0f) * 0.5f;
const float buttonY = layout.controlRect.y + 32.0f;
layout.buttons = {
{ ActionId::Reset, "重置", UIRect(layout.controlRect.x + 14.0f, buttonY, buttonWidth, 36.0f) },
{ ActionId::Capture, "截图(F12)", UIRect(layout.controlRect.x + 26.0f + buttonWidth, buttonY, buttonWidth, 36.0f) }
};
return layout;
}
void DrawCard(
UIDrawList& drawList,
const UIRect& rect,
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
std::string_view title,
std::string_view subtitle = {}) {
drawList.AddFilledRect(rect, shellPalette.cardBackground, shellMetrics.cardRadius);
drawList.AddRectOutline(rect, shellPalette.cardBorder, 1.0f, shellMetrics.cardRadius);
drawList.AddText(
UIPoint(rect.x + 16.0f, rect.y + 14.0f),
std::string(title),
shellPalette.textPrimary,
shellMetrics.titleFontSize);
if (!subtitle.empty()) {
drawList.AddText(
UIPoint(rect.x + 16.0f, rect.y + 40.0f),
std::string(subtitle),
shellPalette.textMuted,
shellMetrics.bodyFontSize);
}
}
void DrawButton(
UIDrawList& drawList,
const ButtonLayout& button,
const XCEngine::Tests::EditorUI::EditorValidationShellPalette& shellPalette,
const XCEngine::Tests::EditorUI::EditorValidationShellMetrics& shellMetrics,
bool hovered) {
drawList.AddFilledRect(
button.rect,
hovered ? shellPalette.buttonHoverBackground : shellPalette.buttonBackground,
shellMetrics.buttonRadius);
drawList.AddRectOutline(button.rect, shellPalette.cardBorder, 1.0f, shellMetrics.buttonRadius);
drawList.AddText(
UIPoint(button.rect.x + 16.0f, button.rect.y + 10.0f),
button.label,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
}
std::string DescribeHitTarget(const UIEditorColorFieldHitTarget& hitTarget) {
switch (hitTarget.kind) {
case UIEditorColorFieldHitTargetKind::Swatch:
return "swatch";
case UIEditorColorFieldHitTargetKind::PopupCloseButton:
return "popup_close";
case UIEditorColorFieldHitTargetKind::HueWheel:
return "popup_hue_wheel";
case UIEditorColorFieldHitTargetKind::SaturationValue:
return "popup_sv_square";
case UIEditorColorFieldHitTargetKind::RedChannel:
return "popup_red_channel";
case UIEditorColorFieldHitTargetKind::GreenChannel:
return "popup_green_channel";
case UIEditorColorFieldHitTargetKind::BlueChannel:
return "popup_blue_channel";
case UIEditorColorFieldHitTargetKind::AlphaChannel:
return "popup_alpha_channel";
case UIEditorColorFieldHitTargetKind::PopupSurface:
return "popup_surface";
case UIEditorColorFieldHitTargetKind::Row:
return "row";
case UIEditorColorFieldHitTargetKind::None:
default:
return "none";
}
}
UIInputEvent MakePointerEvent(
UIInputEventType type,
const UIPoint& position,
UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = position;
event.pointerButton = button;
return event;
}
class ScenarioApp {
public:
int Run(HINSTANCE hInstance, int nCmdShow) {
if (!Initialize(hInstance, nCmdShow)) {
Shutdown();
return 1;
}
MSG message = {};
while (message.message != WM_QUIT) {
if (PeekMessageW(&message, nullptr, 0U, 0U, PM_REMOVE)) {
TranslateMessage(&message);
DispatchMessageW(&message);
continue;
}
RenderFrame();
Sleep(8);
}
Shutdown();
return static_cast<int>(message.wParam);
}
private:
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
if (message == WM_NCCREATE) {
const auto* createStruct = reinterpret_cast<CREATESTRUCTW*>(lParam);
auto* app = reinterpret_cast<ScenarioApp*>(createStruct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
return TRUE;
}
auto* app = reinterpret_cast<ScenarioApp*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (message) {
case WM_SIZE:
if (app != nullptr && wParam != SIZE_MINIMIZED) {
app->OnResize(static_cast<UINT>(LOWORD(lParam)), static_cast<UINT>(HIWORD(lParam)));
}
return 0;
case WM_MOUSEMOVE:
if (app != nullptr) {
app->HandleMouseMove(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_MOUSELEAVE:
if (app != nullptr) {
app->HandleMouseLeave();
return 0;
}
break;
case WM_LBUTTONDOWN:
if (app != nullptr) {
app->HandleLeftButtonDown(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_LBUTTONUP:
if (app != nullptr) {
app->HandleLeftButtonUp(
static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
return 0;
}
break;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
if (app != nullptr && wParam == VK_F12) {
app->m_autoScreenshot.RequestCapture("manual_f12");
app->m_lastResult = "已请求截图,输出到 captures/latest.png";
InvalidateRect(hwnd, nullptr, FALSE);
return 0;
}
break;
case WM_PAINT:
if (app != nullptr) {
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
app->RenderFrame();
EndPaint(hwnd, &paintStruct);
return 0;
}
break;
case WM_ERASEBKGND:
return 1;
case WM_DESTROY:
if (app != nullptr) {
app->m_hwnd = nullptr;
}
PostQuitMessage(0);
return 0;
default:
break;
}
return DefWindowProcW(hwnd, message, wParam, lParam);
}
bool Initialize(HINSTANCE hInstance, int nCmdShow) {
WNDCLASSEXW windowClass = {};
windowClass.cbSize = sizeof(windowClass);
windowClass.style = CS_HREDRAW | CS_VREDRAW;
windowClass.lpfnWndProc = &ScenarioApp::WndProc;
windowClass.hInstance = hInstance;
windowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
windowClass.lpszClassName = kWindowClassName;
m_windowClassAtom = RegisterClassExW(&windowClass);
if (m_windowClassAtom == 0) {
return false;
}
m_hwnd = CreateWindowExW(
0,
kWindowClassName,
kWindowTitle,
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT,
CW_USEDEFAULT,
1540,
940,
nullptr,
nullptr,
hInstance,
this);
if (m_hwnd == nullptr) {
return false;
}
ShowWindow(m_hwnd, nCmdShow);
UpdateWindow(m_hwnd);
if (!m_renderer.Initialize(m_hwnd)) {
return false;
}
m_captureRoot =
ResolveRepoRootPath() / "tests/UI/Editor/integration/shell/color_field_basic/captures";
m_autoScreenshot.Initialize(m_captureRoot);
const auto themeLoad =
XCEngine::Tests::EditorUI::LoadEditorValidationTheme(ResolveValidationThemePath());
if (themeLoad.succeeded) {
m_theme = themeLoad.theme;
m_themeStatus = "loaded";
} else {
m_themeStatus = themeLoad.error.empty() ? "fallback" : themeLoad.error;
}
ResetScenario();
if (IsTruthyEnvironmentFlag("XCUI_COLOR_FIELD_OPEN_POPUP_ON_STARTUP")) {
const UIPoint swatchPoint(
m_frame.layout.swatchRect.x + 4.0f,
m_frame.layout.swatchRect.y + 4.0f);
PumpEvents({
MakePointerEvent(
UIInputEventType::PointerButtonDown,
swatchPoint,
UIPointerButton::Left),
MakePointerEvent(
UIInputEventType::PointerButtonUp,
swatchPoint,
UIPointerButton::Left)
});
m_lastResult = "已自动打开 ColorField 弹窗";
}
return true;
}
void Shutdown() {
m_autoScreenshot.Shutdown();
m_renderer.Shutdown();
if (m_hwnd != nullptr && IsWindow(m_hwnd)) {
DestroyWindow(m_hwnd);
}
m_hwnd = nullptr;
if (m_windowClassAtom != 0) {
UnregisterClassW(kWindowClassName, GetModuleHandleW(nullptr));
m_windowClassAtom = 0;
}
}
ScenarioLayout GetLayout() const {
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
const float width = static_cast<float>((std::max)(1L, clientRect.right - clientRect.left));
const float height = static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top));
return BuildScenarioLayout(
width,
height,
XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme));
}
UIRect GetViewportRect() const {
RECT clientRect = {};
GetClientRect(m_hwnd, &clientRect);
return UIRect(
0.0f,
0.0f,
static_cast<float>((std::max)(1L, clientRect.right - clientRect.left)),
static_cast<float>((std::max)(1L, clientRect.bottom - clientRect.top)));
}
void ResetScenario() {
m_spec = {};
m_spec.fieldId = "tint";
m_spec.label = "Tint";
m_spec.value = XCEngine::UI::UIColor(0.84f, 0.42f, 0.28f, 0.65f);
m_spec.showAlpha = true;
m_interactionState = {};
m_interactionState.colorFieldState.focused = true;
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hoveredAction = ActionId::Reset;
m_hasHoveredAction = false;
m_lastResult = "已重置到默认 ColorField 状态";
RefreshFrame();
}
void RefreshFrame() {
if (m_hwnd == nullptr) {
return;
}
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics(m_theme);
m_frame = UpdateUIEditorColorFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
{},
metrics,
GetViewportRect());
}
void OnResize(UINT width, UINT height) {
if (width == 0u || height == 0u) {
return;
}
m_renderer.Resize(width, height);
RefreshFrame();
}
void HandleMouseMove(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
UpdateHoveredAction(layout, x, y);
TRACKMOUSEEVENT trackEvent = {};
trackEvent.cbSize = sizeof(trackEvent);
trackEvent.dwFlags = TME_LEAVE;
trackEvent.hwndTrack = m_hwnd;
TrackMouseEvent(&trackEvent);
UpdateResultText(PumpEvents({ MakePointerEvent(UIInputEventType::PointerMove, m_mousePosition) }));
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleMouseLeave() {
m_mousePosition = UIPoint(-1000.0f, -1000.0f);
m_hasHoveredAction = false;
PumpEvents({ MakePointerEvent(UIInputEventType::PointerLeave, m_mousePosition) });
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonDown(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
if (HitTestAction(layout, x, y) != nullptr) {
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
UpdateResultText(
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonDown, m_mousePosition, UIPointerButton::Left) }));
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void HandleLeftButtonUp(float x, float y) {
m_mousePosition = UIPoint(x, y);
const ScenarioLayout layout = GetLayout();
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button != nullptr) {
ExecuteAction(button->action);
UpdateHoveredAction(layout, x, y);
InvalidateRect(m_hwnd, nullptr, FALSE);
return;
}
UpdateResultText(
PumpEvents({ MakePointerEvent(UIInputEventType::PointerButtonUp, m_mousePosition, UIPointerButton::Left) }));
InvalidateRect(m_hwnd, nullptr, FALSE);
}
void UpdateHoveredAction(const ScenarioLayout& layout, float x, float y) {
const ButtonLayout* button = HitTestAction(layout, x, y);
if (button == nullptr) {
m_hasHoveredAction = false;
return;
}
m_hoveredAction = button->action;
m_hasHoveredAction = true;
}
const ButtonLayout* HitTestAction(const ScenarioLayout& layout, float x, float y) const {
for (const ButtonLayout& button : layout.buttons) {
if (ContainsPoint(button.rect, x, y)) {
return &button;
}
}
return nullptr;
}
UIEditorColorFieldInteractionResult PumpEvents(std::vector<UIInputEvent> events) {
const ScenarioLayout layout = GetLayout();
const auto metrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics(m_theme);
m_frame = UpdateUIEditorColorFieldInteraction(
m_interactionState,
m_spec,
layout.fieldRect,
std::move(events),
metrics,
GetViewportRect());
return m_frame.result;
}
void UpdateResultText(const UIEditorColorFieldInteractionResult& result) {
if (result.colorChanged) {
m_lastResult = "颜色值已更新";
return;
}
if (result.popupOpened) {
m_lastResult = "已打开拾色弹窗";
return;
}
if (result.popupClosed) {
m_lastResult = "已关闭拾色弹窗";
return;
}
if (result.consumed) {
m_lastResult = "控件已消费输入";
return;
}
m_lastResult = "等待交互";
}
void ExecuteAction(ActionId action) {
switch (action) {
case ActionId::Reset:
ResetScenario();
break;
case ActionId::Capture:
m_autoScreenshot.RequestCapture("manual_button");
m_lastResult = "已请求截图,输出到 captures/latest.png";
break;
}
}
std::string BuildColorSummary() const {
char buffer[128] = {};
std::snprintf(
buffer,
sizeof(buffer),
"R %.3f G %.3f B %.3f A %.3f",
m_spec.value.r,
m_spec.value.g,
m_spec.value.b,
m_spec.value.a);
return std::string(buffer);
}
void RenderFrame() {
if (m_hwnd == nullptr) {
return;
}
const UIRect viewportRect = GetViewportRect();
const auto shellMetrics = XCEngine::Tests::EditorUI::ResolveEditorValidationShellMetrics(m_theme);
const auto shellPalette = XCEngine::Tests::EditorUI::ResolveEditorValidationShellPalette(m_theme);
const ScenarioLayout layout = BuildScenarioLayout(viewportRect.width, viewportRect.height, shellMetrics);
RefreshFrame();
const UIEditorColorFieldHitTarget currentHit =
HitTestUIEditorColorField(
m_frame.layout,
m_interactionState.colorFieldState.popupOpen,
m_mousePosition);
const auto fieldMetrics = XCEngine::UI::Editor::ResolveUIEditorColorFieldMetrics(m_theme);
const auto fieldPalette = XCEngine::UI::Editor::ResolveUIEditorColorFieldPalette(m_theme);
UIDrawData drawData = {};
UIDrawList& drawList = drawData.EmplaceDrawList("EditorColorFieldBasic");
drawList.AddFilledRect(viewportRect, shellPalette.windowBackground);
DrawCard(
drawList,
layout.introRect,
shellPalette,
shellMetrics,
"这个测试在验证什么功能",
"只验证 Editor ColorField 的独立样式、弹窗结构和拾色交互,不混入 PropertyGrid。");
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 72.0f),
"1. 点击 swatch检查 popup 是否在控件下方稳定展开。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 94.0f),
"2. 在 SV square 内拖拽,检查颜色是否连续变化。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 116.0f),
"3. 拖拽 hue wheel 与 R/G/B/A slider检查颜色和透明度是否同步更新。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 138.0f),
"4. 点击右上角 close 或点击控件外部,检查 popup 是否关闭。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.introRect.x + 16.0f, layout.introRect.y + 160.0f),
"5. 检查 Hexadecimal、数值框、handle、checkerboard 与截图路径是否同步。",
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
DrawCard(drawList, layout.controlRect, shellPalette, shellMetrics, "操作");
for (const ButtonLayout& button : layout.buttons) {
DrawButton(
drawList,
button,
shellPalette,
shellMetrics,
m_hasHoveredAction && m_hoveredAction == button.action);
}
DrawCard(
drawList,
layout.stateRect,
shellPalette,
shellMetrics,
"状态摘要",
"重点检查 hover / popup / result / hex / rgba。");
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 70.0f),
"Hover: " + DescribeHitTarget(currentHit),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 94.0f),
std::string("Popup: ") + (m_interactionState.colorFieldState.popupOpen ? "open" : "closed"),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 118.0f),
"Hex: " + FormatUIEditorColorFieldHexText(m_spec),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 142.0f),
BuildColorSummary(),
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 166.0f),
"Result: " + m_lastResult,
shellPalette.textPrimary,
shellMetrics.bodyFontSize);
const std::string captureSummary =
m_autoScreenshot.HasPendingCapture()
? "截图排队中..."
: (m_autoScreenshot.GetLastCaptureSummary().empty()
? std::string("F12 -> tests/UI/Editor/integration/shell/color_field_basic/captures/")
: m_autoScreenshot.GetLastCaptureSummary());
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 190.0f),
captureSummary,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
drawList.AddText(
UIPoint(layout.stateRect.x + 16.0f, layout.stateRect.y + 214.0f),
"Theme: " + m_themeStatus,
shellPalette.textWeak,
shellMetrics.bodyFontSize);
DrawCard(
drawList,
layout.previewRect,
shellPalette,
shellMetrics,
"ColorField 预览",
"这里只放一个独立 ColorField便于单点检查样式与交互。");
AppendUIEditorColorField(
drawList,
layout.fieldRect,
m_spec,
m_interactionState.colorFieldState,
fieldPalette,
fieldMetrics,
viewportRect);
const bool framePresented = m_renderer.Render(drawData);
m_autoScreenshot.CaptureIfRequested(
m_renderer,
drawData,
static_cast<unsigned int>(viewportRect.width),
static_cast<unsigned int>(viewportRect.height),
framePresented);
}
HWND m_hwnd = nullptr;
ATOM m_windowClassAtom = 0;
NativeRenderer m_renderer = {};
AutoScreenshotController m_autoScreenshot = {};
std::filesystem::path m_captureRoot = {};
UIEditorColorFieldSpec m_spec = {};
UIEditorColorFieldInteractionState m_interactionState = {};
UIEditorColorFieldInteractionFrame m_frame = {};
Style::UITheme m_theme = {};
UIPoint m_mousePosition = UIPoint(-1000.0f, -1000.0f);
ActionId m_hoveredAction = ActionId::Reset;
bool m_hasHoveredAction = false;
std::string m_lastResult = {};
std::string m_themeStatus = "fallback";
};
} // namespace
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int nCmdShow) {
return ScenarioApp().Run(hInstance, nCmdShow);
}

View File

@@ -21,6 +21,8 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_theme.cpp
test_ui_editor_bool_field.cpp
test_ui_editor_bool_field_interaction.cpp
test_ui_editor_color_field.cpp
test_ui_editor_color_field_interaction.cpp
test_ui_editor_dock_host.cpp
test_ui_editor_list_view.cpp
test_ui_editor_list_view_interaction.cpp

View File

@@ -0,0 +1,140 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/DrawData.h>
#include <XCEditor/Widgets/UIEditorColorField.h>
namespace {
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::Widgets::AppendUIEditorColorField;
using XCEngine::UI::Editor::Widgets::BuildUIEditorColorFieldLayout;
using XCEngine::UI::Editor::Widgets::FormatUIEditorColorFieldHexText;
using XCEngine::UI::Editor::Widgets::FormatUIEditorColorFieldRgbaText;
using XCEngine::UI::Editor::Widgets::HitTestUIEditorColorField;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldHitTargetKind;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldSpec;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldState;
TEST(UIEditorColorFieldTest, FormatsHexTextWithAndWithoutAlpha) {
UIEditorColorFieldSpec spec = {};
spec.value = XCEngine::UI::UIColor(1.0f, 0.5f, 0.25f, 0.75f);
spec.showAlpha = true;
EXPECT_EQ(FormatUIEditorColorFieldHexText(spec), "#FF8040BF");
spec.showAlpha = false;
EXPECT_EQ(FormatUIEditorColorFieldHexText(spec), "#FF8040");
}
TEST(UIEditorColorFieldTest, FormatsRgbaReadoutForInspectorSummary) {
UIEditorColorFieldSpec spec = {};
spec.value = XCEngine::UI::UIColor(0.25f, 0.5f, 0.75f, 1.0f);
EXPECT_EQ(FormatUIEditorColorFieldRgbaText(spec), "RGBA 64, 128, 191, 255");
}
TEST(UIEditorColorFieldTest, LayoutKeepsInspectorColumnAndCompactSwatch) {
UIEditorColorFieldSpec spec = {};
spec.fieldId = "albedo";
spec.label = "Albedo";
const auto layout = BuildUIEditorColorFieldLayout(
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
spec);
EXPECT_FLOAT_EQ(layout.swatchRect.x, 236.0f);
EXPECT_FLOAT_EQ(layout.swatchRect.width, 54.0f);
EXPECT_FLOAT_EQ(layout.swatchRect.height, 18.0f);
EXPECT_EQ(
HitTestUIEditorColorField(
layout,
false,
UIPoint(layout.swatchRect.x + 2.0f, layout.swatchRect.y + 2.0f)).kind,
UIEditorColorFieldHitTargetKind::Swatch);
}
TEST(UIEditorColorFieldTest, PopupLayoutExposesHueWheelAndChannelTargets) {
UIEditorColorFieldSpec spec = {};
spec.showAlpha = true;
const auto layout = BuildUIEditorColorFieldLayout(
UIRect(10.0f, 20.0f, 360.0f, 22.0f),
spec,
{},
UIRect(0.0f, 0.0f, 800.0f, 600.0f));
EXPECT_GT(layout.saturationValueRect.width, 0.0f);
EXPECT_GT(layout.hueWheelOuterRadius, layout.hueWheelInnerRadius);
EXPECT_GT(layout.redSliderRect.width, 0.0f);
EXPECT_GT(layout.alphaSliderRect.width, 0.0f);
EXPECT_EQ(
HitTestUIEditorColorField(
layout,
true,
UIPoint(
layout.hueWheelCenter.x + (layout.hueWheelInnerRadius + layout.hueWheelOuterRadius) * 0.5f,
layout.hueWheelCenter.y)).kind,
UIEditorColorFieldHitTargetKind::HueWheel);
EXPECT_EQ(
HitTestUIEditorColorField(
layout,
true,
UIPoint(layout.saturationValueRect.x + 5.0f, layout.saturationValueRect.y + 5.0f)).kind,
UIEditorColorFieldHitTargetKind::SaturationValue);
EXPECT_EQ(
HitTestUIEditorColorField(
layout,
true,
UIPoint(layout.redSliderRect.x + 2.0f, layout.redSliderRect.y + 2.0f)).kind,
UIEditorColorFieldHitTargetKind::RedChannel);
EXPECT_EQ(
HitTestUIEditorColorField(
layout,
true,
UIPoint(layout.alphaSliderRect.x + 2.0f, layout.alphaSliderRect.y + 2.0f)).kind,
UIEditorColorFieldHitTargetKind::AlphaChannel);
}
TEST(UIEditorColorFieldTest, PopupDrawEmitsHeaderWheelHandlesAndHexadecimalLabel) {
UIEditorColorFieldSpec spec = {};
spec.label = "Tint";
spec.value = XCEngine::UI::UIColor(0.8f, 0.4f, 0.2f, 0.5f);
spec.showAlpha = true;
UIEditorColorFieldState state = {};
state.popupOpen = true;
XCEngine::UI::UIDrawData drawData = {};
auto& drawList = drawData.EmplaceDrawList("ColorField");
AppendUIEditorColorField(
drawList,
UIRect(0.0f, 0.0f, 360.0f, 22.0f),
spec,
state,
{},
{},
UIRect(0.0f, 0.0f, 800.0f, 600.0f));
bool hasFilledCircle = false;
bool hasCircleOutline = false;
bool hasLine = false;
bool hasTitleText = false;
bool hasHexLabel = false;
for (const auto& command : drawList.GetCommands()) {
hasFilledCircle = hasFilledCircle || command.type == UIDrawCommandType::FilledCircle;
hasCircleOutline = hasCircleOutline || command.type == UIDrawCommandType::CircleOutline;
hasLine = hasLine || command.type == UIDrawCommandType::Line;
hasTitleText = hasTitleText || command.text == "Color";
hasHexLabel = hasHexLabel || command.text == "Hexadecimal";
}
EXPECT_TRUE(hasFilledCircle);
EXPECT_TRUE(hasCircleOutline);
EXPECT_TRUE(hasLine);
EXPECT_TRUE(hasTitleText);
EXPECT_TRUE(hasHexLabel);
}
} // namespace

View File

@@ -0,0 +1,138 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorColorFieldInteraction.h>
#include <XCEngine/Input/InputTypes.h>
namespace {
using XCEngine::Input::KeyCode;
using XCEngine::UI::UIInputEvent;
using XCEngine::UI::UIInputEventType;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UIPointerButton;
using XCEngine::UI::UIRect;
using XCEngine::UI::Editor::UIEditorColorFieldInteractionState;
using XCEngine::UI::Editor::UpdateUIEditorColorFieldInteraction;
using XCEngine::UI::Editor::Widgets::UIEditorColorFieldSpec;
UIInputEvent MakePointer(UIInputEventType type, float x, float y, UIPointerButton button = UIPointerButton::None) {
UIInputEvent event = {};
event.type = type;
event.position = UIPoint(x, y);
event.pointerButton = button;
return event;
}
UIInputEvent MakeKey(KeyCode keyCode) {
UIInputEvent event = {};
event.type = UIInputEventType::KeyDown;
event.keyCode = static_cast<std::int32_t>(keyCode);
return event;
}
TEST(UIEditorColorFieldInteractionTest, ClickSwatchOpensPopupAndEscapeClosesIt) {
UIEditorColorFieldSpec spec = {};
spec.fieldId = "tint";
spec.label = "Tint";
spec.showAlpha = true;
spec.value = XCEngine::UI::UIColor(0.8f, 0.4f, 0.2f, 0.5f);
UIEditorColorFieldInteractionState state = {};
auto frame = UpdateUIEditorColorFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{});
frame = UpdateUIEditorColorFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{
MakePointer(
UIInputEventType::PointerButtonDown,
frame.layout.swatchRect.x + 2.0f,
frame.layout.swatchRect.y + 2.0f,
UIPointerButton::Left),
MakePointer(
UIInputEventType::PointerButtonUp,
frame.layout.swatchRect.x + 2.0f,
frame.layout.swatchRect.y + 2.0f,
UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.popupOpened);
EXPECT_TRUE(state.colorFieldState.popupOpen);
frame = UpdateUIEditorColorFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{ MakeKey(KeyCode::Escape) });
EXPECT_TRUE(frame.result.popupClosed);
EXPECT_FALSE(state.colorFieldState.popupOpen);
}
TEST(UIEditorColorFieldInteractionTest, DraggingHueWheelAndAlphaChannelUpdatesColor) {
UIEditorColorFieldSpec spec = {};
spec.fieldId = "tint";
spec.label = "Tint";
spec.showAlpha = true;
spec.value = XCEngine::UI::UIColor(1.0f, 0.0f, 0.0f, 1.0f);
UIEditorColorFieldInteractionState state = {};
auto frame = UpdateUIEditorColorFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{});
frame = UpdateUIEditorColorFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{
MakePointer(
UIInputEventType::PointerButtonDown,
frame.layout.swatchRect.x + 2.0f,
frame.layout.swatchRect.y + 2.0f,
UIPointerButton::Left),
MakePointer(
UIInputEventType::PointerButtonUp,
frame.layout.swatchRect.x + 2.0f,
frame.layout.swatchRect.y + 2.0f,
UIPointerButton::Left)
});
ASSERT_TRUE(state.colorFieldState.popupOpen);
const float hueRadius = (frame.layout.hueWheelInnerRadius + frame.layout.hueWheelOuterRadius) * 0.5f;
const float hueX = frame.layout.hueWheelCenter.x - hueRadius;
const float hueY = frame.layout.hueWheelCenter.y;
frame = UpdateUIEditorColorFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{
MakePointer(UIInputEventType::PointerButtonDown, hueX, hueY, UIPointerButton::Left),
MakePointer(UIInputEventType::PointerMove, hueX, hueY),
MakePointer(UIInputEventType::PointerButtonUp, hueX, hueY, UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.colorChanged);
EXPECT_GT(spec.value.b, 0.0f);
const float alphaX = frame.layout.alphaSliderRect.x + frame.layout.alphaSliderRect.width * 0.25f;
const float alphaY = frame.layout.alphaSliderRect.y + frame.layout.alphaSliderRect.height * 0.5f;
frame = UpdateUIEditorColorFieldInteraction(
state,
spec,
UIRect(0.0f, 0.0f, 360.0f, 32.0f),
{
MakePointer(UIInputEventType::PointerButtonDown, alphaX, alphaY, UIPointerButton::Left),
MakePointer(UIInputEventType::PointerMove, alphaX, alphaY),
MakePointer(UIInputEventType::PointerButtonUp, alphaX, alphaY, UIPointerButton::Left)
});
EXPECT_TRUE(frame.result.colorChanged);
EXPECT_LT(spec.value.a, 0.5f);
}
} // namespace

View File

@@ -37,6 +37,28 @@ Style::UITheme BuildEditorFieldTheme() {
definition.SetToken("editor.size.field.dropdown_arrow_width", Style::UIStyleValue(14.0f));
definition.SetToken("editor.space.field.dropdown_arrow_inset_x", Style::UIStyleValue(4.0f));
definition.SetToken("editor.space.field.dropdown_arrow_inset_y", Style::UIStyleValue(4.0f));
definition.SetToken("editor.size.field.color_popup_width", Style::UIStyleValue(320.0f));
definition.SetToken("editor.size.field.color_popup_top_row", Style::UIStyleValue(36.0f));
definition.SetToken("editor.size.field.color_preview_width", Style::UIStyleValue(108.0f));
definition.SetToken("editor.size.field.color_preview_height", Style::UIStyleValue(26.0f));
definition.SetToken("editor.size.field.color_wheel_outer_radius", Style::UIStyleValue(112.0f));
definition.SetToken("editor.size.field.color_wheel_ring_thickness", Style::UIStyleValue(22.0f));
definition.SetToken("editor.size.field.color_sv_square", Style::UIStyleValue(118.0f));
definition.SetToken("editor.size.field.color_wheel_region_height", Style::UIStyleValue(224.0f));
definition.SetToken("editor.size.field.color_channel_row_height", Style::UIStyleValue(21.0f));
definition.SetToken("editor.size.field.color_numeric_box_width", Style::UIStyleValue(66.0f));
definition.SetToken("editor.size.field.color_channel_label_width", Style::UIStyleValue(14.0f));
definition.SetToken("editor.size.field.color_hex_label_width", Style::UIStyleValue(90.0f));
definition.SetToken("editor.space.field.color_control_row_spacing", Style::UIStyleValue(7.0f));
definition.SetToken("editor.space.field.color_popup_field_inset", Style::UIStyleValue(5.0f));
definition.SetToken("editor.color.field.color_popup_surface", Style::UIStyleValue(Math::Color(0.22f, 0.22f, 0.22f, 1.0f)));
definition.SetToken("editor.color.field.color_popup_header", Style::UIStyleValue(Math::Color(0.48f, 0.28f, 0.10f, 1.0f)));
definition.SetToken("editor.color.field.color_popup_close", Style::UIStyleValue(Math::Color(0.74f, 0.34f, 0.32f, 1.0f)));
definition.SetToken("editor.color.field.color_popup_close_hover", Style::UIStyleValue(Math::Color(0.80f, 0.38f, 0.36f, 1.0f)));
definition.SetToken("editor.color.field.color_popup_slider_border", Style::UIStyleValue(Math::Color(0.11f, 0.11f, 0.11f, 1.0f)));
definition.SetToken("editor.color.field.color_popup_numeric_box", Style::UIStyleValue(Math::Color(0.17f, 0.17f, 0.17f, 1.0f)));
definition.SetToken("editor.color.field.color_popup_numeric_box_text", Style::UIStyleValue(Math::Color(0.93f, 0.93f, 0.93f, 1.0f)));
definition.SetToken("editor.color.field.color_popup_handle_outline", Style::UIStyleValue(Math::Color(0.10f, 0.10f, 0.10f, 0.5f)));
definition.SetToken("editor.radius.field.row", Style::UIStyleValue(2.0f));
definition.SetToken("editor.radius.field.control", Style::UIStyleValue(2.0f));
definition.SetToken("editor.border.field", Style::UIStyleValue(1.0f));
@@ -244,6 +266,25 @@ TEST(UIEditorThemeTest, FieldResolversReadEditorThemeTokens) {
EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowFontSize, 10.0f);
EXPECT_FLOAT_EQ(enumPalette.arrowColor.r, 0.88f);
const auto colorMetrics = Editor::ResolveUIEditorColorFieldMetrics(theme);
const auto colorPalette = Editor::ResolveUIEditorColorFieldPalette(theme);
EXPECT_FLOAT_EQ(colorMetrics.popupWidth, 320.0f);
EXPECT_FLOAT_EQ(colorMetrics.popupTopRowHeight, 36.0f);
EXPECT_FLOAT_EQ(colorMetrics.popupPreviewWidth, 108.0f);
EXPECT_FLOAT_EQ(colorMetrics.wheelOuterRadius, 112.0f);
EXPECT_FLOAT_EQ(colorMetrics.wheelRingThickness, 22.0f);
EXPECT_FLOAT_EQ(colorMetrics.saturationValueSize, 118.0f);
EXPECT_FLOAT_EQ(colorMetrics.channelRowHeight, 21.0f);
EXPECT_FLOAT_EQ(colorMetrics.numericBoxWidth, 66.0f);
EXPECT_FLOAT_EQ(colorMetrics.hexLabelWidth, 90.0f);
EXPECT_FLOAT_EQ(colorPalette.popupColor.r, 0.22f);
EXPECT_FLOAT_EQ(colorPalette.popupHeaderColor.r, 0.48f);
EXPECT_FLOAT_EQ(colorPalette.closeButtonColor.r, 0.74f);
EXPECT_FLOAT_EQ(colorPalette.closeButtonHoverColor.r, 0.80f);
EXPECT_FLOAT_EQ(colorPalette.sliderBorderColor.r, 0.11f);
EXPECT_FLOAT_EQ(colorPalette.numericBoxTextColor.r, 0.93f);
EXPECT_FLOAT_EQ(colorPalette.handleStrokeColor.r, 0.10f);
const auto popupMetrics = Editor::ResolveUIEditorMenuPopupMetrics(theme);
const auto popupPalette = Editor::ResolveUIEditorMenuPopupPalette(theme);
EXPECT_FLOAT_EQ(popupMetrics.contentPaddingX, 6.0f);
@@ -386,6 +427,18 @@ TEST(UIEditorThemeTest, HostedFieldBuildersInheritPropertyGridMetricsAndPalette)
EXPECT_FLOAT_EQ(enumMetrics.valueTextInsetX, 5.0f);
EXPECT_FLOAT_EQ(enumMetrics.dropdownArrowFontSize, 12.0f);
EXPECT_FLOAT_EQ(enumPalette.arrowColor.r, 0.9f);
const auto colorMetrics = Editor::BuildUIEditorHostedColorFieldMetrics(propertyMetrics);
const auto colorPalette = Editor::BuildUIEditorHostedColorFieldPalette(propertyPalette);
EXPECT_FLOAT_EQ(colorMetrics.controlTrailingInset, 5.0f);
EXPECT_FLOAT_EQ(colorMetrics.swatchInsetY, 2.0f);
EXPECT_FLOAT_EQ(colorMetrics.labelFontSize, 10.0f);
EXPECT_FLOAT_EQ(colorMetrics.valueFontSize, 12.0f);
EXPECT_FLOAT_EQ(colorMetrics.popupHeaderHeight, 30.0f);
EXPECT_FLOAT_EQ(colorPalette.labelColor.r, 0.8f);
EXPECT_FLOAT_EQ(colorPalette.popupBorderColor.r, 0.15f);
EXPECT_FLOAT_EQ(colorPalette.popupHeaderColor.r, 0.43f);
EXPECT_FLOAT_EQ(colorPalette.swatchBorderColor.r, 0.5f);
}
} // namespace

View File

@@ -490,6 +490,44 @@ TEST(AssetImportService_Test, ClearLibraryAndReimportAllAssetsManageArtifactsExp
fs::remove_all(projectRoot);
}
TEST(AssetImportService_Test, OnlyShaderAuthoringFilesAreImportableShaderSources) {
namespace fs = std::filesystem;
AssetImportService importService;
importService.Initialize();
const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_shader_source_scope";
const fs::path assetsDir = projectRoot / "Assets";
const fs::path shaderPath = assetsDir / "runtime.shader";
const fs::path includePath = assetsDir / "shared.hlsl";
fs::remove_all(projectRoot);
fs::create_directories(assetsDir);
{
std::ofstream shaderFile(shaderPath);
ASSERT_TRUE(shaderFile.is_open());
shaderFile << "Shader \"Test/Runtime\" { SubShader { Pass { HLSLPROGRAM #pragma vertex MainVS #pragma fragment MainPS float4 MainVS() : SV_POSITION { return 0; } float4 MainPS() : SV_TARGET { return 1; } ENDHLSL } } }";
}
{
std::ofstream includeFile(includePath);
ASSERT_TRUE(includeFile.is_open());
includeFile << "float4 SharedHelper() { return 1; }";
}
importService.SetProjectRoot(projectRoot.string().c_str());
ResourceType shaderType = ResourceType::Unknown;
EXPECT_TRUE(importService.TryGetImportableResourceType("Assets/runtime.shader", shaderType));
EXPECT_EQ(shaderType, ResourceType::Shader);
ResourceType includeType = ResourceType::Unknown;
EXPECT_FALSE(importService.TryGetImportableResourceType("Assets/shared.hlsl", includeType));
EXPECT_EQ(includeType, ResourceType::Unknown);
importService.Shutdown();
fs::remove_all(projectRoot);
}
TEST(AssetImportService_Test, ImportStatusTracksExplicitOperationsAndRefreshCleanup) {
namespace fs = std::filesystem;

View File

@@ -9,9 +9,12 @@
#include "Core/EditorContext.h"
#include "Core/PlaySessionController.h"
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <chrono>
#include <filesystem>
@@ -24,6 +27,15 @@ namespace fs = std::filesystem;
namespace XCEngine::Editor {
namespace {
bool DirectoryHasEntries(const fs::path& directoryPath) {
std::error_code ec;
if (!fs::exists(directoryPath, ec) || !fs::is_directory(directoryPath, ec)) {
return false;
}
return fs::directory_iterator(directoryPath) != fs::directory_iterator();
}
class EditorActionRoutingTest : public ::testing::Test {
protected:
void SetUp() override {
@@ -138,6 +150,41 @@ TEST_F(EditorActionRoutingTest, HierarchyRouteExecutesCopyPasteDuplicateDeleteAn
EXPECT_FALSE(m_context.GetSelectionManager().IsSelected(duplicatedEntityId));
}
TEST_F(EditorActionRoutingTest, CreatePrimitiveEntityAddsBuiltinMeshComponentsAndSupportsUndoRedo) {
using XCEngine::Resources::BuiltinPrimitiveType;
using XCEngine::Resources::GetBuiltinDefaultPrimitiveMaterialPath;
using XCEngine::Resources::GetBuiltinPrimitiveMeshPath;
auto* entity = Commands::CreatePrimitiveEntity(m_context, BuiltinPrimitiveType::Cube, nullptr);
ASSERT_NE(entity, nullptr);
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), 1u);
auto* meshFilter = entity->GetComponent<::XCEngine::Components::MeshFilterComponent>();
auto* meshRenderer = entity->GetComponent<::XCEngine::Components::MeshRendererComponent>();
ASSERT_NE(meshFilter, nullptr);
ASSERT_NE(meshRenderer, nullptr);
EXPECT_EQ(meshFilter->GetMeshPath(), GetBuiltinPrimitiveMeshPath(BuiltinPrimitiveType::Cube).CStr());
ASSERT_EQ(meshRenderer->GetMaterialCount(), 1u);
EXPECT_EQ(meshRenderer->GetMaterialPath(0), GetBuiltinDefaultPrimitiveMaterialPath().CStr());
EXPECT_TRUE(m_context.GetUndoManager().CanUndo());
m_context.GetUndoManager().Undo();
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), 0u);
m_context.GetUndoManager().Redo();
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), 1u);
auto* restored = m_context.GetSceneManager().GetScene()->Find("Cube");
ASSERT_NE(restored, nullptr);
auto* restoredMeshFilter = restored->GetComponent<::XCEngine::Components::MeshFilterComponent>();
auto* restoredMeshRenderer = restored->GetComponent<::XCEngine::Components::MeshRendererComponent>();
ASSERT_NE(restoredMeshFilter, nullptr);
ASSERT_NE(restoredMeshRenderer, nullptr);
EXPECT_EQ(restoredMeshFilter->GetMeshPath(), GetBuiltinPrimitiveMeshPath(BuiltinPrimitiveType::Cube).CStr());
ASSERT_EQ(restoredMeshRenderer->GetMaterialCount(), 1u);
EXPECT_EQ(restoredMeshRenderer->GetMaterialPath(0), GetBuiltinDefaultPrimitiveMaterialPath().CStr());
}
TEST_F(EditorActionRoutingTest, ProjectRouteExecutesOpenBackAndDelete) {
const fs::path assetsDir = m_projectRoot / "Assets";
const fs::path folderPath = assetsDir / "RouteFolder";
@@ -419,6 +466,32 @@ TEST_F(EditorActionRoutingTest, HierarchyRouterRenameHelpersPublishAndCommit) {
m_context.GetEventBus().Unsubscribe<EntityRenameRequestedEvent>(renameSubscription);
}
TEST_F(EditorActionRoutingTest, CreateTypedLightCommandsAssignExpectedNamesAndTypes) {
auto* directionalLight = Commands::CreateDirectionalLightEntity(m_context);
auto* pointLight = Commands::CreatePointLightEntity(m_context);
auto* spotLight = Commands::CreateSpotLightEntity(m_context);
ASSERT_NE(directionalLight, nullptr);
ASSERT_NE(pointLight, nullptr);
ASSERT_NE(spotLight, nullptr);
EXPECT_EQ(directionalLight->GetName(), "Directional Light");
EXPECT_EQ(pointLight->GetName(), "Point Light");
EXPECT_EQ(spotLight->GetName(), "Spot Light");
auto* directionalComponent = directionalLight->GetComponent<Components::LightComponent>();
auto* pointComponent = pointLight->GetComponent<Components::LightComponent>();
auto* spotComponent = spotLight->GetComponent<Components::LightComponent>();
ASSERT_NE(directionalComponent, nullptr);
ASSERT_NE(pointComponent, nullptr);
ASSERT_NE(spotComponent, nullptr);
EXPECT_EQ(directionalComponent->GetLightType(), Components::LightType::Directional);
EXPECT_EQ(pointComponent->GetLightType(), Components::LightType::Point);
EXPECT_EQ(spotComponent->GetLightType(), Components::LightType::Spot);
}
TEST_F(EditorActionRoutingTest, HierarchyItemContextRequestSelectsEntityAndStoresPopupTarget) {
auto* entity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Entity", "ContextTarget");
ASSERT_NE(entity, nullptr);
@@ -483,68 +556,6 @@ TEST_F(EditorActionRoutingTest, ProjectCommandsRenameAssetUpdatesSelectionAndPre
EXPECT_EQ(m_context.GetProjectManager().GetSelectedItemPath(), renamedItem->fullPath);
}
TEST_F(EditorActionRoutingTest, ProjectCommandsMigrateSceneAssetReferencesRewritesLegacyScenePayloads) {
using ::XCEngine::Resources::ResourceManager;
const fs::path assetsDir = m_projectRoot / "Assets";
const fs::path scenesDir = assetsDir / "Scenes";
const fs::path materialPath = assetsDir / "runtime.material";
const fs::path scenePath = scenesDir / "LegacyScene.xc";
{
std::ofstream materialFile(materialPath.string(), std::ios::out | std::ios::trunc);
ASSERT_TRUE(materialFile.is_open());
materialFile << "{\n";
materialFile << " \"renderQueue\": \"geometry\",\n";
materialFile << " \"renderState\": {\n";
materialFile << " \"cull\": \"back\"\n";
materialFile << " }\n";
materialFile << "}\n";
}
{
std::ofstream sceneFile(scenePath.string(), std::ios::out | std::ios::trunc);
ASSERT_TRUE(sceneFile.is_open());
sceneFile << "# XCEngine Scene File\n";
sceneFile << "scene=Legacy Scene\n";
sceneFile << "active=1\n\n";
sceneFile << "gameobject_begin\n";
sceneFile << "id=1\n";
sceneFile << "uuid=1\n";
sceneFile << "name=Legacy Object\n";
sceneFile << "active=1\n";
sceneFile << "parent=0\n";
sceneFile << "transform=position=0,0,0;rotation=0,0,0,1;scale=1,1,1;\n";
sceneFile << "component=MeshFilter;mesh=builtin://meshes/cube;meshRef=;\n";
sceneFile << "component=MeshRenderer;materials=Assets/runtime.material;materialRefs=;castShadows=1;receiveShadows=1;renderLayer=0;\n";
sceneFile << "gameobject_end\n";
}
ASSERT_TRUE(Commands::CanMigrateSceneAssetReferences(m_context));
const IProjectManager::SceneAssetReferenceMigrationReport report =
Commands::MigrateSceneAssetReferences(m_context);
EXPECT_EQ(report.scannedSceneCount, 1u);
EXPECT_EQ(report.migratedSceneCount, 1u);
EXPECT_EQ(report.unchangedSceneCount, 0u);
EXPECT_EQ(report.failedSceneCount, 0u);
std::ifstream migratedScene(scenePath.string(), std::ios::in | std::ios::binary);
ASSERT_TRUE(migratedScene.is_open());
std::string migratedText((std::istreambuf_iterator<char>(migratedScene)),
std::istreambuf_iterator<char>());
EXPECT_NE(migratedText.find("meshPath=builtin://meshes/cube;"), std::string::npos);
EXPECT_EQ(migratedText.find("component=MeshFilter;mesh=builtin://meshes/cube;"), std::string::npos);
EXPECT_NE(migratedText.find("materialPaths=;"), std::string::npos);
EXPECT_NE(migratedText.find("materialRefs="), std::string::npos);
EXPECT_EQ(migratedText.find("materialRefs=;"), std::string::npos);
EXPECT_EQ(migratedText.find("component=MeshRenderer;materials="), std::string::npos);
ResourceManager::Get().SetResourceRoot("");
ResourceManager::Get().Shutdown();
}
TEST_F(EditorActionRoutingTest, ProjectItemContextRequestSelectsAssetAndStoresPopupTarget) {
const fs::path assetsDir = m_projectRoot / "Assets";
const fs::path filePath = assetsDir / "ContextAsset.txt";
@@ -618,6 +629,64 @@ TEST_F(EditorActionRoutingTest, ProjectSelectionSurvivesRefreshWhenItemOrderChan
FindCurrentItemIndexByName("Selected.txt"));
}
TEST_F(EditorActionRoutingTest, ProjectCommandsExposeAssetCacheMaintenanceActions) {
using ::XCEngine::Resources::ResourceManager;
const fs::path materialPath = m_projectRoot / "Assets" / "ToolMaterial.mat";
std::ofstream(materialPath.string()) << "{\n \"renderQueue\": \"geometry\"\n}\n";
m_context.GetProjectManager().RefreshCurrentFolder();
ResourceManager& resourceManager = ResourceManager::Get();
resourceManager.Initialize();
resourceManager.SetResourceRoot(m_projectRoot.string().c_str());
const AssetItemPtr materialItem = FindCurrentItemByName("ToolMaterial.mat");
ASSERT_NE(materialItem, nullptr);
m_context.GetProjectManager().SetSelectedItem(materialItem);
EXPECT_TRUE(Commands::CanReimportSelectedAsset(m_context));
EXPECT_TRUE(Commands::CanReimportAllAssets(m_context));
EXPECT_TRUE(Commands::CanClearLibrary(m_context));
m_context.SetRuntimeMode(EditorRuntimeMode::Play);
EXPECT_FALSE(Commands::CanReimportSelectedAsset(m_context));
EXPECT_FALSE(Commands::CanReimportAllAssets(m_context));
EXPECT_FALSE(Commands::CanClearLibrary(m_context));
m_context.SetRuntimeMode(EditorRuntimeMode::Edit);
resourceManager.SetResourceRoot("");
resourceManager.Shutdown();
}
TEST_F(EditorActionRoutingTest, ProjectCommandsReimportSelectedAssetAndClearLibraryDriveAssetCache) {
using ::XCEngine::Resources::ResourceManager;
const fs::path materialPath = m_projectRoot / "Assets" / "ToolMaterial.mat";
std::ofstream(materialPath.string()) << "{\n \"renderQueue\": \"geometry\"\n}\n";
m_context.GetProjectManager().RefreshCurrentFolder();
ResourceManager& resourceManager = ResourceManager::Get();
resourceManager.Initialize();
resourceManager.SetResourceRoot(m_projectRoot.string().c_str());
const AssetItemPtr materialItem = FindCurrentItemByName("ToolMaterial.mat");
ASSERT_NE(materialItem, nullptr);
m_context.GetProjectManager().SetSelectedItem(materialItem);
const fs::path libraryRoot(resourceManager.GetProjectLibraryRoot().CStr());
EXPECT_TRUE(Commands::ReimportSelectedAsset(m_context));
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
EXPECT_TRUE(Commands::ClearLibrary(m_context));
EXPECT_FALSE(DirectoryHasEntries(libraryRoot / "Artifacts"));
EXPECT_TRUE(Commands::ReimportAllAssets(m_context));
EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts"));
resourceManager.SetResourceRoot("");
resourceManager.Shutdown();
}
TEST_F(EditorActionRoutingTest, ProjectCommandsRejectMovingFolderIntoItsDescendant) {
const fs::path assetsDir = m_projectRoot / "Assets";
const fs::path parentPath = assetsDir / "Parent";

View File

@@ -34,6 +34,8 @@ using XCEngine::Editor::SceneViewportInteractionResult;
using XCEngine::Editor::SceneViewportOrientationAxis;
using XCEngine::Editor::IViewportHostService;
using XCEngine::Math::Vector2;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UISize;
class StubSelectionManager : public ISelectionManager {
public:
@@ -121,6 +123,8 @@ public:
const std::string& GetCurrentSceneName() const override { return empty; }
XCEngine::Components::Scene* GetScene() override { return nullptr; }
const XCEngine::Components::Scene* GetScene() const override { return nullptr; }
XCEngine::Editor::SceneLoadProgressSnapshot GetSceneLoadProgress() const override { return {}; }
void NotifySceneViewportFramePresented(std::uint32_t) override {}
SceneSnapshot CaptureSceneSnapshot() const override { return {}; }
bool RestoreSceneSnapshot(const SceneSnapshot&) override { return false; }
void CreateDemoScene() override {}
@@ -187,9 +191,9 @@ public:
void BeginFrame() override {}
XCEngine::Editor::EditorViewportFrame RequestViewport(
XCEngine::Editor::EditorViewportKind,
const ImVec2&) override { return {}; }
const UISize&) override { return {}; }
void UpdateSceneViewInput(IEditorContext&, const XCEngine::Editor::SceneViewportInput&) override {}
uint64_t PickSceneViewEntity(IEditorContext&, const ImVec2&, const ImVec2&) override {
uint64_t PickSceneViewEntity(IEditorContext&, const UISize&, const UIPoint&) override {
++pickCallCount;
return pickedEntity;
}
@@ -285,7 +289,7 @@ TEST(SceneViewportInteractionActionsTest, DispatchOrientationActionAlignsViewpor
actions,
context,
viewportHostService,
ImVec2(200.0f, 100.0f),
UISize(200.0f, 100.0f),
Vector2(40.0f, 30.0f));
EXPECT_EQ(viewportHostService.alignedAxis, SceneViewportOrientationAxis::PositiveY);
@@ -305,7 +309,7 @@ TEST(SceneViewportInteractionActionsTest, DispatchSceneIconClickSelectsEntityWit
actions,
context,
viewportHostService,
ImVec2(200.0f, 100.0f),
UISize(200.0f, 100.0f),
Vector2(40.0f, 30.0f));
EXPECT_EQ(context.selectionManager.selectedEntity, 42u);
@@ -325,7 +329,7 @@ TEST(SceneViewportInteractionActionsTest, DispatchScenePickSelectsPickedEntityOr
actions,
context,
viewportHostService,
ImVec2(200.0f, 100.0f),
UISize(200.0f, 100.0f),
Vector2(40.0f, 30.0f));
EXPECT_EQ(context.selectionManager.selectedEntity, 77u);
@@ -336,7 +340,7 @@ TEST(SceneViewportInteractionActionsTest, DispatchScenePickSelectsPickedEntityOr
actions,
context,
viewportHostService,
ImVec2(200.0f, 100.0f),
UISize(200.0f, 100.0f),
Vector2(40.0f, 30.0f));
EXPECT_EQ(context.selectionManager.selectedEntity, 0u);

View File

@@ -44,6 +44,8 @@ using XCEngine::Editor::SceneViewportTransformGizmoOverlayState;
using XCEngine::Editor::SceneViewportTransformSpaceMode;
using XCEngine::Editor::ShouldFocusSceneViewportAfterInteraction;
using XCEngine::Rendering::RenderContext;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UISize;
class EmptySelectionManager : public ISelectionManager {
public:
@@ -90,6 +92,8 @@ public:
const std::string& GetCurrentSceneName() const override { return empty; }
XCEngine::Components::Scene* GetScene() override { return nullptr; }
const XCEngine::Components::Scene* GetScene() const override { return nullptr; }
XCEngine::Editor::SceneLoadProgressSnapshot GetSceneLoadProgress() const override { return {}; }
void NotifySceneViewportFramePresented(std::uint32_t) override {}
SceneSnapshot CaptureSceneSnapshot() const override { return {}; }
bool RestoreSceneSnapshot(const SceneSnapshot&) override { return false; }
void CreateDemoScene() override {}
@@ -180,9 +184,9 @@ public:
class StubViewportHostService : public IViewportHostService {
public:
void BeginFrame() override {}
EditorViewportFrame RequestViewport(EditorViewportKind, const ImVec2&) override { return {}; }
EditorViewportFrame RequestViewport(EditorViewportKind, const UISize&) override { return {}; }
void UpdateSceneViewInput(IEditorContext&, const SceneViewportInput&) override {}
uint64_t PickSceneViewEntity(IEditorContext&, const ImVec2&, const ImVec2&) override { return 0; }
uint64_t PickSceneViewEntity(IEditorContext&, const UISize&, const UIPoint&) override { return 0; }
void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis) override {}
SceneViewportOverlayData GetSceneViewOverlayData() const override { return overlay; }
const SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext&) override {

View File

@@ -13,8 +13,6 @@ using XCEngine::Editor::SceneViewportNavigationState;
using XCEngine::Editor::SceneViewportToolMode;
using XCEngine::Editor::SceneViewportToolShortcutRequest;
using XCEngine::Editor::UpdateSceneViewportNavigationState;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UISize;
TEST(SceneViewportNavigationTest, ToolShortcutActionIgnoresShortcutsDuringTextInputOrDrag) {
SceneViewportToolShortcutRequest request = {};
@@ -90,6 +88,22 @@ TEST(SceneViewportNavigationTest, NavigationUpdateBeginsLookAndPanDrags) {
EXPECT_EQ(middlePanUpdate.state.panDragButton, ImGuiMouseButton_Middle);
}
TEST(SceneViewportNavigationTest, NavigationUpdateBeginsMiddlePanWhenHeldButtonMissedClickEdge) {
SceneViewportNavigationRequest middlePanRequest = {};
middlePanRequest.hasInteractiveViewport = true;
middlePanRequest.viewportHovered = true;
middlePanRequest.middleMouseDown = true;
const auto middlePanUpdate = UpdateSceneViewportNavigationState(middlePanRequest);
EXPECT_FALSE(middlePanUpdate.beginLookDrag);
EXPECT_TRUE(middlePanUpdate.beginPanDrag);
EXPECT_FALSE(middlePanUpdate.beginLeftPanDrag);
EXPECT_TRUE(middlePanUpdate.beginMiddlePanDrag);
EXPECT_TRUE(middlePanUpdate.state.panDragging);
EXPECT_EQ(middlePanUpdate.state.panDragButton, ImGuiMouseButton_Middle);
}
TEST(SceneViewportNavigationTest, NavigationUpdateEndsDragsWhenButtonsRelease) {
SceneViewportNavigationRequest lookRequest = {};
lookRequest.state.lookDragging = true;
@@ -120,7 +134,7 @@ TEST(SceneViewportNavigationTest, CaptureFlagsTrackNavigationAndActiveGizmos) {
TEST(SceneViewportNavigationTest, BuildInputRoutesWheelFocusMovementAndMouseDelta) {
SceneViewportInputBuildRequest request = {};
request.viewportSize = UISize(640.0f, 360.0f);
request.viewportSize = XCEngine::UI::UISize(640.0f, 360.0f);
request.viewportHovered = true;
request.viewportFocused = true;
request.mouseWheel = 2.0f;
@@ -132,10 +146,10 @@ TEST(SceneViewportNavigationTest, BuildInputRoutesWheelFocusMovementAndMouseDelt
request = {};
request.state.lookDragging = true;
request.viewportSize = UISize(640.0f, 360.0f);
request.viewportSize = XCEngine::UI::UISize(640.0f, 360.0f);
request.viewportHovered = true;
request.mouseWheel = 1.5f;
request.mouseDelta = UIPoint(5.0f, -3.0f);
request.mouseDelta = XCEngine::UI::UIPoint(5.0f, -3.0f);
request.fastMove = true;
request.focusSelectionKeyPressed = true;
request.moveForwardKeyDown = true;

View File

@@ -103,6 +103,20 @@ bool ContainsSpriteKind(
});
}
const SceneViewportOverlayLinePrimitive* FindLineStartingAt(
const SceneViewportOverlayFrameData& frameData,
const Math::Vector3& position) {
const auto matchesPosition = [&position](const SceneViewportOverlayLinePrimitive& line) {
return (line.startWorld - position).SqrMagnitude() <= 1e-4f;
};
const auto it = std::find_if(
frameData.worldLines.begin(),
frameData.worldLines.end(),
matchesPosition);
return it != frameData.worldLines.end() ? &(*it) : nullptr;
}
TEST(SceneViewportOverlayProviderRegistryTest, AppendsProvidersInRegistrationOrder) {
EditorContext context;
context.GetSceneManager().NewScene("Overlay Provider Registry");
@@ -186,10 +200,89 @@ TEST(SceneViewportOverlayProviderRegistryTest, LightProviderBuildsSceneIconAndSe
provider->AppendOverlay(buildContext, frameData);
ASSERT_EQ(frameData.worldSprites.size(), 1u);
EXPECT_EQ(frameData.worldSprites[0].textureKind, SceneViewportOverlaySpriteTextureKind::Light);
EXPECT_EQ(frameData.worldSprites[0].textureKind, SceneViewportOverlaySpriteTextureKind::DirectionalLight);
ASSERT_EQ(frameData.handleRecords.size(), 1u);
EXPECT_EQ(frameData.handleRecords[0].entityId, lightEntity->GetID());
EXPECT_GT(frameData.worldLines.size(), 0u);
const SceneViewportOverlayLinePrimitive* connectorLine =
FindLineStartingAt(frameData, lightEntity->GetTransform()->GetPosition());
ASSERT_NE(connectorLine, nullptr);
EXPECT_GT(connectorLine->endWorld.z, connectorLine->startWorld.z);
}
TEST(SceneViewportOverlayProviderRegistryTest, LightProviderBuildsSelectedPointLightHelper) {
EditorContext context;
context.GetSceneManager().NewScene("Point Light Overlay Provider");
auto* lightEntity = context.GetSceneManager().CreateEntity("PointLight");
ASSERT_NE(lightEntity, nullptr);
lightEntity->GetTransform()->SetPosition(Math::Vector3(0.0f, 0.0f, 12.0f));
auto* light = lightEntity->AddComponent<Components::LightComponent>();
ASSERT_NE(light, nullptr);
light->SetLightType(Components::LightType::Point);
light->SetRange(3.5f);
const SceneViewportOverlayData overlay = CreateValidOverlay();
const std::vector<uint64_t> selectedObjectIds = { lightEntity->GetID() };
const SceneViewportOverlayBuildContext buildContext =
CreateBuildContext(context, overlay, selectedObjectIds);
auto provider = CreateSceneViewportLightOverlayProvider();
ASSERT_NE(provider, nullptr);
SceneViewportOverlayFrameData frameData = {};
frameData.overlay = overlay;
provider->AppendOverlay(buildContext, frameData);
ASSERT_EQ(frameData.worldSprites.size(), 1u);
EXPECT_EQ(frameData.worldSprites[0].textureKind, SceneViewportOverlaySpriteTextureKind::PointLight);
ASSERT_EQ(frameData.handleRecords.size(), 1u);
EXPECT_EQ(frameData.handleRecords[0].entityId, lightEntity->GetID());
EXPECT_EQ(frameData.worldLines.size(), 96u);
EXPECT_NEAR(
(frameData.worldLines[0].startWorld - lightEntity->GetTransform()->GetPosition()).Magnitude(),
light->GetRange(),
1e-3f);
}
TEST(SceneViewportOverlayProviderRegistryTest, LightProviderBuildsSelectedSpotLightHelper) {
EditorContext context;
context.GetSceneManager().NewScene("Spot Light Overlay Provider");
auto* lightEntity = context.GetSceneManager().CreateEntity("SpotLight");
ASSERT_NE(lightEntity, nullptr);
lightEntity->GetTransform()->SetPosition(Math::Vector3(0.0f, 0.0f, 6.0f));
auto* light = lightEntity->AddComponent<Components::LightComponent>();
ASSERT_NE(light, nullptr);
light->SetLightType(Components::LightType::Spot);
light->SetRange(4.0f);
light->SetSpotAngle(30.0f);
const SceneViewportOverlayData overlay = CreateValidOverlay();
const std::vector<uint64_t> selectedObjectIds = { lightEntity->GetID() };
const SceneViewportOverlayBuildContext buildContext =
CreateBuildContext(context, overlay, selectedObjectIds);
auto provider = CreateSceneViewportLightOverlayProvider();
ASSERT_NE(provider, nullptr);
SceneViewportOverlayFrameData frameData = {};
frameData.overlay = overlay;
provider->AppendOverlay(buildContext, frameData);
ASSERT_EQ(frameData.worldSprites.size(), 1u);
EXPECT_EQ(frameData.worldSprites[0].textureKind, SceneViewportOverlaySpriteTextureKind::SpotLight);
ASSERT_EQ(frameData.handleRecords.size(), 1u);
EXPECT_EQ(frameData.handleRecords[0].entityId, lightEntity->GetID());
EXPECT_EQ(frameData.worldLines.size(), 37u);
const SceneViewportOverlayLinePrimitive* connectorLine =
FindLineStartingAt(frameData, lightEntity->GetTransform()->GetPosition());
ASSERT_NE(connectorLine, nullptr);
EXPECT_GT(connectorLine->endWorld.z, connectorLine->startWorld.z);
}
TEST(SceneViewportOverlayProviderRegistryTest, TransformGizmoProviderBuildsOverlayFromFormalState) {
@@ -247,7 +340,7 @@ TEST(
EXPECT_EQ(frameData.worldSprites.size(), 2u);
EXPECT_EQ(frameData.handleRecords.size(), 2u);
EXPECT_TRUE(ContainsSpriteKind(frameData, SceneViewportOverlaySpriteTextureKind::Camera));
EXPECT_TRUE(ContainsSpriteKind(frameData, SceneViewportOverlaySpriteTextureKind::Light));
EXPECT_TRUE(ContainsSpriteKind(frameData, SceneViewportOverlaySpriteTextureKind::DirectionalLight));
EXPECT_GT(frameData.worldLines.size(), 12u);
}

View File

@@ -72,6 +72,9 @@ using XCEngine::Editor::BuildSceneViewportHudOverlayData;
using XCEngine::Editor::BuildSceneViewportViewMatrix;
using XCEngine::Editor::HitTestSceneViewportHudOverlay;
using XCEngine::Editor::ProjectSceneViewportWorldPoint;
using XCEngine::Editor::SceneViewportOverlayFrameData;
using XCEngine::Editor::SceneViewportOverlaySpritePrimitive;
using XCEngine::Editor::SceneViewportOverlaySpriteTextureKind;
using XCEngine::Editor::SceneViewportOverlayData;
using XCEngine::Components::GameObject;
using XCEngine::Math::Vector3;
@@ -284,6 +287,26 @@ TEST(SceneViewportOverlayRenderer_Test, BuildSceneViewportHudOverlayDataTracksVi
EXPECT_FALSE(hiddenHud.HasVisibleElements());
}
TEST(SceneViewportOverlayRenderer_Test, BuildSceneViewportHudOverlayDataCanExposeSceneIconsWithoutOrientationGizmo) {
SceneViewportOverlayData overlay = {};
overlay.valid = true;
SceneViewportOverlayFrameData frameData = {};
frameData.overlay = overlay;
SceneViewportOverlaySpritePrimitive& sprite = frameData.worldSprites.emplace_back();
sprite.worldPosition = Vector3::Zero();
sprite.sizePixels = XCEngine::Math::Vector2(32.0f, 32.0f);
sprite.textureKind = SceneViewportOverlaySpriteTextureKind::Camera;
const auto iconsOnlyHud = BuildSceneViewportHudOverlayData(
overlay,
false,
&frameData,
true);
EXPECT_TRUE(iconsOnlyHud.HasVisibleElements());
}
TEST(SceneViewportOverlayRenderer_Test, HitTestSceneViewportHudOverlaySkipsInvalidOrHiddenOverlay) {
const SceneViewportHudOverlayHitResult invalidHit =
HitTestSceneViewportHudOverlay({}, ImVec2(0.0f, 0.0f), ImVec2(200.0f, 200.0f), ImVec2(100.0f, 100.0f));

View File

@@ -17,11 +17,23 @@ using XCEngine::Editor::kSceneViewportOverlaySpriteResourceCount;
using XCEngine::Editor::kSceneViewportOverlaySpriteTextureKinds;
TEST(SceneViewportOverlaySpriteResourcesTest, TextureKindIndexMappingIsStable) {
EXPECT_EQ(kSceneViewportOverlaySpriteResourceCount, 2u);
EXPECT_EQ(kSceneViewportOverlaySpriteResourceCount, 4u);
EXPECT_EQ(GetSceneViewportOverlaySpriteResourceIndex(SceneViewportOverlaySpriteTextureKind::Camera), 0u);
EXPECT_EQ(GetSceneViewportOverlaySpriteResourceIndex(SceneViewportOverlaySpriteTextureKind::Light), 1u);
EXPECT_EQ(
GetSceneViewportOverlaySpriteResourceIndex(SceneViewportOverlaySpriteTextureKind::DirectionalLight),
1u);
EXPECT_EQ(GetSceneViewportOverlaySpriteResourceIndex(SceneViewportOverlaySpriteTextureKind::PointLight), 2u);
EXPECT_EQ(GetSceneViewportOverlaySpriteResourceIndex(SceneViewportOverlaySpriteTextureKind::SpotLight), 3u);
EXPECT_EQ(GetSceneViewportOverlaySpriteTextureKindByIndex(0u), SceneViewportOverlaySpriteTextureKind::Camera);
EXPECT_EQ(GetSceneViewportOverlaySpriteTextureKindByIndex(1u), SceneViewportOverlaySpriteTextureKind::Light);
EXPECT_EQ(
GetSceneViewportOverlaySpriteTextureKindByIndex(1u),
SceneViewportOverlaySpriteTextureKind::DirectionalLight);
EXPECT_EQ(
GetSceneViewportOverlaySpriteTextureKindByIndex(2u),
SceneViewportOverlaySpriteTextureKind::PointLight);
EXPECT_EQ(
GetSceneViewportOverlaySpriteTextureKindByIndex(3u),
SceneViewportOverlaySpriteTextureKind::SpotLight);
}
TEST(SceneViewportOverlaySpriteResourcesTest, AssetSpecsResolveKnownEditorIcons) {
@@ -34,6 +46,24 @@ TEST(SceneViewportOverlaySpriteResourcesTest, AssetSpecsResolveKnownEditorIcons)
EXPECT_TRUE(path.is_absolute());
EXPECT_TRUE(std::filesystem::exists(path));
EXPECT_NE(path.generic_string().find("editor/resources/Icons"), std::string::npos);
switch (textureKind) {
case SceneViewportOverlaySpriteTextureKind::Camera:
EXPECT_EQ(path.filename().generic_string(), "camera_gizmo.png");
break;
case SceneViewportOverlaySpriteTextureKind::DirectionalLight:
EXPECT_EQ(path.filename().generic_string(), "directional_light_gizmo.png");
break;
case SceneViewportOverlaySpriteTextureKind::PointLight:
EXPECT_EQ(path.filename().generic_string(), "point_light_gizmo.png");
break;
case SceneViewportOverlaySpriteTextureKind::SpotLight:
EXPECT_EQ(path.filename().generic_string(), "spot_light_gizmo.png");
break;
default:
FAIL() << "Unexpected texture kind";
break;
}
}
}

View File

@@ -4,6 +4,7 @@
#include "Viewport/SceneViewportShaderPaths.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Resources/Shader/ShaderLoader.h>
#include <filesystem>
@@ -12,9 +13,11 @@
namespace {
using XCEngine::Editor::GetSceneViewportCameraGizmoIconPath;
using XCEngine::Editor::GetSceneViewportDirectionalLightGizmoIconPath;
using XCEngine::Editor::GetSceneViewportInfiniteGridShaderPath;
using XCEngine::Editor::GetSceneViewportMainLightGizmoIconPath;
using XCEngine::Editor::GetSceneViewportObjectIdOutlineShaderPath;
using XCEngine::Editor::GetSceneViewportPointLightGizmoIconPath;
using XCEngine::Editor::GetSceneViewportSpotLightGizmoIconPath;
using XCEngine::Resources::GetBuiltinObjectIdOutlineShaderPath;
using XCEngine::Resources::LoadResult;
using XCEngine::Resources::ResourceHandle;
using XCEngine::Resources::ResourceManager;
@@ -27,22 +30,29 @@ using XCEngine::Resources::ShaderType;
TEST(SceneViewportShaderPathsTest, ResolvePathsUnderEditorResources) {
const std::filesystem::path gridPath(GetSceneViewportInfiniteGridShaderPath().CStr());
const std::filesystem::path outlinePath(GetSceneViewportObjectIdOutlineShaderPath().CStr());
const std::filesystem::path cameraIconPath(GetSceneViewportCameraGizmoIconPath().CStr());
const std::filesystem::path lightIconPath(GetSceneViewportMainLightGizmoIconPath().CStr());
const std::filesystem::path directionalLightIconPath(GetSceneViewportDirectionalLightGizmoIconPath().CStr());
const std::filesystem::path pointLightIconPath(GetSceneViewportPointLightGizmoIconPath().CStr());
const std::filesystem::path spotLightIconPath(GetSceneViewportSpotLightGizmoIconPath().CStr());
EXPECT_TRUE(gridPath.is_absolute());
EXPECT_TRUE(outlinePath.is_absolute());
EXPECT_TRUE(cameraIconPath.is_absolute());
EXPECT_TRUE(lightIconPath.is_absolute());
EXPECT_TRUE(directionalLightIconPath.is_absolute());
EXPECT_TRUE(pointLightIconPath.is_absolute());
EXPECT_TRUE(spotLightIconPath.is_absolute());
EXPECT_TRUE(std::filesystem::exists(gridPath));
EXPECT_TRUE(std::filesystem::exists(outlinePath));
EXPECT_TRUE(std::filesystem::exists(cameraIconPath));
EXPECT_TRUE(std::filesystem::exists(lightIconPath));
EXPECT_TRUE(std::filesystem::exists(directionalLightIconPath));
EXPECT_TRUE(std::filesystem::exists(pointLightIconPath));
EXPECT_TRUE(std::filesystem::exists(spotLightIconPath));
EXPECT_NE(gridPath.generic_string().find("editor/resources/shaders/scene-viewport"), std::string::npos);
EXPECT_NE(outlinePath.generic_string().find("editor/resources/shaders/scene-viewport"), std::string::npos);
EXPECT_NE(cameraIconPath.generic_string().find("editor/resources/Icons"), std::string::npos);
EXPECT_NE(lightIconPath.generic_string().find("editor/resources/Icons"), std::string::npos);
EXPECT_NE(directionalLightIconPath.generic_string().find("editor/resources/Icons"), std::string::npos);
EXPECT_NE(pointLightIconPath.generic_string().find("editor/resources/Icons"), std::string::npos);
EXPECT_NE(spotLightIconPath.generic_string().find("editor/resources/Icons"), std::string::npos);
EXPECT_EQ(directionalLightIconPath.filename().generic_string(), "directional_light_gizmo.png");
EXPECT_EQ(pointLightIconPath.filename().generic_string(), "point_light_gizmo.png");
EXPECT_EQ(spotLightIconPath.filename().generic_string(), "spot_light_gizmo.png");
}
TEST(SceneViewportShaderPathsTest, ShaderLoaderLoadsSceneViewportInfiniteGridShader) {
@@ -81,7 +91,7 @@ TEST(SceneViewportShaderPathsTest, ResourceManagerLoadsSceneViewportOutlineShade
ResourceManager& manager = ResourceManager::Get();
manager.Shutdown();
const ResourceHandle<Shader> shaderHandle = manager.Load<Shader>(GetSceneViewportObjectIdOutlineShaderPath());
const ResourceHandle<Shader> shaderHandle = manager.Load<Shader>(GetBuiltinObjectIdOutlineShaderPath());
ASSERT_TRUE(shaderHandle.IsValid());
const ShaderPass* pass = shaderHandle->FindPass("ObjectIdOutline");
@@ -94,7 +104,7 @@ TEST(SceneViewportShaderPathsTest, ResourceManagerLoadsSceneViewportOutlineShade
ShaderBackend::D3D12);
ASSERT_NE(fragment, nullptr);
EXPECT_NE(
std::string(fragment->sourceCode.CStr()).find("XC_EDITOR_SCENE_VIEW_OBJECT_ID_OUTLINE_D3D12_PS"),
std::string(fragment->sourceCode.CStr()).find("XC_BUILTIN_OBJECT_ID_OUTLINE_D3D12_PS"),
std::string::npos);
manager.Shutdown();

View File

@@ -48,6 +48,8 @@ using XCEngine::Editor::SubmitSceneViewportTransformGizmoOverlaySubmission;
using XCEngine::Editor::SceneSnapshot;
using XCEngine::Rendering::RenderContext;
using XCEngine::Math::Vector2;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UISize;
class StubSelectionManager : public ISelectionManager {
public:
@@ -134,6 +136,8 @@ public:
const std::string& GetCurrentSceneName() const override { return empty; }
XCEngine::Components::Scene* GetScene() override { return nullptr; }
const XCEngine::Components::Scene* GetScene() const override { return nullptr; }
XCEngine::Editor::SceneLoadProgressSnapshot GetSceneLoadProgress() const override { return {}; }
void NotifySceneViewportFramePresented(std::uint32_t) override {}
SceneSnapshot CaptureSceneSnapshot() const override { return {}; }
bool RestoreSceneSnapshot(const SceneSnapshot&) override { return false; }
void CreateDemoScene() override {}
@@ -226,9 +230,9 @@ public:
class StubViewportHostService : public IViewportHostService {
public:
void BeginFrame() override {}
EditorViewportFrame RequestViewport(EditorViewportKind, const ImVec2&) override { return {}; }
EditorViewportFrame RequestViewport(EditorViewportKind, const UISize&) override { return {}; }
void UpdateSceneViewInput(IEditorContext&, const SceneViewportInput&) override {}
uint64_t PickSceneViewEntity(IEditorContext&, const ImVec2&, const ImVec2&) override { return 0; }
uint64_t PickSceneViewEntity(IEditorContext&, const UISize&, const UIPoint&) override { return 0; }
void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis) override {}
SceneViewportOverlayData GetSceneViewOverlayData() const override { return {}; }
const SceneViewportOverlayFrameData& GetSceneViewEditorOverlayFrameData(IEditorContext&) override {

View File

@@ -17,6 +17,8 @@ using XCEngine::Editor::ViewportObjectIdReadbackRequest;
using XCEngine::RHI::RHICommandQueue;
using XCEngine::RHI::RHITexture;
using XCEngine::RHI::ResourceStates;
using XCEngine::UI::UIPoint;
using XCEngine::UI::UISize;
RHICommandQueue* MakeDummyQueue() {
return reinterpret_cast<RHICommandQueue*>(static_cast<uintptr_t>(0x1));
@@ -34,8 +36,8 @@ ViewportObjectIdPickContext CreateValidContext() {
context.textureWidth = 1280;
context.textureHeight = 720;
context.hasValidFrame = true;
context.viewportSize = ImVec2(1280.0f, 720.0f);
context.viewportMousePosition = ImVec2(640.0f, 360.0f);
context.viewportSize = UISize(1280.0f, 720.0f);
context.viewportMousePosition = UIPoint(640.0f, 360.0f);
return context;
}
@@ -59,17 +61,17 @@ TEST(ViewportObjectIdPickerTest, CanPickRejectsMissingOrOutOfBoundsInputs) {
EXPECT_FALSE(CanPickViewportObjectId(context));
context = CreateValidContext();
context.viewportMousePosition = ImVec2(-1.0f, 10.0f);
context.viewportMousePosition = UIPoint(-1.0f, 10.0f);
EXPECT_FALSE(CanPickViewportObjectId(context));
context = CreateValidContext();
context.viewportMousePosition = ImVec2(10.0f, 721.0f);
context.viewportMousePosition = UIPoint(10.0f, 721.0f);
EXPECT_FALSE(CanPickViewportObjectId(context));
}
TEST(ViewportObjectIdPickerTest, BuildReadbackRequestMapsViewportCoordinatesToTexturePixels) {
ViewportObjectIdPickContext context = CreateValidContext();
context.viewportMousePosition = ImVec2(1280.0f, 720.0f);
context.viewportMousePosition = UIPoint(1280.0f, 720.0f);
ViewportObjectIdReadbackRequest request = {};
ASSERT_TRUE(BuildViewportObjectIdReadbackRequest(context, request));

View File

@@ -299,6 +299,41 @@ TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportRenderPlanCollectsPostSceneA
EXPECT_EQ(result.warningStatusText, nullptr);
}
TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportRenderPlanSkipsRhiOverlayPassWhenFrameContainsOnlySceneIcons) {
const SceneViewportOverlayData overlay = CreateValidOverlay();
SceneViewportOverlayFrameData editorOverlayFrameData = {};
editorOverlayFrameData.overlay = overlay;
auto& sprite = editorOverlayFrameData.worldSprites.emplace_back();
sprite.worldPosition = XCEngine::Math::Vector3::Zero();
sprite.sizePixels = XCEngine::Math::Vector2(32.0f, 32.0f);
size_t overlayFactoryCallCount = 0u;
const auto result = BuildSceneViewportRenderPlan(
{},
overlay,
{},
editorOverlayFrameData,
[](const SceneViewportGridPassData&) {
return std::make_unique<NoopRenderPass>();
},
[](
RHIResourceView*,
const std::vector<uint64_t>&,
const SceneViewportSelectionOutlineStyle&) {
return std::make_unique<NoopRenderPass>();
},
[&overlayFactoryCallCount](const SceneViewportOverlayFrameData&) {
++overlayFactoryCallCount;
return std::make_unique<NoopRenderPass>();
},
false);
EXPECT_EQ(result.plan.postScenePasses.GetPassCount(), 1u);
EXPECT_EQ(result.plan.overlayPasses.GetPassCount(), 0u);
EXPECT_EQ(overlayFactoryCallCount, 0u);
}
TEST(ViewportRenderFlowUtilsTest, BuildSceneViewportRenderPlanWarnsWhenSelectionOutlineCannotAccessObjectIdTexture) {
const SceneViewportOverlayData overlay = CreateValidOverlay();

View File

@@ -81,7 +81,9 @@ TEST(ViewportRenderTargetsTest, BuildReuseQueryReflectsCurrentResourcePresence)
targets.objectIdTexture = reinterpret_cast<RHITexture*>(static_cast<uintptr_t>(0x5));
targets.objectIdView = reinterpret_cast<RHIResourceView*>(static_cast<uintptr_t>(0x6));
targets.objectIdShaderView = reinterpret_cast<RHIResourceView*>(static_cast<uintptr_t>(0x7));
targets.textureId = static_cast<ImTextureID>(static_cast<uintptr_t>(0x8));
targets.textureHandle.nativeHandle = 0x8;
targets.textureHandle.width = 1280;
targets.textureHandle.height = 720;
const auto query =
BuildViewportRenderTargetsReuseQuery(EditorViewportKind::Scene, targets, 1280, 720);
@@ -146,7 +148,9 @@ TEST(ViewportRenderTargetsTest, DestroyViewportRenderTargetsShutsDownAndClearsSt
targets.objectIdShaderView = objectIdShaderView;
targets.imguiCpuHandle.ptr = 123;
targets.imguiGpuHandle.ptr = 456;
targets.textureId = static_cast<ImTextureID>(static_cast<uintptr_t>(789));
targets.textureHandle.nativeHandle = 789;
targets.textureHandle.width = 640;
targets.textureHandle.height = 360;
targets.colorState = ResourceStates::RenderTarget;
targets.objectIdState = ResourceStates::PixelShaderResource;
targets.hasValidObjectIdFrame = true;
@@ -171,7 +175,9 @@ TEST(ViewportRenderTargetsTest, DestroyViewportRenderTargetsShutsDownAndClearsSt
EXPECT_EQ(targets.objectIdShaderView, nullptr);
EXPECT_EQ(targets.imguiCpuHandle.ptr, 0u);
EXPECT_EQ(targets.imguiGpuHandle.ptr, 0u);
EXPECT_EQ(targets.textureId, ImTextureID{});
EXPECT_EQ(targets.textureHandle.nativeHandle, 0u);
EXPECT_EQ(targets.textureHandle.width, 0u);
EXPECT_EQ(targets.textureHandle.height, 0u);
EXPECT_EQ(targets.colorState, ResourceStates::Common);
EXPECT_EQ(targets.objectIdState, ResourceStates::Common);
EXPECT_FALSE(targets.hasValidObjectIdFrame);