Add UI layout XML automation
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
71
src/ui_core/layout_xml.cpp
Normal file
71
src/ui_core/layout_xml.cpp
Normal 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
17
src/ui_core/layout_xml.h
Normal 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);
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
5
tests/data/layouts/simple-layout.xml
Normal file
5
tests/data/layouts/simple-layout.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<layout width="100%" height="auto">
|
||||
<panel width="320" height="200">
|
||||
<button width="64" height="28"/>
|
||||
</panel>
|
||||
</layout>
|
||||
47
tests/ui_core/layout_xml_tests.cpp
Normal file
47
tests/ui_core/layout_xml_tests.cpp
Normal 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();
|
||||
}
|
||||
@@ -5,4 +5,5 @@ target_link_libraries(pano_cli PRIVATE
|
||||
pp_project_warnings
|
||||
pp_foundation
|
||||
pp_assets
|
||||
pp_document)
|
||||
pp_document
|
||||
pp_ui_core)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user