Add XCUI schema document regression coverage
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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' };
|
||||
|
||||
@@ -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<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 {
|
||||
Containers::String tagName;
|
||||
Containers::Array<UIDocumentAttribute> attributes;
|
||||
@@ -58,6 +128,7 @@ struct UIDocumentModel {
|
||||
Containers::String sourcePath;
|
||||
Containers::String displayName;
|
||||
UIDocumentNode rootNode;
|
||||
UISchemaDefinition schemaDefinition;
|
||||
Containers::Array<Containers::String> dependencies;
|
||||
Containers::Array<UIDocumentDiagnostic> diagnostics;
|
||||
bool valid = false;
|
||||
@@ -66,6 +137,7 @@ struct UIDocumentModel {
|
||||
sourcePath.Clear();
|
||||
displayName.Clear();
|
||||
rootNode = UIDocumentNode();
|
||||
schemaDefinition.Clear();
|
||||
dependencies.Clear();
|
||||
diagnostics.Clear();
|
||||
valid = false;
|
||||
|
||||
@@ -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<Containers::String>& GetDependencies() const { return m_document.dependencies; }
|
||||
const Containers::Array<UIDocumentDiagnostic>& GetDiagnostics() const { return m_document.diagnostics; }
|
||||
const Containers::String& GetSourcePath() const { return m_document.sourcePath; }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
153
tests/Resources/UI/test_ui_schema_document.cpp
Normal file
153
tests/Resources/UI/test_ui_schema_document.cpp
Normal 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
|
||||
Reference in New Issue
Block a user