diff --git a/CMakeLists.txt b/CMakeLists.txt index dd6f97d..df54432 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,6 +114,18 @@ target_link_libraries(pp_renderer_api PRIVATE pp_project_warnings) +add_library(pp_ui_core STATIC + src/ui_core/layout_value.cpp) +target_include_directories(pp_ui_core + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_link_libraries(pp_ui_core + PUBLIC + pp_foundation + pp_project_options + PRIVATE + pp_project_warnings) + if(PP_BUILD_TOOLS) add_subdirectory(tools/pano_cli) endif() diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 02824c1..327df87 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -75,8 +75,8 @@ Known local toolchain state: - Android NDK: `C:\Users\omara\AppData\Local\Android\Sdk\ndk\29.0.14206865` - 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`, `pano_cli`, and their current - headless test binaries. + `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_ui_core`, `pano_cli`, and + their current headless test binaries. - `vcpkg` is not on PATH yet; see DEBT-0007. Known warnings after the current CMake app build: diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 3562467..b9e41a8 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -307,9 +307,10 @@ PNG/JPEG signature detection and corrupt/truncated/unsupported tests. `pp_paint` has started with CPU reference math for the five current shader blend modes. `pp_document` has started with a pure canvas/layer model and layer invariant tests. `pp_renderer_api` has started with renderer-neutral -texture/readback descriptors and validation tests. Continue expanding document -behavior toward legacy Canvas parity and then port OpenGL classes behind the -renderer boundary. +texture/readback descriptors and validation tests. `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. Implementation tasks: @@ -513,7 +514,7 @@ Last verified on 2026-05-31: ```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 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_ui_core_layout_value_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 @@ -530,6 +531,7 @@ Results: - `pp_paint_blend_tests` passed. - `pp_document_tests` passed. - `pp_renderer_api_tests` passed. +- `pp_ui_core_layout_value_tests` passed. - `pano_cli_create_document_smoke` passed. - `pano_cli_inspect_image_rejects_unsupported` passed as an expected failure test. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 7e6d76a..4f4a064 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param( [string[]]$Presets = @("android-arm64"), - [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "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") + [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "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_ui_core_layout_value_tests") ) $ErrorActionPreference = "Stop" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index b061ccf..01e7844 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u preset="${1:-android-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api 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}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api 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_ui_core_layout_value_tests}" start="$(date +%s)" cmake --preset "$preset" diff --git a/src/ui_core/layout_value.cpp b/src/ui_core/layout_value.cpp new file mode 100644 index 0000000..b6ff5e0 --- /dev/null +++ b/src/ui_core/layout_value.cpp @@ -0,0 +1,57 @@ +#include "ui_core/layout_value.h" + +#include "foundation/parse.h" + +namespace pp::ui { + +pp::foundation::Result parse_layout_length(std::string_view text) noexcept +{ + if (text == "auto") { + return pp::foundation::Result::success( + LayoutLength { .kind = LayoutLengthKind::auto_value, .value = 0 }); + } + + if (text.empty()) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("layout length must not be empty")); + } + + if (text.back() == '%') { + const auto number = pp::foundation::parse_u32(text.substr(0, text.size() - 1U)); + if (!number) { + return pp::foundation::Result::failure(number.status()); + } + + if (number.value() > 100U) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("layout percent must be between 0 and 100")); + } + + return pp::foundation::Result::success( + LayoutLength { .kind = LayoutLengthKind::percent, .value = number.value() }); + } + + const auto pixels = pp::foundation::parse_u32(text); + if (!pixels) { + return pp::foundation::Result::failure(pixels.status()); + } + + return pp::foundation::Result::success( + LayoutLength { .kind = LayoutLengthKind::pixels, .value = pixels.value() }); +} + +const char* layout_length_kind_name(LayoutLengthKind kind) noexcept +{ + switch (kind) { + case LayoutLengthKind::auto_value: + return "auto"; + case LayoutLengthKind::pixels: + return "pixels"; + case LayoutLengthKind::percent: + return "percent"; + } + + return "unknown"; +} + +} diff --git a/src/ui_core/layout_value.h b/src/ui_core/layout_value.h new file mode 100644 index 0000000..6299c9a --- /dev/null +++ b/src/ui_core/layout_value.h @@ -0,0 +1,24 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include + +namespace pp::ui { + +enum class LayoutLengthKind : std::uint8_t { + auto_value, + pixels, + percent, +}; + +struct LayoutLength { + LayoutLengthKind kind = LayoutLengthKind::auto_value; + std::uint32_t value = 0; +}; + +[[nodiscard]] pp::foundation::Result parse_layout_length(std::string_view text) noexcept; +[[nodiscard]] const char* layout_length_kind_name(LayoutLengthKind kind) noexcept; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a59942b..f24bab5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -76,6 +76,16 @@ add_test(NAME pp_renderer_api_tests COMMAND pp_renderer_api_tests) set_tests_properties(pp_renderer_api_tests PROPERTIES LABELS "renderer;desktop-fast") +add_executable(pp_ui_core_layout_value_tests + ui_core/layout_value_tests.cpp) +target_link_libraries(pp_ui_core_layout_value_tests PRIVATE + pp_ui_core + pp_test_harness) + +add_test(NAME pp_ui_core_layout_value_tests COMMAND pp_ui_core_layout_value_tests) +set_tests_properties(pp_ui_core_layout_value_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) diff --git a/tests/ui_core/layout_value_tests.cpp b/tests/ui_core/layout_value_tests.cpp new file mode 100644 index 0000000..139e387 --- /dev/null +++ b/tests/ui_core/layout_value_tests.cpp @@ -0,0 +1,58 @@ +#include "ui_core/layout_value.h" +#include "test_harness.h" + +#include + +using pp::foundation::StatusCode; +using pp::ui::LayoutLengthKind; +using pp::ui::layout_length_kind_name; +using pp::ui::parse_layout_length; + +namespace { + +void parses_auto_pixels_and_percent(pp::tests::Harness& h) +{ + const auto auto_value = parse_layout_length("auto"); + const auto pixels = parse_layout_length("28"); + const auto percent = parse_layout_length("100%"); + + PP_EXPECT(h, auto_value.ok()); + PP_EXPECT(h, auto_value.value().kind == LayoutLengthKind::auto_value); + PP_EXPECT(h, pixels.ok()); + PP_EXPECT(h, pixels.value().kind == LayoutLengthKind::pixels); + PP_EXPECT(h, pixels.value().value == 28U); + PP_EXPECT(h, percent.ok()); + PP_EXPECT(h, percent.value().kind == LayoutLengthKind::percent); + PP_EXPECT(h, percent.value().value == 100U); + PP_EXPECT(h, layout_length_kind_name(LayoutLengthKind::percent) == std::string_view("percent")); +} + +void rejects_invalid_layout_lengths(pp::tests::Harness& h) +{ + const auto empty = parse_layout_length(""); + const auto negative = parse_layout_length("-1"); + const auto trailing = parse_layout_length("28px"); + const auto too_large_percent = parse_layout_length("101%"); + const auto bare_percent = parse_layout_length("%"); + + PP_EXPECT(h, !empty.ok()); + PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !negative.ok()); + PP_EXPECT(h, negative.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !trailing.ok()); + PP_EXPECT(h, trailing.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !too_large_percent.ok()); + PP_EXPECT(h, too_large_percent.status().code == StatusCode::out_of_range); + PP_EXPECT(h, !bare_percent.ok()); + PP_EXPECT(h, bare_percent.status().code == StatusCode::invalid_argument); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("parses_auto_pixels_and_percent", parses_auto_pixels_and_percent); + harness.run("rejects_invalid_layout_lengths", rejects_invalid_layout_lengths); + return harness.finish(); +}