diff --git a/CMakeLists.txt b/CMakeLists.txt index 676e4f5..87e2525 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,6 +150,7 @@ target_link_libraries(pp_paint_renderer pp_project_warnings) add_library(pp_ui_core STATIC + src/ui_core/color.cpp src/ui_core/layout_value.cpp src/ui_core/layout_xml.cpp) target_include_directories(pp_ui_core diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 2749503..3aa1ade 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -81,7 +81,8 @@ Known local toolchain state: `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, `pp_ui_core`, `pano_cli`, and their current headless test binaries, including foundation event/logging/task queue coverage, PPI header, settings - document, paint stroke sampling, and layout XML parse coverage. + document, paint stroke sampling, UI color parsing, and layout XML parse + coverage. - `panopainter_validate_shaders` validates the current combined GLSL shader files for one vertex stage marker, one fragment stage marker, valid marker order, and existing relative includes. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 2073166..27e8884 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -317,7 +317,8 @@ layer/frame/undo-redo history invariant tests. `pp_renderer_api` has started wit 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, tinyxml-backed layout XML parsing, and invalid input tests. +length parsing, color 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. @@ -525,7 +526,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_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_blend_tests pp_paint_stroke_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 +cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_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 @@ -552,6 +553,7 @@ Results: - `pp_document_tests` passed. - `pp_renderer_api_tests` passed. - `pp_paint_renderer_compositor_tests` passed. +- `pp_ui_core_color_tests` passed. - `pp_ui_core_layout_value_tests` passed. - `pp_ui_core_layout_xml_tests` passed. - `pano_cli_create_document_smoke` passed. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index e884394..578ac60 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", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_blend_tests", "pp_paint_stroke_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") + [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_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_color_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests") ) $ErrorActionPreference = "Stop" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index 6c40ec4..6b69024 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 pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_blend_tests pp_paint_stroke_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}" +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_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_color_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}" start="$(date +%s)" cmake --preset "$preset" diff --git a/src/ui_core/color.cpp b/src/ui_core/color.cpp new file mode 100644 index 0000000..cb7672c --- /dev/null +++ b/src/ui_core/color.cpp @@ -0,0 +1,95 @@ +#include "ui_core/color.h" + +namespace pp::ui { + +namespace { + +[[nodiscard]] int hex_value(char ch) noexcept +{ + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + if (ch >= 'a' && ch <= 'f') { + return 10 + (ch - 'a'); + } + if (ch >= 'A' && ch <= 'F') { + return 10 + (ch - 'A'); + } + return -1; +} + +[[nodiscard]] pp::foundation::Result parse_hex_byte(std::string_view value) noexcept +{ + const auto high = hex_value(value[0]); + const auto low = hex_value(value[1]); + if (high < 0 || low < 0) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("color contains a non-hex character")); + } + + return pp::foundation::Result::success( + static_cast((high << 4) | low)); +} + +[[nodiscard]] pp::foundation::Result parse_hex_nibble(char value) noexcept +{ + const auto nibble = hex_value(value); + if (nibble < 0) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("color contains a non-hex character")); + } + + return pp::foundation::Result::success( + static_cast((nibble << 4) | nibble)); +} + +} + +pp::foundation::Result parse_hex_color(std::string_view value) noexcept +{ + if (value.empty()) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("color must not be empty")); + } + + if (value.front() != '#') { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("color must start with #")); + } + + const auto hex = value.substr(1); + if (hex.size() != 3U && hex.size() != 4U && hex.size() != 6U && hex.size() != 8U) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("color must use #rgb, #rgba, #rrggbb, or #rrggbbaa")); + } + + ColorRgba8 color; + if (hex.size() == 3U || hex.size() == 4U) { + const auto r = parse_hex_nibble(hex[0]); + const auto g = parse_hex_nibble(hex[1]); + const auto b = parse_hex_nibble(hex[2]); + const auto a = hex.size() == 4U ? parse_hex_nibble(hex[3]) + : pp::foundation::Result::success(255); + if (!r || !g || !b || !a) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("color contains a non-hex character")); + } + color = ColorRgba8 { .r = r.value(), .g = g.value(), .b = b.value(), .a = a.value() }; + return pp::foundation::Result::success(color); + } + + const auto r = parse_hex_byte(hex.substr(0, 2)); + const auto g = parse_hex_byte(hex.substr(2, 2)); + const auto b = parse_hex_byte(hex.substr(4, 2)); + const auto a = hex.size() == 8U ? parse_hex_byte(hex.substr(6, 2)) + : pp::foundation::Result::success(255); + if (!r || !g || !b || !a) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("color contains a non-hex character")); + } + + color = ColorRgba8 { .r = r.value(), .g = g.value(), .b = b.value(), .a = a.value() }; + return pp::foundation::Result::success(color); +} + +} diff --git a/src/ui_core/color.h b/src/ui_core/color.h new file mode 100644 index 0000000..d4fa7c0 --- /dev/null +++ b/src/ui_core/color.h @@ -0,0 +1,19 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include + +namespace pp::ui { + +struct ColorRgba8 { + std::uint8_t r = 0; + std::uint8_t g = 0; + std::uint8_t b = 0; + std::uint8_t a = 255; +}; + +[[nodiscard]] pp::foundation::Result parse_hex_color(std::string_view value) noexcept; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0c9c57e..526b937 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -146,6 +146,16 @@ add_test(NAME pp_paint_renderer_compositor_tests COMMAND pp_paint_renderer_compo set_tests_properties(pp_paint_renderer_compositor_tests PROPERTIES LABELS "renderer;paint;desktop-fast") +add_executable(pp_ui_core_color_tests + ui_core/color_tests.cpp) +target_link_libraries(pp_ui_core_color_tests PRIVATE + pp_ui_core + pp_test_harness) + +add_test(NAME pp_ui_core_color_tests COMMAND pp_ui_core_color_tests) +set_tests_properties(pp_ui_core_color_tests PROPERTIES + LABELS "ui;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 diff --git a/tests/ui_core/color_tests.cpp b/tests/ui_core/color_tests.cpp new file mode 100644 index 0000000..4ffeb2c --- /dev/null +++ b/tests/ui_core/color_tests.cpp @@ -0,0 +1,71 @@ +#include "test_harness.h" +#include "ui_core/color.h" + +using pp::foundation::StatusCode; +using pp::ui::parse_hex_color; + +namespace { + +void parses_short_and_long_rgb_forms(pp::tests::Harness& h) +{ + const auto short_rgb = parse_hex_color("#0f8"); + const auto long_rgb = parse_hex_color("#102aFF"); + + PP_EXPECT(h, short_rgb.ok()); + PP_EXPECT(h, short_rgb.value().r == 0x00); + PP_EXPECT(h, short_rgb.value().g == 0xff); + PP_EXPECT(h, short_rgb.value().b == 0x88); + PP_EXPECT(h, short_rgb.value().a == 0xff); + + PP_EXPECT(h, long_rgb.ok()); + PP_EXPECT(h, long_rgb.value().r == 0x10); + PP_EXPECT(h, long_rgb.value().g == 0x2a); + PP_EXPECT(h, long_rgb.value().b == 0xff); + PP_EXPECT(h, long_rgb.value().a == 0xff); +} + +void parses_alpha_forms(pp::tests::Harness& h) +{ + const auto short_rgba = parse_hex_color("#1234"); + const auto long_rgba = parse_hex_color("#11223344"); + + PP_EXPECT(h, short_rgba.ok()); + PP_EXPECT(h, short_rgba.value().r == 0x11); + PP_EXPECT(h, short_rgba.value().g == 0x22); + PP_EXPECT(h, short_rgba.value().b == 0x33); + PP_EXPECT(h, short_rgba.value().a == 0x44); + + PP_EXPECT(h, long_rgba.ok()); + PP_EXPECT(h, long_rgba.value().r == 0x11); + PP_EXPECT(h, long_rgba.value().g == 0x22); + PP_EXPECT(h, long_rgba.value().b == 0x33); + PP_EXPECT(h, long_rgba.value().a == 0x44); +} + +void rejects_invalid_colors(pp::tests::Harness& h) +{ + const auto empty = parse_hex_color(""); + const auto missing_hash = parse_hex_color("112233"); + const auto bad_length = parse_hex_color("#12"); + const auto bad_character = parse_hex_color("#12xz45"); + + PP_EXPECT(h, !empty.ok()); + PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_hash.ok()); + PP_EXPECT(h, missing_hash.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_length.ok()); + PP_EXPECT(h, bad_length.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_character.ok()); + PP_EXPECT(h, bad_character.status().code == StatusCode::invalid_argument); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("parses_short_and_long_rgb_forms", parses_short_and_long_rgb_forms); + harness.run("parses_alpha_forms", parses_alpha_forms); + harness.run("rejects_invalid_colors", rejects_invalid_colors); + return harness.finish(); +}