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