diff --git a/.clang-tidy b/.clang-tidy deleted file mode 100644 index 812bcaed5ae..00000000000 --- a/.clang-tidy +++ /dev/null @@ -1,6 +0,0 @@ ---- -Checks: 'clang-diagnostic-*,clang-analyzer-*,portability-*,performance-*,bugprone-*,misc-*,-misc-unused-parameters' -WarningsAsErrors: '*,-bugprone-parent-virtual-call,-clang-analyzer-optin.cplusplus.VirtualCall' -HeaderFilterRegex: 'src/.*hpp' -AnalyzeTemporaryDtors: true -... diff --git a/CMakeLists.txt b/CMakeLists.txt index ac11a34ac72..269a7d1c088 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ # make # -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.5) ## Project name to use as command prefix. diff --git a/src/gui/item_colorchannel_rgba.cpp b/src/gui/item_colorchannel_rgba.cpp index fae55b8639a..2d072f3756d 100644 --- a/src/gui/item_colorchannel_rgba.cpp +++ b/src/gui/item_colorchannel_rgba.cpp @@ -198,4 +198,4 @@ ItemColorChannelRGBA::get_color() const return m_channel; } -/* EOF */ +/* EOF */ \ No newline at end of file diff --git a/src/gui/item_colorchannel_rgba.hpp b/src/gui/item_colorchannel_rgba.hpp index c7264a0175d..1cf3b024ace 100644 --- a/src/gui/item_colorchannel_rgba.hpp +++ b/src/gui/item_colorchannel_rgba.hpp @@ -67,4 +67,4 @@ class ItemColorChannelRGBA final : public MenuItem #endif -/* EOF */ +/* EOF */ \ No newline at end of file diff --git a/src/gui/item_colorchannel_saturation.cpp b/src/gui/item_colorchannel_saturation.cpp new file mode 100644 index 00000000000..9bdb543ece2 --- /dev/null +++ b/src/gui/item_colorchannel_saturation.cpp @@ -0,0 +1,209 @@ +// SuperTux +// Copyright (C) 2015 Hume2 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "gui/item_colorchannel_saturation.hpp" + +#include + +#include "math/util.hpp" +#include "supertux/resources.hpp" +#include "video/drawing_context.hpp" + +namespace { + +std::string sat_value_to_string(float v_raw) +{ + float percent = v_raw * 100.0f; + // Round to nearest integer percentage + percent = 0.01f * floorf(percent * 100.0f + 0.5f); + std::ostringstream os; + os << v_raw << " (" << percent << " %" << ")"; + return os.str(); +} + +std::string float_to_string(float v) +{ + std::ostringstream os; + os << v; + return os.str(); +} + +} // namespace + +ItemColorChannelSaturation::ItemColorChannelSaturation(Color* color, ColorOKLCh* okl, int id) : + MenuItem(sat_value_to_string(okl->C), id), + m_color(color), + m_okl(okl), + m_sat(&okl->C), + m_sat_prev(okl->C), + m_edit_mode(false), + m_flickw(static_cast(Resources::normal_font->get_text_width("_"))) + //m_channel(channel) +{ +} + +void +ItemColorChannelSaturation::draw(DrawingContext& context, const Vector& pos, + int menu_width, bool active) +{ + if (!m_edit_mode && *m_sat != m_sat_prev) { + set_text(sat_value_to_string(*m_sat)); + m_sat_prev = *m_sat; + } + + MenuItem::draw(context, pos, menu_width, active); + *m_okl = ColorOKLCh(*m_color); + float maxC = m_okl->get_max_chroma(); + float fraction = 0.0f; + if (maxC > 0.0f) fraction = std::clamp((*m_sat / 1.0f), 0.0f, 1.0f); + const float lw = float(menu_width - 32) * (fraction); + context.color().draw_filled_rect(Rectf(pos + Vector(16, -4), + pos + Vector(16 + lw, 4)), + Color::WHITE, 0.0f, LAYER_GUI-1); +} + +int +ItemColorChannelSaturation::get_width() const +{ + return static_cast(Resources::normal_font->get_text_width(get_text()) + 16 + static_cast(m_flickw)); +} + +void +ItemColorChannelSaturation::enable_edit_mode() +{ + if (m_edit_mode) + // Do nothing if it is already enabled + return; + m_edit_mode = true; + set_text(float_to_string(*m_sat)); +} + + +void +ItemColorChannelSaturation::event(const SDL_Event& ev) +{ + if (ev.type == SDL_TEXTINPUT) { + std::string txt = ev.text.text; + for (auto& c : txt) { + add_char(c); + } + } +} + +void +ItemColorChannelSaturation::add_char(char c) +{ + enable_edit_mode(); + std::string text = get_text(); + + if (c == '.' || c == ',') + { + const bool has_comma = (text.find('.') != std::string::npos); + if (!has_comma) + { + if (text.empty()) { + text = "0."; + } else { + text.push_back('.'); + } + } + } + else if (isdigit(c)) + { + text.push_back(c); + } + else + { + return; + } + + float number = std::stof(text); + if (0.0f <= number && number <= 1.0f) { + *m_sat = number; + set_text(text); + } +} + +void +ItemColorChannelSaturation::remove_char() +{ + enable_edit_mode(); + std::string text = get_text(); + + if (text.empty()) + { + *m_sat = 0.0f; + } + else + { + text.pop_back(); + + if (!text.empty()) { + *m_sat = std::stof(text); + } else { + *m_sat = 0.0f; + } + } + + set_text(text); +} + +void +ItemColorChannelSaturation::process_action(const MenuAction& action) +{ + switch (action) + { + case MenuAction::REMOVE: + remove_char(); + break; + + case MenuAction::LEFT: + case MenuAction::RIGHT: { + float maxC = m_okl->get_max_chroma(); + float delta = (action == MenuAction::LEFT ? -0.01f : +0.01f); + + float newSat = *m_sat + delta; + newSat = std::clamp(newSat, 0.0f, maxC); + newSat = roundf(newSat * 100.0f) / 100.0f; + // Snap to reduce jitter at boundaries + if(newSat < 0.005f) newSat = 0.0f; + if(newSat > maxC - 0.005f) newSat = maxC; + + *m_sat = newSat; + + float oldA = m_color->alpha; + *m_color = Color::from_oklch(*m_okl); + m_color->alpha = oldA; // if you want to preserve alpha + m_edit_mode = false; + break; + } + + case MenuAction::UNSELECT: + m_edit_mode = false; + break; + + default: + break; + } +} + +// Color +// ItemColorChannelSaturation::get_color() const +// { +// return m_channel; +// } + +/* EOF */ diff --git a/src/gui/item_colorchannel_saturation.hpp b/src/gui/item_colorchannel_saturation.hpp new file mode 100644 index 00000000000..5f08989a8c3 --- /dev/null +++ b/src/gui/item_colorchannel_saturation.hpp @@ -0,0 +1,69 @@ +// SuperTux +// Copyright (C) 2015 Hume2 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#ifndef HEADER_SUPERTUX_GUI_ITEM_COLORCHANNEL_SATURATION_HPP +#define HEADER_SUPERTUX_GUI_ITEM_COLORCHANNEL_SATURATION_HPP + +#include "gui/menu_item.hpp" + +#include "util/colorspace_oklab.hpp" +#include "video/color.hpp" + + +class ItemColorChannelSaturation final : public MenuItem +{ +public: + ItemColorChannelSaturation(Color* color, ColorOKLCh* okl, int id = -1); + + /** Draws the menu item. */ + virtual void draw(DrawingContext&, const Vector& pos, int menu_width, bool active) override; + + /** Returns the minimum width of the menu item. */ + virtual int get_width() const override; + + /** Processes the menu action. */ + virtual void process_action(const MenuAction& action) override; + + /** Processes the given event. */ + virtual void event(const SDL_Event& ev) override; + + //virtual Color get_color() const override; + + virtual bool changes_width() const override { return true; } + + void change_input(const std::string& input_) { set_text(input_); } + +private: + void enable_edit_mode(); + void add_char(char c); + void remove_char(); + +private: + Color* m_color; + ColorOKLCh* m_okl; + float* m_sat; + float m_sat_prev; + bool m_edit_mode; + int m_flickw; + +private: + ItemColorChannelSaturation(const ItemColorChannelSaturation&) = delete; + ItemColorChannelSaturation& operator=(const ItemColorChannelSaturation&) = delete; +}; + +#endif + +/* EOF */ diff --git a/src/gui/menu.cpp b/src/gui/menu.cpp index 3f6c82cc057..504a79370e6 100644 --- a/src/gui/menu.cpp +++ b/src/gui/menu.cpp @@ -21,6 +21,7 @@ #include "gui/item_back.hpp" #include "gui/item_color.hpp" #include "gui/item_colorchannel_rgba.hpp" +#include "gui/item_colorchannel_saturation.hpp" #include "gui/item_colordisplay.hpp" #include "gui/item_color_picker_2d.hpp" #include "gui/item_controlfield.hpp" @@ -267,6 +268,11 @@ Menu::add_color_channel_rgba(float* input, Color channel, int id, bool is_linear return add_item(input, channel, id, is_linear); } +ItemColorChannelSaturation& +Menu::add_color_channel_saturation(Color* color, ColorOKLCh* okl, int id) { + return add_item(color, okl, id); +} + ItemColorPicker2D& Menu::add_color_picker_2d(Color& color) { return add_item(color); diff --git a/src/gui/menu.hpp b/src/gui/menu.hpp index 4d923d5797f..4a796a1028d 100644 --- a/src/gui/menu.hpp +++ b/src/gui/menu.hpp @@ -30,6 +30,7 @@ class ItemAction; class ItemBack; class ItemColor; class ItemColorChannelRGBA; +class ItemColorChannelSaturation; class ItemColorPicker2D; class ItemColorDisplay; class ItemControlField; @@ -102,6 +103,7 @@ class Menu ItemColorDisplay& add_color_display(Color* color, int id = -1); ItemColorChannelRGBA& add_color_channel_rgba(float* input, Color channel, int id = -1, bool is_linear = false); + ItemColorChannelSaturation& add_color_channel_saturation(Color* color, ColorOKLCh* okl, int id); ItemColorPicker2D& add_color_picker_2d(Color& color); ItemPaths& add_path_settings(const std::string& text, PathObject& target, const std::string& path_ref); ItemStringArray& add_string_array(const std::string& text, std::vector& items, int id = -1); diff --git a/src/gui/menu_color.cpp b/src/gui/menu_color.cpp index b6ec5adb4fd..a5cee43c113 100644 --- a/src/gui/menu_color.cpp +++ b/src/gui/menu_color.cpp @@ -24,12 +24,16 @@ #include "util/gettext.hpp" ColorMenu::ColorMenu(Color* color) : - m_color(color) + m_color(color), + m_okl(*color) { add_label(_("Mix the colour")); add_hl(); add_color_picker_2d(*m_color); + + add_color_channel_saturation(m_color, &m_okl, MNID_SATURATION); + add_color_channel_rgba(&(m_color->red), Color::RED); add_color_channel_rgba(&(m_color->green), Color::GREEN); add_color_channel_rgba(&(m_color->blue), Color::BLUE); @@ -117,6 +121,12 @@ ColorMenu::menu_action(MenuItem& item) log_warning << "Invalid color format: " << text << ". Supported formats: rgb(r,g,b) and #rrggbb" << std::endl; } } + else if (item.get_id() == MNID_SATURATION) + { + float oldA = m_color->alpha; + *m_color = Color::from_oklch(m_okl); + m_color->alpha = oldA; + } } /* EOF */ diff --git a/src/gui/menu_color.hpp b/src/gui/menu_color.hpp index de1dbad9cf0..768071ac6c2 100644 --- a/src/gui/menu_color.hpp +++ b/src/gui/menu_color.hpp @@ -34,11 +34,13 @@ class ColorMenu final : public Menu { MNID_COPY_CLIPBOARD_RGB = 1, MNID_COPY_CLIPBOARD_HEX, - MNID_PASTE_CLIPBOARD + MNID_PASTE_CLIPBOARD, + MNID_SATURATION }; private: Color* m_color; + ColorOKLCh m_okl; private: ColorMenu(const ColorMenu&) = delete; diff --git a/src/util/colorspace_oklab.cpp b/src/util/colorspace_oklab.cpp index e7e35af8b24..f1bf3e778f1 100644 --- a/src/util/colorspace_oklab.cpp +++ b/src/util/colorspace_oklab.cpp @@ -107,4 +107,22 @@ ColorOKLCh::get_modified_lightness() const + 4 * k_2 * k_3 * L)); } +float +ColorOKLCh::get_max_chroma(int steps) const +{ + float low = 0.0f, high = 1.0f; + for (int i = 0; i < steps; ++i) { + float mid = 0.5f * (low + high); + Color c = Color::from_oklch(ColorOKLCh{L, mid, h}); + if (c.red >= 0.0f && c.red <= 1.0f && + c.green>= 0.0f && c.green<= 1.0f && + c.blue >= 0.0f && c.blue <= 1.0f) { + low = mid; // still in gamut, try more chroma + } else { + high = mid; // out of gamut, back off + } + } + return low; +} + /* EOF */ diff --git a/src/util/colorspace_oklab.hpp b/src/util/colorspace_oklab.hpp index 0d65be50997..1ae075aa227 100644 --- a/src/util/colorspace_oklab.hpp +++ b/src/util/colorspace_oklab.hpp @@ -32,6 +32,8 @@ struct ColorOKLCh final { // Calculate a different lightness estimate which has less dark values float get_modified_lightness() const; + float get_max_chroma(int steps = 20) const; + float L, C, h; }; diff --git a/src/video/color.cpp b/src/video/color.cpp index 2b0fd027353..cac8c6d3643 100644 --- a/src/video/color.cpp +++ b/src/video/color.cpp @@ -193,4 +193,40 @@ Color::serialize_to_rgb(const Color& color) return ss.str(); } +Color +Color::from_oklch(const ColorOKLCh& lch) +{ + // 1) LCh → OKLab (Lab) components + float L = lch.L; + float a = lch.C * cosf(lch.h); + float b = lch.C * sinf(lch.h); + + // 2) OKLab → linear-srgb (matrix from Ottosson’s inverse) + // see https://bottosson.github.io/posts/oklab/#converting-from-oklab-to-linear-srgb + float l_ = L + 0.3963377774f * a + 0.2158037573f * b; + float m_ = L - 0.1055613458f * a - 0.0638541728f * b; + float s_ = L - 0.0894841775f * a - 1.2914855480f * b; + + float l = l_ * l_ * l_; + float m = m_ * m_ * m_; + float s = s_ * s_ * s_; + + float r_lin = +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s; + float g_lin = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s; + float b_lin = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s; + + // 3) linear-srgb → gamma-encoded sRGB + float r = linear_to_srgb(r_lin); + float g = linear_to_srgb(g_lin); + float bl = linear_to_srgb(b_lin); + + // 4) clamp to [0,1] and return + return Color( + std::clamp(r, 0.0f, 1.0f), + std::clamp(g, 0.0f, 1.0f), + std::clamp(bl,0.0f,1.0f), + 1.0f + ); +} + /* EOF */ diff --git a/src/video/color.hpp b/src/video/color.hpp index 5523a1674fe..9e84b38b8e4 100644 --- a/src/video/color.hpp +++ b/src/video/color.hpp @@ -23,6 +23,7 @@ #include #include +#include "util/colorspace_oklab.hpp" class Color final { @@ -97,6 +98,13 @@ class Color final static float add_gamma(float x) { return powf(x, 1.0f / 2.2f); } static float remove_gamma(float x) { return powf(x, 2.2f); } + static float linear_to_srgb(float x) { + if (x <= 0.0031308f) return 12.92f * x; + else return 1.055f * powf(x, 1.0f/2.4f) - 0.055f; + } + + static Color from_oklch(const ColorOKLCh& lch); + public: Color(); diff --git a/tests/test_color.cpp b/tests/test_color.cpp new file mode 100755 index 00000000000..2af618226b1 --- /dev/null +++ b/tests/test_color.cpp @@ -0,0 +1,73 @@ +#include +#include +#include "video/color.hpp" + +// Helper function to compare floats with a small tolerance +bool approx_equal(float a, float b, float epsilon = 0.01f) { + return std::fabs(a - b) < epsilon; +} + +// Test Case 1: Check default constructor +void test_default_constructor() { + Color c; + assert(approx_equal(c.red, 0.0f)); + assert(approx_equal(c.green, 0.0f)); + assert(approx_equal(c.blue, 0.0f)); + assert(approx_equal(c.alpha, 1.0f)); +} + +// Test Case 2: Check parameterized constructor +void test_parameterized_constructor() { + Color c(0.5f, 0.5f, 0.5f, 0.5f); + assert(approx_equal(c.red, 0.5f)); + assert(approx_equal(c.green, 0.5f)); + assert(approx_equal(c.blue, 0.5f)); + assert(approx_equal(c.alpha, 0.5f)); +} + +// Test Case 3: Check equality operator +void test_equality_operator() { + Color c1(0.1f, 0.2f, 0.3f, 1.0f); + Color c2(0.1f, 0.2f, 0.3f, 1.0f); + Color c3(0.3f, 0.2f, 0.1f, 1.0f); + assert(c1 == c2); + assert(!(c1 == c3)); +} + + + +// Test Case 4: Check color clamping in from_oklch +void test_from_oklch_clamping() { + ColorOKLCh lch = {1.5f, 0.5f, 3.0f}; // Should clamp to [0,1] + Color c = Color::from_oklch(lch); + assert(c.red <= 1.0f && c.red >= 0.0f); + assert(c.green <= 1.0f && c.green >= 0.0f); + assert(c.blue <= 1.0f && c.blue >= 0.0f); + assert(approx_equal(c.alpha, 1.0f)); +} + +// Test Case 5: Test hex serialization/deserialization +void test_serialize_deserialize_hex() { + Color c(0.5f, 0.4f, 0.3f, 1.0f); + std::string hex = Color::serialize_to_hex(c); + auto parsed = Color::deserialize_from_hex(hex); + assert(parsed.has_value()); + assert(approx_equal(parsed->red, c.red)); + assert(approx_equal(parsed->green, c.green)); + assert(approx_equal(parsed->blue, c.blue)); + assert(approx_equal(parsed->alpha, c.alpha)); +} + +int main() { + // Run tests + test_default_constructor(); + test_parameterized_constructor(); + test_equality_operator(); + test_from_oklch_clamping(); + test_serialize_deserialize_hex(); + + std::cout << "All tests passed!\n"; + return 0; +} + +/* EOF */ diff --git a/tests/test_colorspace_oklab.cpp b/tests/test_colorspace_oklab.cpp new file mode 100644 index 00000000000..ddb1fd3cbe1 --- /dev/null +++ b/tests/test_colorspace_oklab.cpp @@ -0,0 +1,50 @@ +#include +#include +#include +#include "util/colorspace_oklab.hpp" +#include "video/color.hpp" + +// Approximate comparison helper +bool approx_equal(float a, float b, float epsilon = 1e-4f) { + return std::fabs(a - b) < epsilon; +} + +void test_conversion_to_oklch() { + Color c(1.0f, 0.0f, 0.0f); // bright red + ColorOKLCh oklch(c); + + std::cout << "Converted OKLCh values:\n"; + std::cout << "L: " << oklch.L << " C: " << oklch.C << " h: " << oklch.h << "\n"; + + assert(oklch.L > 0.0f && oklch.L <= 1.0f); + assert(oklch.C > 0.0f); +} + +void test_modified_lightness() { + Color c(0.5f, 0.5f, 0.5f); // neutral gray + ColorOKLCh oklch(c); + + float modL = oklch.get_modified_lightness(); + std::cout << "Modified Lightness: " << modL << "\n"; + + assert(modL >= 0.0f && modL <= 1.0f); +} + +void test_max_chroma() { + Color c(0.8f, 0.2f, 0.4f); + ColorOKLCh oklch(c); + float max_c = oklch.get_max_chroma(10); + std::cout << "Max Chroma (10 steps): " << max_c << "\n"; + + assert(max_c >= 0.0f && max_c <= 1.0f); +} + +int main() { + test_conversion_to_oklch(); + test_modified_lightness(); + test_max_chroma(); + std::cout << "All tests passed.\n"; + return 0; +} + +/* EOF */ diff --git a/tests/test_item_colorchannel_saturation.cpp b/tests/test_item_colorchannel_saturation.cpp new file mode 100644 index 00000000000..9daf52102d8 --- /dev/null +++ b/tests/test_item_colorchannel_saturation.cpp @@ -0,0 +1,66 @@ +#include +#include +#include "gui/item_colorchannel_saturation.hpp" +#include "video/color.hpp" + +bool approx_equal(float a, float b, float epsilon = 0.01f) { + return std::fabs(a - b) < epsilon; +} + +void test_initialization() { + Color c(0.5f, 0.5f, 0.5f); + ColorOKLCh oklch(c); + + ItemColorChannelSaturation item(&c, &oklch, 1); + assert(approx_equal(*item.m_sat, oklch.C)); + std::cout << "Initialization test passed.\n"; +} + +void test_add_char_and_clamp() { + Color c(0.3f, 0.4f, 0.5f); + ColorOKLCh oklch(c); + + ItemColorChannelSaturation item(&c, &oklch, 1); + item.add_char('0'); + item.add_char('.'); + item.add_char('9'); + + assert(approx_equal(*item.m_sat, 0.9f)); + std::cout << "Char input test passed.\n"; +} + +void test_left_right_actions() { + Color c(0.6f, 0.2f, 0.2f); + ColorOKLCh oklch(c); + float original = oklch.C; + + ItemColorChannelSaturation item(&c, &oklch, 1); + item.process_action(MenuAction::RIGHT); + assert(*item.m_sat >= original); // could be clamped but shouldn't go down + + item.process_action(MenuAction::LEFT); + assert(*item.m_sat <= original + 0.01f); // should go back down or stay near original + std::cout << "Left/right action test passed.\n"; +} + +void test_edit_mode_toggle() { + Color c(0.1f, 0.2f, 0.3f); + ColorOKLCh oklch(c); + ItemColorChannelSaturation item(&c, &oklch, 1); + + item.enable_edit_mode(); + item.add_char('0'); + assert(item.get_text().find('0') != std::string::npos); + std::cout << "Edit mode toggle test passed.\n"; +} + +int main() { + test_initialization(); + test_add_char_and_clamp(); + test_left_right_actions(); + test_edit_mode_toggle(); + std::cout << "All ItemColorChannelSaturation tests passed.\n"; + return 0; +} + +/* EOF */ diff --git a/tests/test_menu.cpp b/tests/test_menu.cpp new file mode 100644 index 00000000000..51afcbe70aa --- /dev/null +++ b/tests/test_menu.cpp @@ -0,0 +1,66 @@ +#include "gui/menu.hpp" +#include "gui/item_action.hpp" +#include "supertux/menu_action.hpp" +#include + +TEST_CASE("Menu basic item management", "[gui][menu]") { + Menu menu; + + SECTION("Add a non-skippable item and check active index") { + auto& item = menu.add_entry(1, "Start Game"); + REQUIRE_FALSE(item.skippable()); + REQUIRE(menu.get_active_item_index() == 0); + } + + SECTION("Add a skippable item and ensure active index is not set") { + struct DummySkippableItem : public MenuItem { + bool skippable() const override { return true; } + void draw(DrawingContext&, const Vector&, int, bool) override {} + }; + + menu.add_item(std::make_unique()); + REQUIRE(menu.get_active_item_index() == -1); + } + + SECTION("Delete an item and ensure active item adjusts") { + menu.add_entry(1, "Play"); + menu.add_entry(2, "Options"); + REQUIRE(menu.get_active_item_index() == 0); + + menu.delete_item(0); + REQUIRE(menu.get_active_item_index() == 0); // "Options" moves to index 0 + } + + SECTION("Clear the menu") { + menu.add_entry(1, "Play"); + menu.clear(); + REQUIRE(menu.get_active_item_index() == -1); + REQUIRE(menu.get_item_count() == 0); + } +} + +TEST_CASE("Menu navigation", "[gui][menu]") { + Menu menu; + menu.add_entry(1, "Play"); + menu.add_entry(2, "Options"); + menu.add_entry(3, "Exit"); + + SECTION("Navigate down") { + menu.process_action(MenuAction::DOWN); + REQUIRE(menu.get_active_item_index() == 1); + } + + SECTION("Navigate up wraps around") { + menu.process_action(MenuAction::UP); + REQUIRE(menu.get_active_item_index() == 2); + } + + SECTION("Navigate down wraps around") { + menu.process_action(MenuAction::DOWN); + menu.process_action(MenuAction::DOWN); + menu.process_action(MenuAction::DOWN); + REQUIRE(menu.get_active_item_index() == 0); + } +} + +/* EOF */ diff --git a/tests/test_menu_color.cpp b/tests/test_menu_color.cpp new file mode 100644 index 00000000000..69e16ba642f --- /dev/null +++ b/tests/test_menu_color.cpp @@ -0,0 +1,70 @@ +#include +#include +#include "gui/menu_color.hpp" +#include "util/log.hpp" + +// Mock class to simulate clipboard functionality +class MockSDL { +public: + static void SetClipboardText(const std::string& text) { + clipboard_text = text; + } + + static const std::string& GetClipboardText() { + return clipboard_text; + } + +private: + static std::string clipboard_text; +}; + +std::string MockSDL::clipboard_text = ""; + +// Helper function to simulate SDL_SetClipboardText +void SDL_SetClipboardText(const char* text) { + MockSDL::SetClipboardText(text); +} + +// Helper function to simulate SDL_GetClipboardText +const char* SDL_GetClipboardText() { + return MockSDL::GetClipboardText().c_str(); +} + +// Test function for ColorMenu +void test_ColorMenu() { + // Create a Color object and initialize ColorMenu + Color color(1.f, 0.f, 0.f); // Red color + ColorMenu menu(&color); + + // Test copying to clipboard (RGB format) + menu.menu_action(menu.get_item_by_id(MNID_COPY_CLIPBOARD_RGB)); + assert(MockSDL::GetClipboardText() == "rgb(1.000000,0.000000,0.000000)"); + + // Test copying to clipboard (Hex format) + menu.menu_action(menu.get_item_by_id(MNID_COPY_CLIPBOARD_HEX)); + assert(MockSDL::GetClipboardText() == "#ff0000"); + + // Test pasting from clipboard (valid color) + MockSDL::SetClipboardText("rgb(0.0,1.0,0.0)"); // Green color + menu.menu_action(menu.get_item_by_id(MNID_PASTE_CLIPBOARD)); + assert(color == Color(0.f, 1.f, 0.f)); // Verify color is updated to green + + // Test pasting from clipboard (invalid color format) + MockSDL::SetClipboardText("invalid color format"); + menu.menu_action(menu.get_item_by_id(MNID_PASTE_CLIPBOARD)); + assert(color != Color(1.f, 0.f, 0.f)); // Ensure color hasn't changed + + // Test saturation adjustment + float old_alpha = color.alpha; + menu.menu_action(menu.get_item_by_id(MNID_SATURATION)); + assert(color.alpha == old_alpha); // Ensure alpha hasn't changed + assert(color != Color(1.f, 0.f, 0.f)); // Ensure color was modified by saturation +} + +int main() { + test_ColorMenu(); + log_info << "All tests passed!" << std::endl; + return 0; +} + +/* EOF */