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) pp_project_warnings)
add_library(pp_ui_core STATIC add_library(pp_ui_core STATIC
src/ui_core/color.cpp
src/ui_core/layout_value.cpp src/ui_core/layout_value.cpp
src/ui_core/layout_xml.cpp) src/ui_core/layout_xml.cpp)
target_include_directories(pp_ui_core 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_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 foundation event/logging/task queue coverage, PPI header, settings 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 - `panopainter_validate_shaders` validates the current combined GLSL shader
files for one vertex stage marker, one fragment stage marker, valid marker files for one vertex stage marker, one fragment stage marker, valid marker
order, and existing relative includes. 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 texture/readback descriptors and validation tests. `pp_paint_renderer` has
started with deterministic CPU layer compositing over renderer extents using started with deterministic CPU layer compositing over renderer extents using
the paint blend reference. `pp_ui_core` has started with XML-layout-facing 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 `pano_cli parse-layout` now exercises that path. Continue expanding document
behavior toward legacy Canvas parity and then port OpenGL classes behind the behavior toward legacy Canvas parity and then port OpenGL classes behind the
renderer boundary. renderer boundary.
@@ -525,7 +526,7 @@ Last verified on 2026-06-01:
```powershell ```powershell
cmake --preset windows-msvc-default 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 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\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli 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_document_tests` passed.
- `pp_renderer_api_tests` passed. - `pp_renderer_api_tests` passed.
- `pp_paint_renderer_compositor_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_value_tests` passed.
- `pp_ui_core_layout_xml_tests` passed. - `pp_ui_core_layout_xml_tests` passed.
- `pano_cli_create_document_smoke` passed. - `pano_cli_create_document_smoke` passed.

View File

@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[string[]]$Presets = @("android-arm64"), [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" $ErrorActionPreference = "Stop"

View File

@@ -3,7 +3,7 @@ set -u
preset="${1:-android-arm64}" preset="${1:-android-arm64}"
shift || true 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)" start="$(date +%s)"
cmake --preset "$preset" 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 set_tests_properties(pp_paint_renderer_compositor_tests PROPERTIES
LABELS "renderer;paint;desktop-fast") 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 add_executable(pp_ui_core_layout_value_tests
ui_core/layout_value_tests.cpp) ui_core/layout_value_tests.cpp)
target_link_libraries(pp_ui_core_layout_value_tests PRIVATE 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();
}