Add XCUI schema document regression coverage

This commit is contained in:
2026-04-05 05:45:51 +08:00
parent 6dcf881967
commit a7662a1d43
7 changed files with 278 additions and 7 deletions

View File

@@ -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 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`). - 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. - Phase 3 has now produced a stable mixed batch across common/runtime/editor:
- The current stable editor-layer batch is centered on `LayoutLab` as the widget proving ground for tree/list/property-section style controls. - 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 ## 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. - `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. - `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`. - 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 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`. - 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: 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. - 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. - 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. - 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`. - 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. - `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: Current gap:
@@ -66,6 +72,7 @@ Current gap:
- `XCNewEditor` Debug target builds successfully - `XCNewEditor` Debug target builds successfully
- `core_ui_tests`: `14/14` - `core_ui_tests`: `14/14`
- `core_ui_style_tests`: `5/5` - `core_ui_style_tests`: `5/5`
- `ui_resource_tests`: `7/7`
## Landed This Phase ## Landed This Phase
@@ -74,11 +81,17 @@ Current gap:
- Demo authored resources updated to exercise the input field. - Demo authored resources updated to exercise the input field.
- LayoutLab `ScrollView` prototype with clipping and hover rejection outside clipped content. - 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. - 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: - Engine runtime layer added:
- `UIScreenPlayer` - `UIScreenPlayer`
- `UIDocumentScreenHost` - `UIDocumentScreenHost`
- `UISystem` - `UISystem`
- layered screen composition and modal blocking semantics - layered screen composition and modal blocking semantics
- Runtime document-host draw emission now preserves button labels for shared screen rendering.
- RHI image path improvements: - RHI image path improvements:
- clipped image UV adjustment - clipped image UV adjustment
- mirrored image UV preservation - mirrored image UV preservation
@@ -91,7 +104,7 @@ Current gap:
## Phase Risks Still Open ## 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. - `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. - `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. - Editor shell still depends on ImGui as host chrome.
@@ -99,8 +112,11 @@ Current gap:
## Next Phase ## 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. 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. 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. 5. Continue phased validation, commit, push, and plan refresh after each stable batch.

View File

@@ -14,7 +14,7 @@ constexpr Core::uint32 kTextureArtifactSchemaVersion = 1;
constexpr Core::uint32 kMaterialArtifactSchemaVersion = 2; constexpr Core::uint32 kMaterialArtifactSchemaVersion = 2;
constexpr Core::uint32 kMeshArtifactSchemaVersion = 2; constexpr Core::uint32 kMeshArtifactSchemaVersion = 2;
constexpr Core::uint32 kShaderArtifactSchemaVersion = 1; constexpr Core::uint32 kShaderArtifactSchemaVersion = 1;
constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 1; constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 2;
struct TextureArtifactHeader { struct TextureArtifactHeader {
char magic[8] = { 'X', 'C', 'T', 'E', 'X', '0', '1', '\0' }; char magic[8] = { 'X', 'C', 'T', 'E', 'X', '0', '1', '\0' };

View File

@@ -19,6 +19,15 @@ enum class UIDocumentDiagnosticSeverity : Core::uint8 {
Error Error
}; };
enum class UISchemaValueType : Core::uint8 {
String = 0,
Boolean,
Integer,
Number,
Document,
Enum
};
struct UIDocumentSourceLocation { struct UIDocumentSourceLocation {
Core::uint32 line = 1; Core::uint32 line = 1;
Core::uint32 column = 1; Core::uint32 column = 1;
@@ -35,6 +44,67 @@ struct UIDocumentDiagnostic {
Containers::String message; Containers::String message;
}; };
struct UISchemaAttributeDefinition {
Containers::String name;
UISchemaValueType valueType = UISchemaValueType::String;
UIDocumentKind documentKind = UIDocumentKind::View;
Containers::Array<Containers::String> allowedValues;
UIDocumentSourceLocation location = {};
bool required = false;
bool restrictDocumentKind = false;
};
struct UISchemaElementDefinition {
Containers::String tagName;
Containers::Array<UISchemaAttributeDefinition> attributes;
Containers::Array<UISchemaElementDefinition> 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<UISchemaElementDefinition> 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 { struct UIDocumentNode {
Containers::String tagName; Containers::String tagName;
Containers::Array<UIDocumentAttribute> attributes; Containers::Array<UIDocumentAttribute> attributes;
@@ -58,6 +128,7 @@ struct UIDocumentModel {
Containers::String sourcePath; Containers::String sourcePath;
Containers::String displayName; Containers::String displayName;
UIDocumentNode rootNode; UIDocumentNode rootNode;
UISchemaDefinition schemaDefinition;
Containers::Array<Containers::String> dependencies; Containers::Array<Containers::String> dependencies;
Containers::Array<UIDocumentDiagnostic> diagnostics; Containers::Array<UIDocumentDiagnostic> diagnostics;
bool valid = false; bool valid = false;
@@ -66,6 +137,7 @@ struct UIDocumentModel {
sourcePath.Clear(); sourcePath.Clear();
displayName.Clear(); displayName.Clear();
rootNode = UIDocumentNode(); rootNode = UIDocumentNode();
schemaDefinition.Clear();
dependencies.Clear(); dependencies.Clear();
diagnostics.Clear(); diagnostics.Clear();
valid = false; valid = false;

View File

@@ -17,6 +17,7 @@ public:
const UIDocumentModel& GetDocument() const { return m_document; } const UIDocumentModel& GetDocument() const { return m_document; }
const UIDocumentNode& GetRootNode() const { return m_document.rootNode; } const UIDocumentNode& GetRootNode() const { return m_document.rootNode; }
const UISchemaDefinition& GetSchemaDefinition() const { return m_document.schemaDefinition; }
const Containers::Array<Containers::String>& GetDependencies() const { return m_document.dependencies; } const Containers::Array<Containers::String>& GetDependencies() const { return m_document.dependencies; }
const Containers::Array<UIDocumentDiagnostic>& GetDiagnostics() const { return m_document.diagnostics; } const Containers::Array<UIDocumentDiagnostic>& GetDiagnostics() const { return m_document.diagnostics; }
const Containers::String& GetSourcePath() const { return m_document.sourcePath; } const Containers::String& GetSourcePath() const { return m_document.sourcePath; }

View File

@@ -24,6 +24,33 @@ size_t MeasureDiagnosticMemorySize(const UIDocumentDiagnostic& diagnostic) {
return sizeof(UIDocumentDiagnostic) + diagnostic.message.Length(); 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 } // namespace
void UIDocumentResource::Release() { void UIDocumentResource::Release() {
@@ -47,6 +74,7 @@ void UIDocumentResource::RecalculateMemorySize() {
size += m_document.sourcePath.Length(); size += m_document.sourcePath.Length();
size += m_document.displayName.Length(); size += m_document.displayName.Length();
size += MeasureNodeMemorySize(m_document.rootNode); size += MeasureNodeMemorySize(m_document.rootNode);
size += MeasureSchemaDefinitionMemorySize(m_document.schemaDefinition);
for (const Containers::String& dependency : m_document.dependencies) { for (const Containers::String& dependency : m_document.dependencies) {
size += sizeof(Containers::String) + dependency.Length(); size += sizeof(Containers::String) + dependency.Length();
} }

View File

@@ -4,6 +4,7 @@
set(UI_RESOURCE_TEST_SOURCES set(UI_RESOURCE_TEST_SOURCES
test_ui_document_loader.cpp test_ui_document_loader.cpp
test_ui_schema_document.cpp
) )
add_executable(ui_resource_tests ${UI_RESOURCE_TEST_SOURCES}) add_executable(ui_resource_tests ${UI_RESOURCE_TEST_SOURCES})

View File

@@ -0,0 +1,153 @@
#include <gtest/gtest.h>
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
#include <XCEngine/Resources/UI/UIDocumentLoaders.h>
#include <filesystem>
#include <fstream>
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<bool>(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,
"<Schema name=\"EditorMarkup\">\n"
" <Element tag=\"View\" allowUnknownChildren=\"true\">\n"
" <Attribute name=\"theme\" type=\"document\" kind=\"theme\" />\n"
" <Attribute name=\"mode\" type=\"enum\" values=\"compact, cozy\" />\n"
" <Element tag=\"Column\">\n"
" <Attribute name=\"gap\" type=\"number\" />\n"
" </Element>\n"
" </Element>\n"
"</Schema>\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<UISchema*>(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,
"<Schema>\n"
" <Element tag=\"View\" allowUnknownChildren=\"sometimes\" />\n"
"</Schema>\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,
"<Schema>\n"
" <Element tag=\"View\">\n"
" <Attribute name=\"id\" type=\"string\" />\n"
" <Attribute name=\"id\" type=\"string\" />\n"
" </Element>\n"
"</Schema>\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