From a7662a1d43647b52509ec102f9e2ce2d1dc423ae Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 5 Apr 2026 05:45:51 +0800 Subject: [PATCH] Add XCUI schema document regression coverage --- docs/plan/XCUI_Phase_Status_2026-04-05.md | 28 +++- .../XCEngine/Core/Asset/ArtifactFormats.h | 2 +- .../XCEngine/Resources/UI/UIDocumentTypes.h | 72 +++++++++ .../XCEngine/Resources/UI/UIDocuments.h | 1 + engine/src/Resources/UI/UIDocuments.cpp | 28 ++++ tests/Resources/UI/CMakeLists.txt | 1 + .../Resources/UI/test_ui_schema_document.cpp | 153 ++++++++++++++++++ 7 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 tests/Resources/UI/test_ui_schema_document.cpp diff --git a/docs/plan/XCUI_Phase_Status_2026-04-05.md b/docs/plan/XCUI_Phase_Status_2026-04-05.md index 8d12a40a..21946169 100644 --- a/docs/plan/XCUI_Phase_Status_2026-04-05.md +++ b/docs/plan/XCUI_Phase_Status_2026-04-05.md @@ -9,8 +9,11 @@ Old `editor` replacement is explicitly out of scope for this phase. - Phase 1 sandbox batch committed and pushed as `67a28bd` (`Add XCUI new editor sandbox phase 1`). - Phase 2 common/runtime batch committed and pushed as `ade5be3` (`Add XCUI runtime screen layer and demo textarea`). -- Current work has moved into Phase 3: stabilize schema/validation and continue filling the remaining common/runtime/editor gaps instead of replacing the old editor. -- The current stable editor-layer batch is centered on `LayoutLab` as the widget proving ground for tree/list/property-section style controls. +- Phase 3 has now produced a stable mixed batch across common/runtime/editor: + - schema document definition data is now retained on `UIDocumentModel` and round-trips through the UI artifact path + - engine runtime coverage was tightened again around `UISystem` and concrete document-host rendering + - `LayoutLab` continues as the editor widget proving ground for tree/list/property-section style controls +- Old `editor` replacement remains deferred; all active execution still stays inside XCUI shared code and `new_editor`. ## Three-Layer Status @@ -18,6 +21,8 @@ Old `editor` replacement is explicitly out of scope for this phase. - `UI::DrawData`, input event types, focus routing, style/theme resolution are in active use. - `UIDocumentCompiler` is buildable again after repairing the duplicated schema helper regression introduced by overlapping schema work. +- `UIDocumentModel` / `UIDocumentResource` now retain schema definition metadata explicitly, including memory accounting and `UISchema` accessors. +- `.xcschema` round-trip coverage is now present through compile, loader, artifact write, and artifact read paths. - Build-system hardening for MSVC/PDB output paths has started in root CMake, `engine/CMakeLists.txt`, `new_editor/CMakeLists.txt`, and `tests/NewEditor/CMakeLists.txt`. - Shared engine-side XCUI runtime scaffolding is now present under `engine/include/XCEngine/UI/Runtime` and `engine/src/UI/Runtime`. - Shared engine-side `UIDocumentScreenHost` now compiles `.xcui` / `.xctheme` screen documents into a runtime-facing document host path instead of leaving all document ownership in `new_editor`. @@ -25,7 +30,7 @@ Old `editor` replacement is explicitly out of scope for this phase. Current gap: -- Schema/validation is not yet landed in a stable form. +- Minimal schema self-definition support is landed, but schema-driven validation for `.xcui` / `.xctheme` instances is still not implemented. - Shared widget/runtime instantiation is still thin and mostly editor-side. - Common widget primitives are still incomplete: multiline text editing, tree/list virtualization, property-grid composition, and native image/source-rect level APIs. @@ -35,6 +40,7 @@ Current gap: - The demo runtime has moved past single-line input: multiline `TextArea` behavior is now covered in the sandbox testbed. - Engine-side runtime ownership is no longer zero: `UIScreenPlayer`, `UIDocumentScreenHost`, and `UISystem` now define a shared runtime contract for loading a screen document, ticking it with input, and collecting `UI::UIDrawData`. - `UISystem` now supports layered screen composition semantics: stacked screen players, top-interactive input routing, and modal layers that block lower screens. +- Runtime screen emission now also carries concrete button text in the shared document host path instead of silently dropping button labels. Current gap: @@ -66,6 +72,7 @@ Current gap: - `XCNewEditor` Debug target builds successfully - `core_ui_tests`: `14/14` - `core_ui_style_tests`: `5/5` +- `ui_resource_tests`: `7/7` ## Landed This Phase @@ -74,11 +81,17 @@ Current gap: - Demo authored resources updated to exercise the input field. - LayoutLab `ScrollView` prototype with clipping and hover rejection outside clipped content. - LayoutLab editor-widget prototypes for tree/list/property-style sections with dedicated runtime coverage. +- Schema document support extended with: + - retained `UISchemaDefinition` data on `UIDocumentModel` + - artifact schema version bump for UI documents + - loader/resource accessors and memory accounting + - schema compile/load/artifact regression coverage - Engine runtime layer added: - `UIScreenPlayer` - `UIDocumentScreenHost` - `UISystem` - layered screen composition and modal blocking semantics +- Runtime document-host draw emission now preserves button labels for shared screen rendering. - RHI image path improvements: - clipped image UV adjustment - mirrored image UV preservation @@ -91,7 +104,7 @@ Current gap: ## Phase Risks Still Open -- Schema/validation still needs a clean minimal landing instead of the current partially merged state in `UIDocumentCompiler.cpp`. +- Schema instance validation is still open beyond `.xcschema` self-definition and artifact round-trip coverage. - `ScrollView` is still authored/static; no wheel-driven scrolling or virtualization yet. - `Image` widgets still do not have source-rect/atlas-subregion level API in the high-level draw command model. - Editor shell still depends on ImGui as host chrome. @@ -99,8 +112,11 @@ Current gap: ## Next Phase -1. Cleanly stabilize schema/validation in `UIDocumentCompiler.cpp` and add targeted schema regression tests. +1. Expand schema rules just one level further inside `UIDocumentCompiler.cpp`: + - `allowedValues` only for `Enum` + - `documentKind` / `restrictDocumentKind` only for `Document` + - `restrictDocumentKind=true` requires explicit `documentKind` 2. Expand runtime/game-layer ownership from the current document host + layered `UISystem` into reusable menu/HUD stack patterns and engine runtime integration. 3. Promote the current editor-facing widget prototypes out of authored `LayoutLab` content and into reusable XCUI widget/runtime modules, then continue with toolbar/menu and more native shell-owned chrome. -4. Move more diagnostics and shell affordances into XCUI-owned editor-layer surfaces instead of only ImGui HUDs. +4. Start the window-level compositor split in `new_editor` so the editor shell can run through `ImGuiHostCompositor` first and then grow a native XCUI compositor path on the same seam. 5. Continue phased validation, commit, push, and plan refresh after each stable batch. diff --git a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h index e71e7410..f5810fd6 100644 --- a/engine/include/XCEngine/Core/Asset/ArtifactFormats.h +++ b/engine/include/XCEngine/Core/Asset/ArtifactFormats.h @@ -14,7 +14,7 @@ constexpr Core::uint32 kTextureArtifactSchemaVersion = 1; constexpr Core::uint32 kMaterialArtifactSchemaVersion = 2; constexpr Core::uint32 kMeshArtifactSchemaVersion = 2; constexpr Core::uint32 kShaderArtifactSchemaVersion = 1; -constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 1; +constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 2; struct TextureArtifactHeader { char magic[8] = { 'X', 'C', 'T', 'E', 'X', '0', '1', '\0' }; diff --git a/engine/include/XCEngine/Resources/UI/UIDocumentTypes.h b/engine/include/XCEngine/Resources/UI/UIDocumentTypes.h index a46fc6f4..44fd13db 100644 --- a/engine/include/XCEngine/Resources/UI/UIDocumentTypes.h +++ b/engine/include/XCEngine/Resources/UI/UIDocumentTypes.h @@ -19,6 +19,15 @@ enum class UIDocumentDiagnosticSeverity : Core::uint8 { Error }; +enum class UISchemaValueType : Core::uint8 { + String = 0, + Boolean, + Integer, + Number, + Document, + Enum +}; + struct UIDocumentSourceLocation { Core::uint32 line = 1; Core::uint32 column = 1; @@ -35,6 +44,67 @@ struct UIDocumentDiagnostic { Containers::String message; }; +struct UISchemaAttributeDefinition { + Containers::String name; + UISchemaValueType valueType = UISchemaValueType::String; + UIDocumentKind documentKind = UIDocumentKind::View; + Containers::Array allowedValues; + UIDocumentSourceLocation location = {}; + bool required = false; + bool restrictDocumentKind = false; +}; + +struct UISchemaElementDefinition { + Containers::String tagName; + Containers::Array attributes; + Containers::Array children; + UIDocumentSourceLocation location = {}; + bool allowUnknownAttributes = false; + bool allowUnknownChildren = false; + + const UISchemaAttributeDefinition* FindAttribute(const Containers::String& name) const { + for (const UISchemaAttributeDefinition& attribute : attributes) { + if (attribute.name == name) { + return &attribute; + } + } + + return nullptr; + } + + const UISchemaElementDefinition* FindChild(const Containers::String& tagNameValue) const { + for (const UISchemaElementDefinition& child : children) { + if (child.tagName == tagNameValue) { + return &child; + } + } + + return nullptr; + } +}; + +struct UISchemaDefinition { + Containers::String name; + Containers::Array elements; + bool valid = false; + + const UISchemaElementDefinition* FindElement(const Containers::String& tagNameValue) const { + for (const UISchemaElementDefinition& element : elements) { + if (element.tagName == tagNameValue) { + return &element; + } + } + + return nullptr; + } + + void Clear() { + name.Clear(); + elements.Clear(); + valid = false; + } +}; + struct UIDocumentNode { Containers::String tagName; Containers::Array attributes; @@ -58,6 +128,7 @@ struct UIDocumentModel { Containers::String sourcePath; Containers::String displayName; UIDocumentNode rootNode; + UISchemaDefinition schemaDefinition; Containers::Array dependencies; Containers::Array diagnostics; bool valid = false; @@ -66,6 +137,7 @@ struct UIDocumentModel { sourcePath.Clear(); displayName.Clear(); rootNode = UIDocumentNode(); + schemaDefinition.Clear(); dependencies.Clear(); diagnostics.Clear(); valid = false; diff --git a/engine/include/XCEngine/Resources/UI/UIDocuments.h b/engine/include/XCEngine/Resources/UI/UIDocuments.h index 2dcd50b6..eff10310 100644 --- a/engine/include/XCEngine/Resources/UI/UIDocuments.h +++ b/engine/include/XCEngine/Resources/UI/UIDocuments.h @@ -17,6 +17,7 @@ public: const UIDocumentModel& GetDocument() const { return m_document; } const UIDocumentNode& GetRootNode() const { return m_document.rootNode; } + const UISchemaDefinition& GetSchemaDefinition() const { return m_document.schemaDefinition; } const Containers::Array& GetDependencies() const { return m_document.dependencies; } const Containers::Array& GetDiagnostics() const { return m_document.diagnostics; } const Containers::String& GetSourcePath() const { return m_document.sourcePath; } diff --git a/engine/src/Resources/UI/UIDocuments.cpp b/engine/src/Resources/UI/UIDocuments.cpp index bd7cc94b..274b642c 100644 --- a/engine/src/Resources/UI/UIDocuments.cpp +++ b/engine/src/Resources/UI/UIDocuments.cpp @@ -24,6 +24,33 @@ size_t MeasureDiagnosticMemorySize(const UIDocumentDiagnostic& diagnostic) { return sizeof(UIDocumentDiagnostic) + diagnostic.message.Length(); } +size_t MeasureSchemaAttributeMemorySize(const UISchemaAttributeDefinition& attribute) { + size_t size = sizeof(UISchemaAttributeDefinition) + attribute.name.Length(); + for (const Containers::String& value : attribute.allowedValues) { + size += sizeof(Containers::String) + value.Length(); + } + return size; +} + +size_t MeasureSchemaElementMemorySize(const UISchemaElementDefinition& element) { + size_t size = sizeof(UISchemaElementDefinition) + element.tagName.Length(); + for (const UISchemaAttributeDefinition& attribute : element.attributes) { + size += MeasureSchemaAttributeMemorySize(attribute); + } + for (const UISchemaElementDefinition& child : element.children) { + size += MeasureSchemaElementMemorySize(child); + } + return size; +} + +size_t MeasureSchemaDefinitionMemorySize(const UISchemaDefinition& schemaDefinition) { + size_t size = sizeof(UISchemaDefinition) + schemaDefinition.name.Length(); + for (const UISchemaElementDefinition& element : schemaDefinition.elements) { + size += MeasureSchemaElementMemorySize(element); + } + return size; +} + } // namespace void UIDocumentResource::Release() { @@ -47,6 +74,7 @@ void UIDocumentResource::RecalculateMemorySize() { size += m_document.sourcePath.Length(); size += m_document.displayName.Length(); size += MeasureNodeMemorySize(m_document.rootNode); + size += MeasureSchemaDefinitionMemorySize(m_document.schemaDefinition); for (const Containers::String& dependency : m_document.dependencies) { size += sizeof(Containers::String) + dependency.Length(); } diff --git a/tests/Resources/UI/CMakeLists.txt b/tests/Resources/UI/CMakeLists.txt index 829e4abd..91c339d4 100644 --- a/tests/Resources/UI/CMakeLists.txt +++ b/tests/Resources/UI/CMakeLists.txt @@ -4,6 +4,7 @@ set(UI_RESOURCE_TEST_SOURCES test_ui_document_loader.cpp + test_ui_schema_document.cpp ) add_executable(ui_resource_tests ${UI_RESOURCE_TEST_SOURCES}) diff --git a/tests/Resources/UI/test_ui_schema_document.cpp b/tests/Resources/UI/test_ui_schema_document.cpp new file mode 100644 index 00000000..7aa4eca1 --- /dev/null +++ b/tests/Resources/UI/test_ui_schema_document.cpp @@ -0,0 +1,153 @@ +#include + +#include +#include + +#include +#include + +using namespace XCEngine::Resources; + +namespace { + +void WriteTextFile(const std::filesystem::path& path, const std::string& contents) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream output(path, std::ios::binary | std::ios::trunc); + ASSERT_TRUE(output.is_open()); + output << contents; + ASSERT_TRUE(static_cast(output)); +} + +TEST(UISchemaDocument, CompileAndArtifactLoadPopulateSchemaDefinition) { + namespace fs = std::filesystem; + + const fs::path root = fs::temp_directory_path() / "xc_ui_schema_compile_test"; + const fs::path schemaPath = root / "markup.xcschema"; + const fs::path artifactPath = root / "markup.xcschemaasset"; + fs::remove_all(root); + + WriteTextFile( + schemaPath, + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"); + + UISchemaLoader loader; + UIDocumentCompileResult compileResult = {}; + ASSERT_TRUE(loader.CompileDocument(schemaPath.string().c_str(), compileResult)); + ASSERT_TRUE(compileResult.succeeded); + ASSERT_TRUE(compileResult.document.valid); + ASSERT_TRUE(compileResult.document.schemaDefinition.valid); + EXPECT_EQ(compileResult.document.schemaDefinition.name, "EditorMarkup"); + + const UISchemaElementDefinition* viewElement = + compileResult.document.schemaDefinition.FindElement("View"); + ASSERT_NE(viewElement, nullptr); + EXPECT_TRUE(viewElement->allowUnknownChildren); + + const UISchemaAttributeDefinition* themeAttribute = viewElement->FindAttribute("theme"); + ASSERT_NE(themeAttribute, nullptr); + EXPECT_EQ(themeAttribute->valueType, UISchemaValueType::Document); + EXPECT_TRUE(themeAttribute->restrictDocumentKind); + EXPECT_EQ(themeAttribute->documentKind, UIDocumentKind::Theme); + + const UISchemaAttributeDefinition* modeAttribute = viewElement->FindAttribute("mode"); + ASSERT_NE(modeAttribute, nullptr); + EXPECT_EQ(modeAttribute->valueType, UISchemaValueType::Enum); + ASSERT_EQ(modeAttribute->allowedValues.Size(), 2u); + EXPECT_EQ(modeAttribute->allowedValues[0], "compact"); + EXPECT_EQ(modeAttribute->allowedValues[1], "cozy"); + + LoadResult loadResult = loader.Load(schemaPath.string().c_str()); + ASSERT_TRUE(loadResult); + ASSERT_NE(loadResult.resource, nullptr); + auto* schemaResource = static_cast(loadResult.resource); + ASSERT_NE(schemaResource, nullptr); + ASSERT_TRUE(schemaResource->GetSchemaDefinition().valid); + ASSERT_NE(schemaResource->GetSchemaDefinition().FindElement("View"), nullptr); + delete schemaResource; + + XCEngine::Containers::String artifactWriteError; + ASSERT_TRUE( + WriteUIDocumentArtifact(artifactPath.string().c_str(), compileResult, &artifactWriteError)) + << artifactWriteError.CStr(); + + UIDocumentCompileResult artifactResult = {}; + ASSERT_TRUE(LoadUIDocumentArtifact( + artifactPath.string().c_str(), + UIDocumentKind::Schema, + artifactResult)); + ASSERT_TRUE(artifactResult.succeeded); + ASSERT_TRUE(artifactResult.document.valid); + ASSERT_TRUE(artifactResult.document.schemaDefinition.valid); + + const UISchemaElementDefinition* artifactViewElement = + artifactResult.document.schemaDefinition.FindElement("View"); + ASSERT_NE(artifactViewElement, nullptr); + ASSERT_NE(artifactViewElement->FindChild("Column"), nullptr); + + fs::remove_all(root); +} + +TEST(UISchemaDocument, CompileRejectsInvalidSchemaFlags) { + namespace fs = std::filesystem; + + const fs::path root = fs::temp_directory_path() / "xc_ui_schema_invalid_flag_test"; + const fs::path schemaPath = root / "broken.xcschema"; + fs::remove_all(root); + + WriteTextFile( + schemaPath, + "\n" + " \n" + "\n"); + + UISchemaLoader loader; + UIDocumentCompileResult compileResult = {}; + EXPECT_FALSE(loader.CompileDocument(schemaPath.string().c_str(), compileResult)); + EXPECT_FALSE(compileResult.succeeded); + EXPECT_FALSE(compileResult.errorMessage.Empty()); + ASSERT_FALSE(compileResult.document.diagnostics.Empty()); + + const std::string errorMessage = compileResult.errorMessage.CStr(); + EXPECT_NE(errorMessage.find("allowUnknownChildren"), std::string::npos); + + fs::remove_all(root); +} + +TEST(UISchemaDocument, CompileRejectsDuplicateAttributeDefinitions) { + namespace fs = std::filesystem; + + const fs::path root = fs::temp_directory_path() / "xc_ui_schema_duplicate_attribute_test"; + const fs::path schemaPath = root / "duplicate.xcschema"; + fs::remove_all(root); + + WriteTextFile( + schemaPath, + "\n" + " \n" + " \n" + " \n" + " \n" + "\n"); + + UISchemaLoader loader; + UIDocumentCompileResult compileResult = {}; + EXPECT_FALSE(loader.CompileDocument(schemaPath.string().c_str(), compileResult)); + EXPECT_FALSE(compileResult.succeeded); + EXPECT_FALSE(compileResult.errorMessage.Empty()); + ASSERT_FALSE(compileResult.document.diagnostics.Empty()); + + const std::string errorMessage = compileResult.errorMessage.CStr(); + EXPECT_NE(errorMessage.find("Duplicate schema attribute definition"), std::string::npos); + + fs::remove_all(root); +} + +} // namespace