Add UI layout XML automation

This commit is contained in:
2026-06-01 00:21:23 +02:00
parent dfdb7a4468
commit 20b5dba41e
13 changed files with 245 additions and 9 deletions

View File

@@ -46,6 +46,15 @@ target_compile_features(pp_project_options INTERFACE cxx_std_23)
add_library(pp_project_warnings INTERFACE)
pp_configure_project_warnings(pp_project_warnings)
add_library(pp_vendor_tinyxml2 STATIC
libs/tinyxml2/tinyxml2.cpp)
target_include_directories(pp_vendor_tinyxml2
SYSTEM PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/libs/tinyxml2")
target_link_libraries(pp_vendor_tinyxml2
PUBLIC
pp_project_options)
add_custom_target(panopainter_modernization_status
COMMAND "${CMAKE_COMMAND}" -E echo "PanoPainter modernization scaffold configured."
COMMAND "${CMAKE_COMMAND}" -E echo "Roadmap: docs/modernization/roadmap.md"
@@ -129,7 +138,8 @@ target_link_libraries(pp_paint_renderer
pp_project_warnings)
add_library(pp_ui_core STATIC
src/ui_core/layout_value.cpp)
src/ui_core/layout_value.cpp
src/ui_core/layout_xml.cpp)
target_include_directories(pp_ui_core
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")
@@ -138,6 +148,7 @@ target_link_libraries(pp_ui_core
pp_foundation
pp_project_options
PRIVATE
pp_vendor_tinyxml2
pp_project_warnings)
if(PP_BUILD_TOOLS)

View File

@@ -77,7 +77,8 @@ Known local toolchain state:
- Android arm64 headless configure/build passes through root CMake and the
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
`pp_ui_core`, `pano_cli`, and their current headless test binaries.
`pp_ui_core`, `pano_cli`, and their current headless test binaries,
including layout XML parse coverage.
- `vcpkg` is not on PATH yet; see DEBT-0007.
Known warnings after the current CMake app build:

View File

@@ -30,6 +30,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, not APK/package variants | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands |
| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document model but is not yet wired to legacy `Canvas`, PPI load/save, selection masks, or undo/redo | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2` | Legacy document behavior is represented by `pp_document` tests and the app consumes it through a boundary/facade |
| DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact only, not AppX/APK/Apple/WebGL package outputs | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Package-smoke covers Windows AppX, Android APK variants, Apple bundles, and WebGL output where local toolchains are present |
| DEBT-0012 | Open | Modernization | `pp_vendor_tinyxml2` compiles the retained vendored tinyxml2 copy for `pp_ui_core` layout parsing | vcpkg is not validated yet, but layout parsing needs a structured XML parser now | `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | Replace with vcpkg tinyxml2 target once desktop and mobile triplets are validated |
## Closed Debt

View File

@@ -311,9 +311,10 @@ and layer/frame invariant tests. `pp_renderer_api` has started with renderer-neu
texture/readback descriptors and validation tests. `pp_paint_renderer` has
started with deterministic CPU layer compositing over renderer extents using
the paint blend reference. `pp_ui_core` has started with XML-layout-facing
length parsing and invalid input tests. Continue expanding document behavior
toward legacy Canvas parity and then port OpenGL classes behind the renderer
boundary.
length parsing, tinyxml-backed layout XML parsing, and invalid input tests.
`pano_cli parse-layout` now exercises that path. Continue expanding document
behavior toward legacy Canvas parity and then port OpenGL classes behind the
renderer boundary.
Implementation tasks:
@@ -517,7 +518,7 @@ Last verified on 2026-06-01:
```powershell
cmake --preset windows-msvc-default
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pano_cli PanoPainter
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter
ctest --preset desktop-fast --build-config Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
@@ -537,9 +538,11 @@ Results:
- `pp_renderer_api_tests` passed.
- `pp_paint_renderer_compositor_tests` passed.
- `pp_ui_core_layout_value_tests` passed.
- `pp_ui_core_layout_xml_tests` passed.
- `pano_cli_create_document_smoke` passed.
- `pano_cli_inspect_image_rejects_unsupported` passed as an expected failure
test.
- `pano_cli_parse_layout_smoke` passed.
- `PanoPainter.exe` built through CMake at
`out/build/windows-msvc-default/Debug/PanoPainter.exe`.
- PowerShell build/test automation wrappers return JSON summaries and passed

View File

@@ -1,7 +1,7 @@
[CmdletBinding()]
param(
[string[]]$Presets = @("android-arm64"),
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_paint_blend_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests")
[string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_paint_blend_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests")
)
$ErrorActionPreference = "Stop"

View File

@@ -3,7 +3,7 @@ set -u
preset="${1:-android-arm64}"
shift || true
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests}"
targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}"
start="$(date +%s)"
cmake --preset "$preset"

View File

@@ -0,0 +1,71 @@
#include "ui_core/layout_xml.h"
#include "ui_core/layout_value.h"
#include <tinyxml2.h>
namespace pp::ui {
namespace {
[[nodiscard]] pp::foundation::Status visit_element(const tinyxml2::XMLElement& element, LayoutParseSummary& summary)
{
++summary.node_count;
for (const char* name : { "width", "height" }) {
const char* value = element.Attribute(name);
if (value == nullptr) {
continue;
}
const auto length = parse_layout_length(value);
if (!length) {
return length.status();
}
++summary.length_attribute_count;
}
for (const tinyxml2::XMLElement* child = element.FirstChildElement();
child != nullptr;
child = child->NextSiblingElement()) {
const auto status = visit_element(*child, summary);
if (!status.ok()) {
return status;
}
}
return pp::foundation::Status::success();
}
}
pp::foundation::Result<LayoutParseSummary> parse_layout_xml(std::string_view xml)
{
if (xml.empty()) {
return pp::foundation::Result<LayoutParseSummary>::failure(
pp::foundation::Status::invalid_argument("layout XML must not be empty"));
}
tinyxml2::XMLDocument document;
const auto error = document.Parse(xml.data(), xml.size());
if (error != tinyxml2::XML_SUCCESS) {
return pp::foundation::Result<LayoutParseSummary>::failure(
pp::foundation::Status::invalid_argument("layout XML could not be parsed"));
}
const tinyxml2::XMLElement* root = document.RootElement();
if (root == nullptr) {
return pp::foundation::Result<LayoutParseSummary>::failure(
pp::foundation::Status::invalid_argument("layout XML has no root element"));
}
LayoutParseSummary summary;
const auto status = visit_element(*root, summary);
if (!status.ok()) {
return pp::foundation::Result<LayoutParseSummary>::failure(status);
}
return pp::foundation::Result<LayoutParseSummary>::success(summary);
}
}

17
src/ui_core/layout_xml.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <string_view>
namespace pp::ui {
struct LayoutParseSummary {
std::size_t node_count = 0;
std::size_t length_attribute_count = 0;
};
[[nodiscard]] pp::foundation::Result<LayoutParseSummary> parse_layout_xml(std::string_view xml);
}

View File

@@ -96,6 +96,16 @@ add_test(NAME pp_ui_core_layout_value_tests COMMAND pp_ui_core_layout_value_test
set_tests_properties(pp_ui_core_layout_value_tests PROPERTIES
LABELS "ui;desktop-fast")
add_executable(pp_ui_core_layout_xml_tests
ui_core/layout_xml_tests.cpp)
target_link_libraries(pp_ui_core_layout_xml_tests PRIVATE
pp_ui_core
pp_test_harness)
add_test(NAME pp_ui_core_layout_xml_tests COMMAND pp_ui_core_layout_xml_tests)
set_tests_properties(pp_ui_core_layout_xml_tests PROPERTIES
LABELS "ui;desktop-fast")
if(TARGET pano_cli)
add_test(NAME pano_cli_create_document_smoke
COMMAND pano_cli create-document --width 64 --height 32 --layers 2)
@@ -107,4 +117,9 @@ if(TARGET pano_cli)
set_tests_properties(pano_cli_inspect_image_rejects_unsupported PROPERTIES
LABELS "assets;integration;desktop-fast"
WILL_FAIL TRUE)
add_test(NAME pano_cli_parse_layout_smoke
COMMAND pano_cli parse-layout --path "${CMAKE_CURRENT_SOURCE_DIR}/data/layouts/simple-layout.xml")
set_tests_properties(pano_cli_parse_layout_smoke PROPERTIES
LABELS "ui;integration;desktop-fast")
endif()

View File

@@ -0,0 +1,5 @@
<layout width="100%" height="auto">
<panel width="320" height="200">
<button width="64" height="28"/>
</panel>
</layout>

View File

@@ -0,0 +1,47 @@
#include "ui_core/layout_xml.h"
#include "test_harness.h"
using pp::foundation::StatusCode;
using pp::ui::parse_layout_xml;
namespace {
void parses_nested_layout_xml_and_lengths(pp::tests::Harness& h)
{
constexpr auto xml =
"<layout width=\"100%\" height=\"auto\">"
" <panel width=\"320\" height=\"200\">"
" <button width=\"64\" height=\"28\"/>"
" </panel>"
"</layout>";
const auto summary = parse_layout_xml(xml);
PP_EXPECT(h, summary.ok());
PP_EXPECT(h, summary.value().node_count == 3U);
PP_EXPECT(h, summary.value().length_attribute_count == 6U);
}
void rejects_malformed_empty_and_invalid_lengths(pp::tests::Harness& h)
{
const auto empty = parse_layout_xml("");
const auto malformed = parse_layout_xml("<layout><panel></layout>");
const auto invalid_length = parse_layout_xml("<layout width=\"101%\"/>");
PP_EXPECT(h, !empty.ok());
PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !malformed.ok());
PP_EXPECT(h, malformed.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !invalid_length.ok());
PP_EXPECT(h, invalid_length.status().code == StatusCode::out_of_range);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("parses_nested_layout_xml_and_lengths", parses_nested_layout_xml_and_lengths);
harness.run("rejects_malformed_empty_and_invalid_lengths", rejects_malformed_empty_and_invalid_lengths);
return harness.finish();
}

View File

@@ -5,4 +5,5 @@ target_link_libraries(pano_cli PRIVATE
pp_project_warnings
pp_foundation
pp_assets
pp_document)
pp_document
pp_ui_core)

View File

@@ -2,6 +2,7 @@
#include "document/document.h"
#include "foundation/parse.h"
#include "foundation/result.h"
#include "ui_core/layout_xml.h"
#include <cstdint>
#include <fstream>
@@ -23,6 +24,10 @@ struct InspectImageArgs {
std::string path;
};
struct ParseLayoutArgs {
std::string path;
};
void print_error(std::string_view command, std::string_view message)
{
std::cout << "{\"ok\":false,\"command\":\"" << command
@@ -35,6 +40,7 @@ void print_help()
<< "pano_cli commands:\n"
<< " create-document --width N --height N [--layers N]\n"
<< " inspect-image --path FILE\n"
<< " parse-layout --path FILE\n"
<< " --help\n";
}
@@ -160,6 +166,60 @@ int inspect_image(int argc, char** argv)
return 0;
}
pp::foundation::Status parse_layout_args(int argc, char** argv, ParseLayoutArgs& args)
{
for (int i = 2; i < argc; ++i) {
const std::string_view key(argv[i]);
if (key == "--path") {
if (i + 1 >= argc) {
return pp::foundation::Status::invalid_argument("missing value for option");
}
args.path = argv[++i];
} else {
return pp::foundation::Status::invalid_argument("unknown option");
}
}
if (args.path.empty()) {
return pp::foundation::Status::invalid_argument("path must not be empty");
}
return pp::foundation::Status::success();
}
int parse_layout(int argc, char** argv)
{
ParseLayoutArgs args;
const auto status = parse_layout_args(argc, argv, args);
if (!status.ok()) {
print_error("parse-layout", status.message);
return 2;
}
std::ifstream stream(args.path, std::ios::binary);
if (!stream) {
print_error("parse-layout", "layout file could not be opened");
return 2;
}
const std::string xml {
std::istreambuf_iterator<char>(stream),
std::istreambuf_iterator<char>()
};
const auto summary = pp::ui::parse_layout_xml(xml);
if (!summary) {
print_error("parse-layout", summary.status().message);
return 2;
}
std::cout << "{\"ok\":true,\"command\":\"parse-layout\""
<< ",\"nodes\":" << summary.value().node_count
<< ",\"lengthAttributes\":" << summary.value().length_attribute_count
<< "}\n";
return 0;
}
}
int main(int argc, char** argv)
@@ -183,6 +243,10 @@ int main(int argc, char** argv)
return inspect_image(argc, argv);
}
if (command == "parse-layout") {
return parse_layout(argc, argv);
}
print_error(command, "unknown command");
return 2;
}