Add UI core color parser tests

This commit is contained in:
2026-06-01 08:38:05 +02:00
parent 551013c771
commit 313a360c01
9 changed files with 204 additions and 5 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

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_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"

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_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"

95
src/ui_core/color.cpp Normal file
View File

@@ -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<std::uint8_t> 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<std::uint8_t>::failure(
pp::foundation::Status::invalid_argument("color contains a non-hex character"));
}
return pp::foundation::Result<std::uint8_t>::success(
static_cast<std::uint8_t>((high << 4) | low));
}
[[nodiscard]] pp::foundation::Result<std::uint8_t> parse_hex_nibble(char value) noexcept
{
const auto nibble = hex_value(value);
if (nibble < 0) {
return pp::foundation::Result<std::uint8_t>::failure(
pp::foundation::Status::invalid_argument("color contains a non-hex character"));
}
return pp::foundation::Result<std::uint8_t>::success(
static_cast<std::uint8_t>((nibble << 4) | nibble));
}
}
pp::foundation::Result<ColorRgba8> parse_hex_color(std::string_view value) noexcept
{
if (value.empty()) {
return pp::foundation::Result<ColorRgba8>::failure(
pp::foundation::Status::invalid_argument("color must not be empty"));
}
if (value.front() != '#') {
return pp::foundation::Result<ColorRgba8>::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<ColorRgba8>::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<std::uint8_t>::success(255);
if (!r || !g || !b || !a) {
return pp::foundation::Result<ColorRgba8>::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<ColorRgba8>::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<std::uint8_t>::success(255);
if (!r || !g || !b || !a) {
return pp::foundation::Result<ColorRgba8>::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<ColorRgba8>::success(color);
}
}

19
src/ui_core/color.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "foundation/result.h"
#include <cstdint>
#include <string_view>
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<ColorRgba8> parse_hex_color(std::string_view value) noexcept;
}

View File

@@ -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

View File

@@ -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();
}