Implement XCUI markup import loader support

This commit is contained in:
2026-04-04 19:51:02 +08:00
parent bcef1f145b
commit 781c3b9a78
9 changed files with 320 additions and 14 deletions

View File

@@ -0,0 +1,46 @@
# Subplan 06XCUI 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 识别
- 导入失败时有可读诊断
- 改动文件后可触发重新加载

View File

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

View File

@@ -5,14 +5,16 @@
#include <XCEngine/Resources/Material/Material.h>
#include <XCEngine/Resources/Mesh/Mesh.h>
#include <XCEngine/Resources/Texture/Texture.h>
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
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

View File

@@ -3,6 +3,7 @@
#include <XCEngine/Core/Asset/AssetRef.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Core/Types.h>
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
#include <filesystem>
#include <unordered_map>
@@ -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<std::string, bool>& seenPaths);
void RemoveMissingRecords(const std::unordered_map<std::string, bool>& 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<ArtifactDependencyRecord>& outDependencies) const;
bool CollectShaderDependencies(const SourceAssetRecord& sourceRecord,
std::vector<ArtifactDependencyRecord>& 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<std::string, SourceAssetRecord> m_sourcesByPathKey;
std::unordered_map<AssetGUID, SourceAssetRecord> m_sourcesByGuid;

View File

@@ -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<class Material>() { return Resour
template<> inline ResourceType GetResourceType<class Shader>() { return ResourceType::Shader; }
template<> inline ResourceType GetResourceType<class AudioClip>() { return ResourceType::AudioClip; }
template<> inline ResourceType GetResourceType<class BinaryResource>() { return ResourceType::Binary; }
template<> inline ResourceType GetResourceType<class UIView>() { return ResourceType::UIView; }
template<> inline ResourceType GetResourceType<class UITheme>() { return ResourceType::UITheme; }
template<> inline ResourceType GetResourceType<class UISchema>() { return ResourceType::UISchema; }
} // namespace Resources
} // namespace XCEngine

View File

@@ -23,6 +23,10 @@
#include <XCEngine/Resources/Shader/ShaderLoader.h>
#include <XCEngine/Resources/AudioClip/AudioClip.h>
#include <XCEngine/Resources/AudioClip/AudioLoader.h>
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
#include <XCEngine/Resources/UI/UIDocuments.h>
#include <XCEngine/Resources/UI/UIDocumentLoaders.h>
#include <XCEngine/Core/IO/ResourceFileSystem.h>
#include <XCEngine/Core/IO/FileArchive.h>

View File

@@ -0,0 +1,167 @@
#include <XCEngine/Resources/UI/UIDocumentLoaders.h>
#include <filesystem>
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<Containers::String> BuildSingleExtensionList(
const char* sourceExtension,
const char* artifactExtension) {
Containers::Array<Containers::String> 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 <typename TDocumentResource>
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<Containers::String> 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<UIView>(path, compileResult);
}
Containers::Array<Containers::String> 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<UITheme>(path, compileResult);
}
Containers::Array<Containers::String> 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<UISchema>(path, compileResult);
}
} // namespace Resources
} // namespace XCEngine

View File

@@ -1,6 +1,7 @@
#include <gtest/gtest.h>
#include <XCEngine/Core/Asset/ResourceTypes.h>
#include <XCEngine/Core/Types.h>
#include <XCEngine/Resources/UI/UIDocuments.h>
using namespace XCEngine::Resources;
@@ -14,12 +15,24 @@ TEST(Resources_Types, ResourceType_EnumValues) {
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Shader), 4);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::AudioClip), 5);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Binary), 6);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::AnimationClip), 7);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Skeleton), 8);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Font), 9);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::ParticleSystem), 10);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Scene), 11);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Prefab), 12);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UIView), 13);
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UITheme), 14);
EXPECT_EQ(static_cast<uint8_t>(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<Shader>(), ResourceType::Shader);
EXPECT_EQ(GetResourceType<AudioClip>(), ResourceType::AudioClip);
EXPECT_EQ(GetResourceType<BinaryResource>(), ResourceType::Binary);
EXPECT_EQ(GetResourceType<UIView>(), ResourceType::UIView);
EXPECT_EQ(GetResourceType<UITheme>(), ResourceType::UITheme);
EXPECT_EQ(GetResourceType<UISchema>(), ResourceType::UISchema);
}
} // namespace

View File

@@ -7,3 +7,4 @@ add_subdirectory(Mesh)
add_subdirectory(Material)
add_subdirectory(Shader)
add_subdirectory(AudioClip)
add_subdirectory(UI)