diff --git a/include/omath/utility/color.hpp b/include/omath/utility/color.hpp index 428225a..b815419 100644 --- a/include/omath/utility/color.hpp +++ b/include/omath/utility/color.hpp @@ -175,6 +175,16 @@ namespace omath static_cast(m_value.z * 255.f), static_cast(m_value.w * 255.f)); } + [[nodiscard]] std::string to_rgbf_string() const noexcept + { + return std::format("[r:{}, g:{}, b:{}, a:{}]", + m_value.x, m_value.y, m_value.z, m_value.w); + } + [[nodiscard]] std::string to_hsv_string() const noexcept + { + const auto [hue, saturation, value] = to_hsv(); + return std::format("[h:{}, s:{}, v:{}]", hue, saturation, value); + } [[nodiscard]] std::wstring to_wstring() const noexcept { const auto ascii_string = to_string(); @@ -192,23 +202,55 @@ namespace omath template<> struct std::formatter // NOLINT(*-dcl58-cpp) { - [[nodiscard]] - static constexpr auto parse(const std::format_parse_context& ctx) + enum class ColorFormat { rgb, rgbf, hsv }; + ColorFormat color_format = ColorFormat::rgb; + + constexpr auto parse(std::format_parse_context& ctx) { - return ctx.begin(); + auto it = ctx.begin(); + const auto end = ctx.end(); + + if (it == end || *it == '}') + return it; + + const std::string_view spec(it, end); + + if (spec.starts_with("rgbf")) + { + color_format = ColorFormat::rgbf; + return it + 4; + } + if (spec.starts_with("rgb")) + { + color_format = ColorFormat::rgb; + return it + 3; + } + if (spec.starts_with("hsv")) + { + color_format = ColorFormat::hsv; + return it + 3; + } + + throw std::format_error("Invalid format specifier for omath::Color. Use rgb, rgbf, or hsv."); } template - [[nodiscard]] - static auto format(const omath::Color& col, FormatContext& ctx) + auto format(const omath::Color& col, FormatContext& ctx) const { - if constexpr (std::is_same_v) - return std::format_to(ctx.out(), "{}", col.to_string()); - if constexpr (std::is_same_v) - return std::format_to(ctx.out(), L"{}", col.to_wstring()); + std::string str; + switch (color_format) + { + case ColorFormat::rgb: str = col.to_string(); break; + case ColorFormat::rgbf: str = col.to_rgbf_string(); break; + case ColorFormat::hsv: str = col.to_hsv_string(); break; + } + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), "{}", str); + if constexpr (std::is_same_v) + return std::format_to(ctx.out(), L"{}", std::wstring(str.cbegin(), str.cend())); if constexpr (std::is_same_v) - return std::format_to(ctx.out(), u8"{}", col.to_u8string()); + return std::format_to(ctx.out(), u8"{}", std::u8string(str.cbegin(), str.cend())); std::unreachable(); } diff --git a/tests/general/unit_test_color_grouped.cpp b/tests/general/unit_test_color_grouped.cpp index a3e6db9..6253707 100644 --- a/tests/general/unit_test_color_grouped.cpp +++ b/tests/general/unit_test_color_grouped.cpp @@ -291,3 +291,35 @@ TEST(UnitTestColorGrouped_More2, FormatterUsesToString) const auto formatted = std::format("{}", c); EXPECT_NE(formatted.find("r:10"), std::string::npos); } + +TEST(UnitTestColorGrouped_More2, FormatterRgb) +{ + constexpr Color c = Color::from_rgba(255, 128, 0, 64); + const auto s = std::format("{:rgb}", c); + EXPECT_NE(s.find("r:255"), std::string::npos); + EXPECT_NE(s.find("g:128"), std::string::npos); + EXPECT_NE(s.find("b:0"), std::string::npos); + EXPECT_NE(s.find("a:64"), std::string::npos); +} + +TEST(UnitTestColorGrouped_More2, FormatterRgbf) +{ + constexpr Color c(0.5f, 0.25f, 1.0f, 0.75f); + const auto s = std::format("{:rgbf}", c); + EXPECT_NE(s.find("r:"), std::string::npos); + EXPECT_NE(s.find("g:"), std::string::npos); + EXPECT_NE(s.find("b:"), std::string::npos); + EXPECT_NE(s.find("a:"), std::string::npos); + // Values should be in [0,1] float range, not 0-255 + EXPECT_EQ(s.find("r:127"), std::string::npos); + EXPECT_EQ(s.find("r:255"), std::string::npos); +} + +TEST(UnitTestColorGrouped_More2, FormatterHsv) +{ + const Color c = Color::red(); + const auto s = std::format("{:hsv}", c); + EXPECT_NE(s.find("h:"), std::string::npos); + EXPECT_NE(s.find("s:"), std::string::npos); + EXPECT_NE(s.find("v:"), std::string::npos); +}