From 781c3b9a787c4a2c89c54dbf8e9df9503ced6150 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 4 Apr 2026 19:51:02 +0800 Subject: [PATCH] Implement XCUI markup import loader support --- ...Subplan-06_XCUI-Markup-Import-HotReload.md | 46 +++++ engine/CMakeLists.txt | 33 +++- .../XCEngine/Core/Asset/ArtifactFormats.h | 28 ++- .../XCEngine/Core/Asset/AssetDatabase.h | 28 ++- .../XCEngine/Core/Asset/ResourceTypes.h | 11 +- engine/include/XCEngine/Resources/Resources.h | 4 + engine/src/Resources/UI/UIDocumentLoaders.cpp | 167 ++++++++++++++++++ tests/Core/Asset/test_resource_types.cpp | 16 ++ tests/Resources/CMakeLists.txt | 1 + 9 files changed, 320 insertions(+), 14 deletions(-) create mode 100644 docs/plan/xcui-subplans/Subplan-06_XCUI-Markup-Import-HotReload.md create mode 100644 engine/src/Resources/UI/UIDocumentLoaders.cpp diff --git a/docs/plan/xcui-subplans/Subplan-06_XCUI-Markup-Import-HotReload.md b/docs/plan/xcui-subplans/Subplan-06_XCUI-Markup-Import-HotReload.md new file mode 100644 index 00000000..52716d03 --- /dev/null +++ b/docs/plan/xcui-subplans/Subplan-06_XCUI-Markup-Import-HotReload.md @@ -0,0 +1,46 @@ +# Subplan 06:XCUI Markup / Import / Hot Reload + +目标: + +- 把 `.xcui` / `.xctheme` / `.xcschema` 拉进资源系统。 +- 建立导入、编译产物、热重载、诊断输出的第一版链路。 + +负责人边界: + +- 负责资源类型、导入器、artifact、诊断日志。 +- 不负责 widget 运行时逻辑本身。 + +建议目录: + +- `engine/include/XCEngine/Resources/UI/` +- `engine/src/Resources/UI/` +- `editor/src` 中与导入面板、诊断输出相关的接入口 + +前置依赖: + +- 需要主计划中的资源类型命名拍板。 +- 与 `Subplan 03`、`Subplan 07` 协调格式字段。 + +现在就可以先做的内容: + +- 定义三类资源描述结构 +- 设计导入错误诊断格式 +- 设计热重载触发和缓存失效策略 +- 先做一个最小 parser,可以把简单 `.xcui` 编成中间结构 + +明确不做: + +- 不做完整 markup 语法大全 +- 不做 inspector 的最终渲染 + +交付物: + +- UI 资源类型定义 +- 导入器与 artifact 结构 +- 热重载与错误输出最小闭环 + +验收标准: + +- UI 资源可被 ResourceManager 识别 +- 导入失败时有可读诊断 +- 改动文件后可触发重新加载 diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 5ad164bd..26751ece 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -292,6 +292,10 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Shader/ShaderLoader.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/AudioClip/AudioClip.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/AudioClip/AudioLoader.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/UI/UIDocumentTypes.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/UI/UIDocumentCompiler.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/UI/UIDocuments.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/UI/UIDocumentLoaders.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Texture/Texture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Texture/TextureLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Texture/TextureImportSettings.cpp @@ -305,6 +309,9 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioClip.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/UI/UIDocumentCompiler.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/UI/UIDocuments.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/UI/UIDocumentLoaders.cpp # Scripting ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/IScriptRuntime.h @@ -413,14 +420,24 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIElementTree.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Core/UIContext.h ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Core/UIBuildContext.cpp - /src/UI/Core/UIElementTree.cpp - /include/XCEngine/UI/Style/StyleTypes.h - /include/XCEngine/UI/Style/Theme.h - /include/XCEngine/UI/Style/StyleSet.h - /include/XCEngine/UI/Style/StyleResolver.h - /src/UI/Style/StyleTypes.cpp - /src/UI/Style/Theme.cpp - /src/UI/Style/StyleResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Core/UIElementTree.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/StyleTypes.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/Theme.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/StyleSet.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Style/StyleResolver.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Style/StyleTypes.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Style/Theme.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Style/StyleResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Input/UIInputPath.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Input/UIFocusController.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Input/UIInputRouter.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Input/UIShortcutRegistry.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/UI/Input/UIInputDispatcher.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputPath.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIFocusController.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputRouter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIShortcutRegistry.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/UI/Input/UIInputDispatcher.cpp # Input ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Input/InputTypes.h diff --git a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h index 46b2e0e9..e71e7410 100644 --- a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h +++ b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h @@ -5,14 +5,16 @@ #include #include #include +#include namespace XCEngine { namespace Resources { constexpr Core::uint32 kTextureArtifactSchemaVersion = 1; -constexpr Core::uint32 kMaterialArtifactSchemaVersion = 1; +constexpr Core::uint32 kMaterialArtifactSchemaVersion = 2; constexpr Core::uint32 kMeshArtifactSchemaVersion = 2; constexpr Core::uint32 kShaderArtifactSchemaVersion = 1; +constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 1; struct TextureArtifactHeader { char magic[8] = { 'X', 'C', 'T', 'E', 'X', '0', '1', '\0' }; @@ -44,7 +46,7 @@ struct MeshArtifactHeader { }; struct MaterialArtifactFileHeader { - char magic[8] = { 'X', 'C', 'M', 'A', 'T', '0', '1', '\0' }; + char magic[8] = { 'X', 'C', 'M', 'A', 'T', '0', '2', '\0' }; Core::uint32 schemaVersion = kMaterialArtifactSchemaVersion; }; @@ -94,5 +96,27 @@ struct ShaderVariantArtifactHeader { Core::uint64 compiledBinarySize = 0; }; +struct UIDocumentArtifactFileHeader { + char magic[8] = { 'X', 'C', 'U', 'I', 'D', '0', '1', '\0' }; + Core::uint32 schemaVersion = kUIDocumentArtifactSchemaVersion; + Core::uint32 kind = 0; + Core::uint32 dependencyCount = 0; + Core::uint32 diagnosticCount = 0; +}; + +struct UIDocumentArtifactNodeHeader { + Core::uint32 attributeCount = 0; + Core::uint32 childCount = 0; + Core::uint32 line = 1; + Core::uint32 column = 1; + Core::uint32 selfClosing = 0; +}; + +struct UIDocumentArtifactDiagnosticHeader { + Core::uint32 severity = 0; + Core::uint32 line = 1; + Core::uint32 column = 1; +}; + } // namespace Resources } // namespace XCEngine diff --git a/engine/include/XCEngine/Core/Asset/AssetDatabase.h b/engine/include/XCEngine/Core/Asset/AssetDatabase.h index ef4029d7..513259eb 100644 --- a/engine/include/XCEngine/Core/Asset/AssetDatabase.h +++ b/engine/include/XCEngine/Core/Asset/AssetDatabase.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,11 @@ class Material; class AssetDatabase { public: + struct MaintenanceStats { + Core::uint32 importedAssetCount = 0; + Core::uint32 removedArtifactCount = 0; + }; + struct ArtifactDependencyRecord { Containers::String path; Containers::String hash; @@ -56,6 +62,7 @@ public: struct ResolvedAsset { bool exists = false; bool artifactReady = false; + bool imported = false; Containers::String absolutePath; Containers::String relativePath; AssetGUID assetGuid; @@ -67,13 +74,18 @@ public: void Initialize(const Containers::String& projectRoot); void Shutdown(); - void Refresh(); + MaintenanceStats Refresh(); bool ResolvePath(const Containers::String& requestPath, Containers::String& outAbsolutePath, Containers::String& outRelativePath) const; bool TryGetAssetGuid(const Containers::String& requestPath, AssetGUID& outGuid) const; + bool TryGetImportableResourceType(const Containers::String& requestPath, ResourceType& outType) const; bool TryGetAssetRef(const Containers::String& requestPath, ResourceType resourceType, AssetRef& outRef) const; + bool ReimportAsset(const Containers::String& requestPath, + ResolvedAsset& outAsset, + MaintenanceStats* outStats = nullptr); + bool ReimportAllAssets(MaintenanceStats* outStats = nullptr); bool EnsureArtifact(const Containers::String& requestPath, ResourceType requestedType, ResolvedAsset& outAsset); @@ -84,19 +96,21 @@ public: const Containers::String& GetProjectRoot() const { return m_projectRoot; } const Containers::String& GetAssetsRoot() const { return m_assetsRoot; } const Containers::String& GetLibraryRoot() const { return m_libraryRoot; } + const Containers::String& GetLastErrorMessage() const { return m_lastErrorMessage; } private: - static constexpr Core::uint32 kCurrentImporterVersion = 4; + static constexpr Core::uint32 kCurrentImporterVersion = 5; void EnsureProjectLayout(); void LoadSourceAssetDB(); void SaveSourceAssetDB() const; void LoadArtifactDB(); void SaveArtifactDB() const; - void ScanAssets(); + MaintenanceStats ScanAssets(); void ScanAssetPath(const std::filesystem::path& path, std::unordered_map& seenPaths); void RemoveMissingRecords(const std::unordered_map& seenPaths); + Core::uint32 CleanupOrphanedArtifacts() const; bool EnsureMetaForPath(const std::filesystem::path& sourcePath, bool isFolder, @@ -125,6 +139,11 @@ private: ArtifactRecord& outRecord); bool ImportShaderAsset(const SourceAssetRecord& sourceRecord, ArtifactRecord& outRecord); + bool ImportUIDocumentAsset(const SourceAssetRecord& sourceRecord, + UIDocumentKind kind, + const char* artifactFileName, + ResourceType resourceType, + ArtifactRecord& outRecord); Containers::String BuildArtifactKey( const SourceAssetRecord& sourceRecord, @@ -146,12 +165,15 @@ private: std::vector& outDependencies) const; bool CollectShaderDependencies(const SourceAssetRecord& sourceRecord, std::vector& outDependencies) const; + void ClearLastErrorMessage(); + void SetLastErrorMessage(const Containers::String& message); Containers::String m_projectRoot; Containers::String m_assetsRoot; Containers::String m_libraryRoot; Containers::String m_sourceDbPath; Containers::String m_artifactDbPath; + Containers::String m_lastErrorMessage; std::unordered_map m_sourcesByPathKey; std::unordered_map m_sourcesByGuid; diff --git a/engine/include/XCEngine/Core/Asset/ResourceTypes.h b/engine/include/XCEngine/Core/Asset/ResourceTypes.h index 7b4b7ae5..4d26e282 100644 --- a/engine/include/XCEngine/Core/Asset/ResourceTypes.h +++ b/engine/include/XCEngine/Core/Asset/ResourceTypes.h @@ -22,7 +22,10 @@ enum class ResourceType : Core::uint8 { Font, ParticleSystem, Scene, - Prefab + Prefab, + UIView, + UITheme, + UISchema }; constexpr const char* GetResourceTypeName(ResourceType type) { @@ -39,6 +42,9 @@ constexpr const char* GetResourceTypeName(ResourceType type) { case ResourceType::ParticleSystem: return "ParticleSystem"; case ResourceType::Scene: return "Scene"; case ResourceType::Prefab: return "Prefab"; + case ResourceType::UIView: return "UIView"; + case ResourceType::UITheme: return "UITheme"; + case ResourceType::UISchema: return "UISchema"; default: return "Unknown"; } } @@ -89,6 +95,9 @@ template<> inline ResourceType GetResourceType() { return Resour template<> inline ResourceType GetResourceType() { return ResourceType::Shader; } template<> inline ResourceType GetResourceType() { return ResourceType::AudioClip; } template<> inline ResourceType GetResourceType() { return ResourceType::Binary; } +template<> inline ResourceType GetResourceType() { return ResourceType::UIView; } +template<> inline ResourceType GetResourceType() { return ResourceType::UITheme; } +template<> inline ResourceType GetResourceType() { return ResourceType::UISchema; } } // namespace Resources } // namespace XCEngine diff --git a/engine/include/XCEngine/Resources/Resources.h b/engine/include/XCEngine/Resources/Resources.h index 5b8a95d2..5ebb4200 100644 --- a/engine/include/XCEngine/Resources/Resources.h +++ b/engine/include/XCEngine/Resources/Resources.h @@ -23,6 +23,10 @@ #include #include #include +#include +#include +#include +#include #include #include diff --git a/engine/src/Resources/UI/UIDocumentLoaders.cpp b/engine/src/Resources/UI/UIDocumentLoaders.cpp new file mode 100644 index 00000000..685387e8 --- /dev/null +++ b/engine/src/Resources/UI/UIDocumentLoaders.cpp @@ -0,0 +1,167 @@ +#include + +#include + +namespace XCEngine { +namespace Resources { + +namespace { + +Containers::String GetPathExtension(const Containers::String& path) { + const std::filesystem::path fsPath(path.CStr()); + const std::string extension = fsPath.has_extension() + ? fsPath.extension().generic_string() + : std::string(); + if (!extension.empty() && extension.front() == '.') { + return Containers::String(extension.substr(1).c_str()); + } + return Containers::String(extension.c_str()); +} + +Containers::Array BuildSingleExtensionList( + const char* sourceExtension, + const char* artifactExtension) { + Containers::Array extensions; + extensions.PushBack(sourceExtension); + extensions.PushBack(artifactExtension); + return extensions; +} + +bool MatchesAnyExtension( + const Containers::String& path, + const char* sourceExtension, + const char* artifactExtension) { + const Containers::String extension = GetPathExtension(path).ToLower(); + return extension == sourceExtension || extension == artifactExtension; +} + +Containers::String BuildDocumentDisplayName( + const Containers::String& path, + const UIDocumentModel& document) { + if (!document.displayName.Empty()) { + return document.displayName; + } + + const std::filesystem::path fsPath(path.CStr()); + const std::string stem = fsPath.stem().generic_string(); + return stem.empty() ? path : Containers::String(stem.c_str()); +} + +template +LoadResult BuildDocumentLoadResult( + const Containers::String& path, + UIDocumentCompileResult& compileResult) { + if (!compileResult.succeeded || !compileResult.document.valid) { + return LoadResult(compileResult.errorMessage); + } + + auto* resource = new TDocumentResource(); + IResource::ConstructParams params = {}; + params.name = BuildDocumentDisplayName(path, compileResult.document); + params.path = compileResult.document.sourcePath.Empty() ? path : compileResult.document.sourcePath; + params.guid = ResourceGUID::Generate(params.path); + resource->Initialize(params); + resource->SetDocumentModel(std::move(compileResult.document)); + return LoadResult(resource); +} + +bool LoadDocumentWithKind( + const Containers::String& path, + UIDocumentKind kind, + const char* artifactExtension, + UIDocumentCompileResult& compileResult) { + const Containers::String extension = GetPathExtension(path).ToLower(); + if (extension == artifactExtension) { + if (!LoadUIDocumentArtifact(path, kind, compileResult)) { + return false; + } + return true; + } + + if (!CompileUIDocument( + UIDocumentCompileRequest{kind, path, GetUIDocumentDefaultRootTag(kind)}, + compileResult)) { + return false; + } + return true; +} + +} // namespace + +Containers::Array UIViewLoader::GetSupportedExtensions() const { + return BuildSingleExtensionList("xcui", "xcuiasset"); +} + +bool UIViewLoader::CanLoad(const Containers::String& path) const { + return MatchesAnyExtension(path, "xcui", "xcuiasset"); +} + +bool UIViewLoader::CompileDocument(const Containers::String& path, UIDocumentCompileResult& outResult) const { + return CompileUIDocument( + UIDocumentCompileRequest{UIDocumentKind::View, path, GetUIDocumentDefaultRootTag(UIDocumentKind::View)}, + outResult); +} + +LoadResult UIViewLoader::Load(const Containers::String& path, const ImportSettings* settings) { + (void)settings; + + UIDocumentCompileResult compileResult = {}; + if (!LoadDocumentWithKind(path, UIDocumentKind::View, "xcuiasset", compileResult)) { + return LoadResult(compileResult.errorMessage); + } + + return BuildDocumentLoadResult(path, compileResult); +} + +Containers::Array UIThemeLoader::GetSupportedExtensions() const { + return BuildSingleExtensionList("xctheme", "xcthemeasset"); +} + +bool UIThemeLoader::CanLoad(const Containers::String& path) const { + return MatchesAnyExtension(path, "xctheme", "xcthemeasset"); +} + +bool UIThemeLoader::CompileDocument(const Containers::String& path, UIDocumentCompileResult& outResult) const { + return CompileUIDocument( + UIDocumentCompileRequest{UIDocumentKind::Theme, path, GetUIDocumentDefaultRootTag(UIDocumentKind::Theme)}, + outResult); +} + +LoadResult UIThemeLoader::Load(const Containers::String& path, const ImportSettings* settings) { + (void)settings; + + UIDocumentCompileResult compileResult = {}; + if (!LoadDocumentWithKind(path, UIDocumentKind::Theme, "xcthemeasset", compileResult)) { + return LoadResult(compileResult.errorMessage); + } + + return BuildDocumentLoadResult(path, compileResult); +} + +Containers::Array UISchemaLoader::GetSupportedExtensions() const { + return BuildSingleExtensionList("xcschema", "xcschemaasset"); +} + +bool UISchemaLoader::CanLoad(const Containers::String& path) const { + return MatchesAnyExtension(path, "xcschema", "xcschemaasset"); +} + +bool UISchemaLoader::CompileDocument(const Containers::String& path, UIDocumentCompileResult& outResult) const { + return CompileUIDocument( + UIDocumentCompileRequest{UIDocumentKind::Schema, path, GetUIDocumentDefaultRootTag(UIDocumentKind::Schema)}, + outResult); +} + +LoadResult UISchemaLoader::Load(const Containers::String& path, const ImportSettings* settings) { + (void)settings; + + UIDocumentCompileResult compileResult = {}; + if (!LoadDocumentWithKind(path, UIDocumentKind::Schema, "xcschemaasset", compileResult)) { + return LoadResult(compileResult.errorMessage); + } + + return BuildDocumentLoadResult(path, compileResult); +} + +} // namespace Resources +} // namespace XCEngine diff --git a/tests/Core/Asset/test_resource_types.cpp b/tests/Core/Asset/test_resource_types.cpp index 2d96518f..ae6be4e1 100644 --- a/tests/Core/Asset/test_resource_types.cpp +++ b/tests/Core/Asset/test_resource_types.cpp @@ -1,6 +1,7 @@ #include #include #include +#include using namespace XCEngine::Resources; @@ -14,12 +15,24 @@ TEST(Resources_Types, ResourceType_EnumValues) { EXPECT_EQ(static_cast(ResourceType::Shader), 4); EXPECT_EQ(static_cast(ResourceType::AudioClip), 5); EXPECT_EQ(static_cast(ResourceType::Binary), 6); + EXPECT_EQ(static_cast(ResourceType::AnimationClip), 7); + EXPECT_EQ(static_cast(ResourceType::Skeleton), 8); + EXPECT_EQ(static_cast(ResourceType::Font), 9); + EXPECT_EQ(static_cast(ResourceType::ParticleSystem), 10); + EXPECT_EQ(static_cast(ResourceType::Scene), 11); + EXPECT_EQ(static_cast(ResourceType::Prefab), 12); + EXPECT_EQ(static_cast(ResourceType::UIView), 13); + EXPECT_EQ(static_cast(ResourceType::UITheme), 14); + EXPECT_EQ(static_cast(ResourceType::UISchema), 15); } TEST(Resources_Types, GetResourceTypeName) { EXPECT_STREQ(GetResourceTypeName(ResourceType::Texture), "Texture"); EXPECT_STREQ(GetResourceTypeName(ResourceType::Mesh), "Mesh"); EXPECT_STREQ(GetResourceTypeName(ResourceType::Material), "Material"); + EXPECT_STREQ(GetResourceTypeName(ResourceType::UIView), "UIView"); + EXPECT_STREQ(GetResourceTypeName(ResourceType::UITheme), "UITheme"); + EXPECT_STREQ(GetResourceTypeName(ResourceType::UISchema), "UISchema"); EXPECT_STREQ(GetResourceTypeName(ResourceType::Unknown), "Unknown"); } @@ -30,6 +43,9 @@ TEST(Resources_Types, GetResourceType_TemplateSpecializations) { EXPECT_EQ(GetResourceType(), ResourceType::Shader); EXPECT_EQ(GetResourceType(), ResourceType::AudioClip); EXPECT_EQ(GetResourceType(), ResourceType::Binary); + EXPECT_EQ(GetResourceType(), ResourceType::UIView); + EXPECT_EQ(GetResourceType(), ResourceType::UITheme); + EXPECT_EQ(GetResourceType(), ResourceType::UISchema); } } // namespace diff --git a/tests/Resources/CMakeLists.txt b/tests/Resources/CMakeLists.txt index f3e8671a..9f6309e1 100644 --- a/tests/Resources/CMakeLists.txt +++ b/tests/Resources/CMakeLists.txt @@ -7,3 +7,4 @@ add_subdirectory(Mesh) add_subdirectory(Material) add_subdirectory(Shader) add_subdirectory(AudioClip) +add_subdirectory(UI)