diff --git a/CMakeLists.txt b/CMakeLists.txt index 598bd0b..7d363f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # set(CMAKE_CXX_VISIBILITY_PRESET hidden) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-everything /W4") @@ -158,14 +159,14 @@ set (SOURCES ${CMAKE_SOURCE_DIR}/src/ui/ui_color_hack.cpp ${CMAKE_SOURCE_DIR}/src/ui/ui_rml_hacks.cpp ${CMAKE_SOURCE_DIR}/src/ui/ui_elements.cpp + ${CMAKE_SOURCE_DIR}/src/ui/util/hsv.cpp ${CMAKE_SOURCE_DIR}/src/ui/elements/ElementConfigGroup.cpp ${CMAKE_SOURCE_DIR}/src/ui/elements/ElementConfigOption.cpp ${CMAKE_SOURCE_DIR}/src/ui/elements/ElementOptionTypeCheckbox.cpp + ${CMAKE_SOURCE_DIR}/src/ui/elements/ElementOptionTypeColor.cpp ${CMAKE_SOURCE_DIR}/src/ui/elements/ElementOptionTypeRadioTabs.cpp ${CMAKE_SOURCE_DIR}/src/ui/elements/ElementOptionTypeRange.cpp ${CMAKE_SOURCE_DIR}/src/ui/elements/ElementDescription.cpp - ${CMAKE_SOURCE_DIR}/src/ui/config_options/ConfigRegistry.cpp - ${CMAKE_SOURCE_DIR}/src/ui/config_options/ConfigOption.cpp ${CMAKE_SOURCE_DIR}/rsp/aspMain.cpp ${CMAKE_SOURCE_DIR}/rsp/njpgdspMain.cpp @@ -338,4 +339,3 @@ build_pixel_shader(Zelda64Recompiled "shaders/InterfacePS.hlsl" "shaders/Interfa target_sources(Zelda64Recompiled PRIVATE ${SOURCES}) set_property(TARGET Zelda64Recompiled PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") - diff --git a/include/recomp_ui.h b/include/recomp_ui.h index 0720ba3..7374866 100644 --- a/include/recomp_ui.h +++ b/include/recomp_ui.h @@ -7,6 +7,8 @@ #include "SDL.h" #include "RmlUi/Core.h" +#include "../src/ui/util/hsv.h" + namespace Rml { class ElementDocument; class EventListenerInstancer; diff --git a/patches/syms.ld b/patches/syms.ld index a0e5d7d..0713f85 100644 --- a/patches/syms.ld +++ b/patches/syms.ld @@ -44,3 +44,4 @@ recomp_get_inverted_axes = 0x8F0000A4; recomp_high_precision_fb_enabled = 0x8F0000A8; recomp_get_resolution_scale = 0x8F0000AC; recomp_get_analog_inverted_axes = 0x8F0000B0; +recomp_get_config_store_int = 0x8F0000B4; diff --git a/src/game/recomp_api.cpp b/src/game/recomp_api.cpp index fa62654..77f3f77 100644 --- a/src/game/recomp_api.cpp +++ b/src/game/recomp_api.cpp @@ -2,7 +2,7 @@ #include "librecomp/recomp.h" #include "librecomp/overlays.hpp" -#include "librecomp/config_store.hpp" +#include "librecomp/config.hpp" #include "zelda_config.h" #include "recomp_input.h" #include "recomp_ui.h" @@ -180,7 +180,7 @@ extern "C" void recomp_get_config_store_int(uint8_t* rdram, recomp_context* ctx) i++; } - _return(ctx, recomp::get_config_store_value( + _return(ctx, recomp::config::get_config_store_value( std::string_view{key_buffer.data(), key_buffer.size()} )); } diff --git a/src/ui/config_options/ConfigOption.cpp b/src/ui/config_options/ConfigOption.cpp deleted file mode 100644 index 60696e1..0000000 --- a/src/ui/config_options/ConfigOption.cpp +++ /dev/null @@ -1,31 +0,0 @@ - -#include "ConfigOption.h" -#include "librecomp/config_store.hpp" -#include -#include - -using json = nlohmann::json; -namespace recompui { - -bool ConfigOption::validate(json& opt_json) { - auto type_struct = get_type_structure(); - for (const auto& [key, expected_type] : type_struct) { - if (!opt_json.contains(key) || opt_json[key].type() != expected_type) { - return false; - } - } - return true; -} - -ConfigOption::ConfigOption(json& opt_json) -{ - type = opt_json[ConfigOption::schema::type]; - key = opt_json[ConfigOption::schema::key]; -} - -ConfigOption::~ConfigOption() -{ - -} - -} // namespace Rml diff --git a/src/ui/config_options/ConfigOption.h b/src/ui/config_options/ConfigOption.h deleted file mode 100644 index 8b3f7af..0000000 --- a/src/ui/config_options/ConfigOption.h +++ /dev/null @@ -1,65 +0,0 @@ -#ifndef RECOMPUI_CONFIG_OPTION_H -#define RECOMPUI_CONFIG_OPTION_H - -#include -#include "json/json.hpp" - -namespace recompui { - -enum class ConfigOptionType { - Label, - // Base types - Checkbox, - RadioTabs, - Dropdown, - Range, - Trigger, - // Group types - CheckboxGroup, - Group, - OptionCount -}; - -NLOHMANN_JSON_SERIALIZE_ENUM(ConfigOptionType, { - {ConfigOptionType::Label, "Label"}, - {ConfigOptionType::Checkbox, "Checkbox"}, - {ConfigOptionType::RadioTabs, "RadioTabs"}, - {ConfigOptionType::Dropdown, "Dropdown"}, - {ConfigOptionType::Range, "Range"}, - {ConfigOptionType::Trigger, "Trigger"}, - {ConfigOptionType::CheckboxGroup, "CheckboxGroup"}, - {ConfigOptionType::Group, "Group"} -}); - -typedef std::unordered_map config_type_structure; - -inline bool config_option_is_group(ConfigOptionType option_type) { - return option_type == ConfigOptionType::Group || option_type == ConfigOptionType::CheckboxGroup; -} - -/** - Base Config Option class. Defines what type of option or group it is, and - sets the key. - */ - -class ConfigOption { -public: - ConfigOption(nlohmann::json& opt_json); - virtual ~ConfigOption(); - - bool validate(nlohmann::json& opt_json); - - ConfigOptionType type; - std::string key; - - struct schema { - static constexpr const char* type = "type"; - static constexpr const char* key = "key"; - }; - -protected: - virtual const config_type_structure& get_type_structure() const = 0; -}; - -} -#endif diff --git a/src/ui/config_options/ConfigRegistry.cpp b/src/ui/config_options/ConfigRegistry.cpp deleted file mode 100644 index 7763e68..0000000 --- a/src/ui/config_options/ConfigRegistry.cpp +++ /dev/null @@ -1,230 +0,0 @@ -#include "ConfigRegistry.h" -#include "ConfigOption.h" -#include "librecomp/config_store.hpp" - -using json = nlohmann::json; - -namespace recompui { - -ConfigRegistry config_registry = {{}, {}}; - -#define TODO_PARSE_ERROR(m, t) printf("Value at key: \"%s\" was invalid. Expected: \"%s\"\n", m, t) - -std::string get_string_in_json(const json& j, const std::string& key) { - std::string ret; - - auto find_it = j.find(key); - if (find_it != j.end()) { - auto at_val = j.at(key); - if (!at_val.is_string()) { - TODO_PARSE_ERROR(key, "string"); - } - - find_it->get_to(ret); - } - - return ret; -} - -std::string get_string_in_json_with_default(const json& j, const std::string& key, const std::string& default_val) { - std::string ret; - - auto find_it = j.find(key); - if (find_it != j.end()) { - auto at_val = j.at(key); - if (!at_val.is_string()) { - return default_val; - } - - find_it->get_to(ret); - } else { - return default_val; - } - - return ret; -} - -static bool validate_json_value_is_array(const json &j, const std::string& json_path, const std::string& arr_key) { - const auto &options = j.find(arr_key); - if (options == j.end()) { - TODO_PARSE_ERROR(json_path + "/" + arr_key, "array"); - return false; - } - const auto &opt_array = j[arr_key]; - - if (!opt_array.is_array()) { - TODO_PARSE_ERROR(json_path + "/" + arr_key, "array"); - return false; - } - - return true; -} - -// Option -void register_config_option( - const json& j, - const std::string& previous_key, // previous path before current key - const std::string& config_group, - const std::string& json_path // path to this json object from the root -) { - const std::string key = get_string_in_json(j, ConfigOption::schema::key); - const std::string this_key = previous_key + "/" + key; - - ConfigOptionType type = get_value_in_json(j, ConfigOption::schema::type); - - config_registry.key_ref_map[this_key] = { config_group, json_path }; - - switch (type) { - case ConfigOptionType::Checkbox: { - bool default_val = false; - if (j.find("default") != j.end()) { - default_val = get_value_in_json(j, "default"); - } - recomp::set_config_store_value_and_default(this_key, default_val, default_val); - break; - } - case ConfigOptionType::RadioTabs: { - if (!validate_json_value_is_array(j, json_path, "values")) { - return; - } - - int default_val = 0; - if (j.find("default") != j.end()) { - const auto &opt_array = j["values"]; - const std::string default_val_string = get_string_in_json(j, "default"); - // Based on default value's string, find which option index corresponds - for (int i = 0; i < opt_array.size(); i++) { - const auto &j_opt = opt_array[i]; - if (!j_opt.is_string()) { - TODO_PARSE_ERROR(key + "/values/" + std::to_string(i) , "string"); - return; - } - const std::string opt_val = j_opt.get(); - if (opt_val == default_val_string) { - default_val = i; - break; - } - } - } - recomp::set_config_store_value_and_default(this_key, default_val, default_val); - break; - } - case ConfigOptionType::Range: { - int default_val = 0; - int max = 0; - int min = 0; - int step = 1; - - if (j.find("default") != j.end()) { - default_val = get_value_in_json(j, "default"); - } - - // Max is required - if (j.find("max") != j.end()) { - max = get_value_in_json(j, "max"); - if (default_val > max) default_val = max; - } else { - TODO_PARSE_ERROR(key + "/max", "int"); - return; - } - - if (j.find("min") != j.end()) { - min = get_value_in_json(j, "min"); - if (default_val < min) default_val = min; - } - - if (j.find("step") != j.end()) { - step = get_value_in_json(j, "step"); - } - - assert(max > min); - assert(step < max - min); - recomp::set_config_store_value_and_default(this_key, default_val, default_val); - break; - } - } - if (j.find("label") != j.end()) { - const std::string label = get_string_in_json(j, "label"); - recomp::set_config_store_value(this_key, label); - } - - if ((type == ConfigOptionType::Group || type == ConfigOptionType::CheckboxGroup) && j.find("options") != j.end()) { - if (!validate_json_value_is_array(j, json_path, "options")) { - return; - } - - const auto &opt_array = j["options"]; - - for (int i = 0; i < opt_array.size(); i++) { - const auto &el = opt_array[i]; - register_config_option( - el, - this_key, - config_group, - json_path + "/options/" + std::to_string(i) - ); - } - } -} - -void register_config(const std::string &json_str, const std::string &config_group) { - config_registry.group_json_map[config_group] = json::parse(json_str); - const auto &j = config_registry.group_json_map[config_group]; - - if (!j.is_array()) { - TODO_PARSE_ERROR("/", "array"); - return; - } - - for (int i = 0; i < j.size(); i++) { - const auto &el = j[i]; - register_config_option( - el, - config_group, - config_group, - "/" + std::to_string(i) // json_path at top level - ); - } -} - -void register_translation(const std::string &json_str, const std::string &config_group) { - const auto &j = json::parse(json_str); - - if (!j.is_object()) { - TODO_PARSE_ERROR("/", "object"); - return; - } - - for (auto& el : j.items()) - { - std::string translation_key = "translations/" + config_group + "/" + el.key(); - const std::string value = el.value(); - recomp::set_config_store_value(translation_key, value); - } -} - -json get_json_from_key(const std::string &config_key) { - if (config_registry.key_ref_map.find(config_key) == config_registry.key_ref_map.end()) { - // TODO: handle not finding config_key - printf("FAILURE: AddOptionTypeElement failed to find config_key '%s' in config_registry.key_ref_map\n", config_key); - } - const JSONRef& json_ref = config_registry.key_ref_map[config_key]; - const json& group_json = config_registry.group_json_map[json_ref.config_group]; - json::json_pointer pointer(json_ref.json_path); - return group_json[pointer]; -} - -bool config_key_is_base_group(const std::string &config_group_key) { - // determine if the key references a base group by checking the group_json_map - if (config_registry.group_json_map.find(config_group_key) == config_registry.group_json_map.end()) { - return false; - } - return true; -} - -json& get_group_json(const std::string &config_group_key) { - return config_registry.group_json_map[config_group_key]; -} - - -} // namespace recompui diff --git a/src/ui/config_options/ConfigRegistry.h b/src/ui/config_options/ConfigRegistry.h deleted file mode 100644 index 724be15..0000000 --- a/src/ui/config_options/ConfigRegistry.h +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef RECOMPUI_CONFIG_REGISTRY_H -#define RECOMPUI_CONFIG_REGISTRY_H - -#include -#include -#include -#include -#include -#include -#include "json/json.hpp" - -namespace recompui { - struct JSONRef { - std::string config_group; - std::string json_path; // used as a json pointer - }; - // config key -> JSONRef - typedef std::unordered_map config_registry_key_reference_map; - // config group -> json - typedef std::unordered_map config_registry_group_json_map; - - struct ConfigRegistry { - config_registry_key_reference_map key_ref_map; - config_registry_group_json_map group_json_map; - }; - - extern ConfigRegistry config_registry; - - void register_config(const std::string& json_str, const std::string& config_group); - void register_translation(const std::string &json_str, const std::string &config_group); - - nlohmann::json get_json_from_key(const std::string &config_key); - std::string get_string_in_json(const nlohmann::json& j, const std::string& key); - std::string get_string_in_json_with_default(const nlohmann::json& j, const std::string& key, const std::string& default_val); - bool config_key_is_base_group(const std::string &config_group_key); - nlohmann::json& get_group_json(const std::string &config_group_key); - - #define TODO_PARSE_ERROR(m, t) printf("Value at key: \"%s\" was invalid. Expected: \"%s\"\n", m, t) - - template - T get_value_in_json(const nlohmann::json& j, const std::string& key) { - T ret; - - auto find_it = j.find(key); - if (find_it != j.end()) { - auto at_val = j.at(key); - find_it->get_to(ret); - } - - return ret; - } - - template - T get_value_in_json_with_default(const nlohmann::json& j, const std::string& key, T default_val) { - T ret = default_val; - - auto find_it = j.find(key); - if (find_it != j.end()) { - auto at_val = j.at(key); - find_it->get_to(ret); - } - - return ret; - } -} -#endif diff --git a/src/ui/elements/ElementConfigGroup.cpp b/src/ui/elements/ElementConfigGroup.cpp index bb42611..c79bb7d 100644 --- a/src/ui/elements/ElementConfigGroup.cpp +++ b/src/ui/elements/ElementConfigGroup.cpp @@ -1,17 +1,12 @@ - #include "ElementConfigGroup.h" #include "ElementConfigOption.h" -#include "../config_options/ConfigOption.h" -#include "../config_options/ConfigRegistry.h" -#include "ElementOptionTypeCheckbox.h" -#include "librecomp/config_store.hpp" + #include -#include "recomp_ui.h" -#include -#include #include using json = nlohmann::json; +using ConfigOptionType = recomp::config::ConfigOptionType; +using ConfigOption = recomp::config::ConfigOption; namespace recompui { @@ -62,7 +57,7 @@ void ElementConfigGroup::AddConfigOptionElement(const json& option_json) { ConfigOptionType el_option_type = ConfigOptionType::Label; from_json(option_json["type"], el_option_type); - const std::string key = get_string_in_json(option_json, ConfigOption::schema::key); + const std::string key = recomp::config::get_string_in_json(option_json, ConfigOption::schema::key); Rml::Element *option_container = GetChild(1); Rml::Element *child_opt = nullptr; @@ -87,11 +82,11 @@ void ElementConfigGroup::AddConfigOptionElement(const json& option_json) { } static nlohmann::json get_options(std::string& config_key) { - if (config_key_is_base_group(config_key)) { - return get_group_json(config_key); + if (recomp::config::config_key_is_base_group(config_key)) { + return recomp::config::get_group_json(config_key); } - const json& group_json = get_json_from_key(config_key); + const json& group_json = recomp::config::get_json_from_key(config_key); return group_json["options"]; } @@ -103,7 +98,7 @@ void ElementConfigGroup::OnAttributeChange(const Rml::ElementAttributes& changed auto config_store_key_attr = changed_attributes.find("recomp-data"); if (config_store_key_attr != changed_attributes.end() && config_store_key_attr->second.GetType() == Rml::Variant::STRING) { config_key = config_store_key_attr->second.Get(); - bool is_base_group = config_key_is_base_group(config_key); + bool is_base_group = recomp::config::config_key_is_base_group(config_key); option_type = ConfigOptionType::Label; @@ -113,13 +108,13 @@ void ElementConfigGroup::OnAttributeChange(const Rml::ElementAttributes& changed ToggleTextLabelVisibility(false); } else { try { - auto value = recomp::get_config_store_value("translations/" + config_key); + auto value = recomp::config::get_config_store_value("translations/" + config_key); SetTextLabel(value); } catch (const std::runtime_error& e) { SetTextLabel(e.what()); } - const json& group_json = get_json_from_key(config_key); + const json& group_json = recomp::config::get_json_from_key(config_key); from_json(group_json["type"], option_type); assert( diff --git a/src/ui/elements/ElementConfigGroup.h b/src/ui/elements/ElementConfigGroup.h index 9b8bf73..04d2aea 100644 --- a/src/ui/elements/ElementConfigGroup.h +++ b/src/ui/elements/ElementConfigGroup.h @@ -1,9 +1,7 @@ #ifndef RECOMPUI_ELEMENTS_CONFIG_GROUP_H #define RECOMPUI_ELEMENTS_CONFIG_GROUP_H -#include "RmlUi/Core/Element.h" -#include "json/json.hpp" -#include "../config_options/ConfigOption.h" +#include "common.h" namespace recompui { @@ -17,7 +15,7 @@ public: ElementConfigGroup(const Rml::String& tag); virtual ~ElementConfigGroup(); - ConfigOptionType option_type; + recomp::config::ConfigOptionType option_type; std::string config_key; protected: void OnAttributeChange(const Rml::ElementAttributes& changed_attributes); diff --git a/src/ui/elements/ElementConfigOption.cpp b/src/ui/elements/ElementConfigOption.cpp index 7f0d64e..724604d 100644 --- a/src/ui/elements/ElementConfigOption.cpp +++ b/src/ui/elements/ElementConfigOption.cpp @@ -1,17 +1,12 @@ #include "ElementConfigOption.h" -#include "../config_options/ConfigOption.h" -#include "../config_options/ConfigRegistry.h" -#include "ElementOptionTypeCheckbox.h" -#include "ElementOptionTypeRadioTabs.h" -#include "ElementOptionTypeRange.h" -#include "librecomp/config_store.hpp" + +#include "../ui_elements.h" #include -#include "recomp_ui.h" -#include -#include + using json = nlohmann::json; +using ConfigOptionType = recomp::config::ConfigOptionType; namespace recompui { @@ -50,9 +45,14 @@ ElementConfigOption::~ElementConfigOption() RemoveEventListener(Rml::EventId::Mouseover, this, true); } +Rml::Element *ElementConfigOption::GetLabel() { + Rml::ElementText *text_label = (Rml::ElementText *)GetElementById("config-opt-label"); + return text_label->GetParentNode(); +} + void ElementConfigOption::SetTextLabel(const std::string& s) { - Rml::ElementText *label = (Rml::ElementText *)GetElementById("config-opt-label"); - label->SetText(s); + Rml::ElementText *text_label = (Rml::ElementText *)GetElementById("config-opt-label"); + text_label->SetText(s); DirtyLayout(); } @@ -68,8 +68,8 @@ static void add_option_el(Rml::ElementDocument *doc, Rml::Element *wrapper, cons void ElementConfigOption::AddOptionTypeElement() { ConfigOptionType el_option_type = ConfigOptionType::Label; - const json& option_json = get_json_from_key(config_key); - from_json(option_json["type"], el_option_type); + const json& option_json = recomp::config::get_json_from_key(config_key); + recomp::config::from_json(option_json["type"], el_option_type); Rml::Element *wrapper = GetOptionTypeWrapper(); Rml::ElementDocument *doc = GetOwnerDocument(); @@ -82,6 +82,10 @@ void ElementConfigOption::AddOptionTypeElement() { add_option_el(doc, wrapper, "recomp-option-type-checkbox", config_key); break; } + case ConfigOptionType::Color: { + add_option_el(doc, wrapper, "recomp-option-type-color", config_key); + break; + } case ConfigOptionType::RadioTabs: { add_option_el(doc, wrapper, "recomp-option-type-radio-tabs", config_key); break; @@ -107,8 +111,11 @@ void ElementConfigOption::OnAttributeChange(const Rml::ElementAttributes& change SetClass(config_option_base_class_hz, true); } + Rml::Element *label = GetLabel(); + label->SetAttribute("for", "input__" + config_key); + try { - auto value = recomp::get_config_store_value("translations/" + config_key); + auto value = recomp::config::get_config_store_value("translations/" + config_key); SetTextLabel(value); printf("found type and translation\n"); AddOptionTypeElement(); diff --git a/src/ui/elements/ElementConfigOption.h b/src/ui/elements/ElementConfigOption.h index 4794ed4..d292146 100644 --- a/src/ui/elements/ElementConfigOption.h +++ b/src/ui/elements/ElementConfigOption.h @@ -1,9 +1,7 @@ #ifndef RECOMPUI_ELEMENTS_CONFIG_OPTION_H #define RECOMPUI_ELEMENTS_CONFIG_OPTION_H -#include "RmlUi/Core/Element.h" -#include "../config_options/ConfigOption.h" -#include "RmlUi/Core/EventListener.h" +#include "common.h" namespace recompui { @@ -17,12 +15,13 @@ public: ElementConfigOption(const Rml::String& tag); virtual ~ElementConfigOption(); - ConfigOptionType option_type; + recomp::config::ConfigOptionType option_type; std::string config_key; bool in_checkbox_group = false; protected: void OnAttributeChange(const Rml::ElementAttributes& changed_attributes); + Rml::Element *GetLabel(void); void SetTextLabel(const std::string& s); void AddOptionTypeElement(); Rml::Element *GetOptionTypeWrapper(); diff --git a/src/ui/elements/ElementDescription.cpp b/src/ui/elements/ElementDescription.cpp index 64e5359..8069eff 100644 --- a/src/ui/elements/ElementDescription.cpp +++ b/src/ui/elements/ElementDescription.cpp @@ -1,10 +1,6 @@ - #include "ElementDescription.h" -#include "librecomp/config_store.hpp" -#include "../config_options/ConfigRegistry.h" + #include -#include -#include using json = nlohmann::json; @@ -50,7 +46,7 @@ void ElementDescription::OnAttributeChange(const Rml::ElementAttributes& changed config_key = config_store_key_attr->second.Get(); try { - auto value = recomp::get_config_store_value("translations/" + config_key + ":description"); + auto value = recomp::config::get_config_store_value("translations/" + config_key + ":description"); update_text(value); } catch (const std::runtime_error& e) { update_text(default_contents); diff --git a/src/ui/elements/ElementDescription.h b/src/ui/elements/ElementDescription.h index 73fb9e8..e89b0fb 100644 --- a/src/ui/elements/ElementDescription.h +++ b/src/ui/elements/ElementDescription.h @@ -1,8 +1,7 @@ #ifndef RECOMPUI_ELEMENT_DESCRIPTION_H #define RECOMPUI_ELEMENT_DESCRIPTION_H -#include "RmlUi/Core/Element.h" -#include "../config_options/ConfigOption.h" +#include "common.h" namespace recompui { diff --git a/src/ui/elements/ElementOptionTypeCheckbox.cpp b/src/ui/elements/ElementOptionTypeCheckbox.cpp index c0cdd3a..883a193 100644 --- a/src/ui/elements/ElementOptionTypeCheckbox.cpp +++ b/src/ui/elements/ElementOptionTypeCheckbox.cpp @@ -1,10 +1,7 @@ - #include "ElementOptionTypeCheckbox.h" -#include "librecomp/config_store.hpp" -#include "../config_options/ConfigRegistry.h" + #include -#include -#include + using json = nlohmann::json; @@ -51,8 +48,8 @@ void ElementOptionTypeCheckbox::set_checked(bool checked) { void ElementOptionTypeCheckbox::init_option(std::string& _config_key) { config_key = _config_key; - const json& option_json = get_json_from_key(config_key); - int value = recomp::get_config_store_value(config_key); + const json& option_json = recomp::config::get_json_from_key(config_key); + int value = recomp::config::get_config_store_value(config_key); set_checked(value); } @@ -63,8 +60,8 @@ void ElementOptionTypeCheckbox::ProcessEvent(Rml::Event& event) { if (event.GetPhase() == Rml::EventPhase::Capture || event.GetPhase() == Rml::EventPhase::Target) { - bool new_value = !recomp::get_config_store_value(config_key); - recomp::set_config_store_value(config_key, new_value); + bool new_value = !recomp::config::get_config_store_value(config_key); + recomp::config::set_config_store_value(config_key, new_value); set_checked(new_value); } } diff --git a/src/ui/elements/ElementOptionTypeCheckbox.h b/src/ui/elements/ElementOptionTypeCheckbox.h index 0d9326d..672911e 100644 --- a/src/ui/elements/ElementOptionTypeCheckbox.h +++ b/src/ui/elements/ElementOptionTypeCheckbox.h @@ -1,10 +1,7 @@ #ifndef RECOMPUI_ELEMENT_OPTION_TYPE_CHECKBOX_H #define RECOMPUI_ELEMENT_OPTION_TYPE_CHECKBOX_H -#include "RmlUi/Core/Element.h" -#include "RmlUi/Core/Elements/ElementFormControlInput.h" -#include "RmlUi/Core/EventListener.h" -#include "../config_options/ConfigOption.h" +#include "common.h" namespace recompui { @@ -17,7 +14,7 @@ public: std::string config_key; - const ConfigOptionType option_type = ConfigOptionType::Checkbox; + const recomp::config::ConfigOptionType option_type = recomp::config::ConfigOptionType::Checkbox; protected: Rml::ElementFormControlInput *get_input(); diff --git a/src/ui/elements/ElementOptionTypeColor.cpp b/src/ui/elements/ElementOptionTypeColor.cpp new file mode 100644 index 0000000..5e2576a --- /dev/null +++ b/src/ui/elements/ElementOptionTypeColor.cpp @@ -0,0 +1,185 @@ +#include "ElementOptionTypeColor.h" + +#include +#include + + +using json = nlohmann::json; + +namespace recompui { + +static const std::string range_input_id = "recomp-color-range:"; // + H/S/V +static const std::string range_label_id = "recomp-color-range-label:"; // + H/S/V +static const std::string hsv_label[] = {"H", "S", "V"}; + +static const std::string preview_block_id = "recomp-color-block"; + +static const std::string cls_base = "config-option-color"; +static const std::string cls_color_preview_wrapper = cls_base + "__preview-wrapper"; +static const std::string cls_color_preview_block = cls_base + "__preview-block"; +static const std::string cls_color_hsv_wrapper = cls_base + "__hsv-wrapper"; + +// TODO: use these 3 directly from ElementOptionTypeRange +static const std::string range_cls_base = "config-option-range"; +static const std::string range_cls_label = cls_base + "__label"; +static const std::string range_cls_range_input = cls_base + "__range-input"; + + +ElementOptionTypeColor::ElementOptionTypeColor(const Rml::String& tag) : Rml::Element(tag) +{ + SetAttribute("recomp-store-element", true); + SetClass(cls_base, true); + + hsv.h = 0; + hsv.s = 0; + hsv.v = 0; + + Rml::ElementDocument *doc = GetOwnerDocument(); + + { + Rml::Element *preview_wrapper = AppendChild(doc->CreateElement("div")); + preview_wrapper->SetClass(cls_color_preview_wrapper, true); + { + Rml::Element *preview_block = preview_wrapper->AppendChild(doc->CreateElement("div")); + preview_block->SetClass(cls_color_preview_block, true); + preview_block->SetId(preview_block_id); + + Rml::Element *hsv_wrapper = preview_wrapper->AppendChild(doc->CreateElement("div")); + hsv_wrapper->SetClass(cls_color_hsv_wrapper, true); + for (int i = 0; i < 3; i++) { + const auto &label = hsv_label[i]; + + Rml::Element *range_wrapper = hsv_wrapper->AppendChild(doc->CreateElement("div")); + range_wrapper->SetClass(range_cls_base, true); + { + Rml::Element *label_el = range_wrapper->AppendChild(doc->CreateElement("label")); + label_el->SetClass(range_cls_label, true); + label_el->SetId(range_label_id); + { + Rml::Element *text_node = label_el->AppendChild(doc->CreateTextNode("")); + text_node->SetId(range_label_id + label); + } + + Rml::ElementFormControlInput *range_el = (Rml::ElementFormControlInput *)range_wrapper->AppendChild(doc->CreateElement("input")); + range_el->SetClass(range_cls_range_input, true); + range_el->SetId(range_input_id + label); + range_el->SetAttribute("type", "range"); + range_el->SetAttribute("min", 0); + range_el->SetAttribute("hsv-index", i); + if (i == 0) { + range_el->SetValue(std::to_string((int)round(hsv[i]))); + range_el->SetAttribute("max", 360); + } else { + range_el->SetValue(std::to_string((int)round(hsv[i] * 100.0f))); + range_el->SetAttribute("max", 100); + } + range_el->AddEventListener(Rml::EventId::Change, this, false); + } + } + + } + + // TODO: RGB hex input + } + +} + +ElementOptionTypeColor::~ElementOptionTypeColor() +{ + Rml::ElementList elements; + GetElementsByTagName(elements, "input"); + for (int i = 0; i < elements.size(); i++) { + Rml::Element *el = elements[i]; + el->RemoveEventListener(Rml::EventId::Click, this, false); + } +} + +void ElementOptionTypeColor::set_value_label(int hsvIndex) { + const auto value = hsv[hsvIndex]; + const auto& which = hsv_label[hsvIndex]; + + Rml::ElementText *text_label = (Rml::ElementText *)GetElementById(range_label_id + which); + if (hsvIndex == 0) { + text_label->SetText(which + ": " + std::to_string((int)round(value))); + } else { + text_label->SetText(which + ": " + std::to_string((int)round(value * 100.0f))); + } + DirtyLayout(); +} + +void ElementOptionTypeColor::set_preview_block_rgb(RgbColor rgb) { + Rml::Element *color_block = GetElementById(preview_block_id); + char hex_buf[8]; // Enough to hold "#RRGGBB\0" + sprintf(hex_buf, "#%02x%02x%02x", rgb.r, rgb.g, rgb.b); + const std::string hex_val = std::string(hex_buf); + + color_block->SetProperty("background-color", hex_val); +} + +void ElementOptionTypeColor::set_config_store_rgb() { + RgbColor rgb; + HsvFToRgb(hsv, rgb); + recomp::config::set_config_store_value(config_key + ":r", rgb.r); + recomp::config::set_config_store_value(config_key + ":g", rgb.g); + recomp::config::set_config_store_value(config_key + ":b", rgb.b); + set_preview_block_rgb(rgb); +} + + +void ElementOptionTypeColor::init_option(std::string& _config_key) { + + + config_key = _config_key; + + const json& option_json = recomp::config::get_json_from_key(config_key); + + RgbColor col; + HsvColor hsv_uc; + col.r = recomp::config::get_config_store_value(config_key + ":r"); + col.g = recomp::config::get_config_store_value(config_key + ":g"); + col.b = recomp::config::get_config_store_value(config_key + ":b"); + + RgbToHsv(col, hsv_uc); + hsv.h = std::clamp(hsv_uc.h * 360.0f, 0.0f, 360.0f); + hsv.s = std::clamp((float)hsv_uc.s / 255.0f, 0.0f, 1.0f); + hsv.v = std::clamp((float)hsv_uc.v / 255.0f, 0.0f, 1.0f); + + set_preview_block_rgb(col); + + for (int i = 0; i < 3; i++) { + const auto &label = hsv_label[i]; + + Rml::ElementFormControlInput *range = (Rml::ElementFormControlInput *)GetElementById(range_input_id + label); + if (i == 0) { + range->SetValue(std::to_string((int)round(hsv[i]))); + } else { + range->SetValue(std::to_string((int)round(hsv[i] * 100.0f))); + } + set_value_label(i); + } +} + +void ElementOptionTypeColor::ProcessEvent(Rml::Event& event) +{ + if (event == Rml::EventId::Change) + { + if (event.GetPhase() == Rml::EventPhase::Bubble || event.GetPhase() == Rml::EventPhase::Target) + { + Rml::Element *target = event.GetTargetElement(); + auto val_variant = target->GetAttribute("value"); + int new_value = val_variant->Get(); + + auto idx_variant = target->GetAttribute("hsv-index"); + auto hsv_index = idx_variant->Get(); + if (hsv_index == 0) { + hsv[hsv_index] = (float)new_value; + } else { + hsv[hsv_index] = (float)new_value / 100.0f; + } + set_value_label(hsv_index); + set_config_store_rgb(); + } + } +} + +} // namespace Rml diff --git a/src/ui/elements/ElementOptionTypeColor.h b/src/ui/elements/ElementOptionTypeColor.h new file mode 100644 index 0000000..e1b25d5 --- /dev/null +++ b/src/ui/elements/ElementOptionTypeColor.h @@ -0,0 +1,26 @@ +#ifndef RECOMPUI_ELEMENT_OPTION_TYPE_COLOR_H +#define RECOMPUI_ELEMENT_OPTION_TYPE_COLOR_H + +#include "common.h" + +namespace recompui { + +class ElementOptionTypeColor : public Rml::Element, public Rml::EventListener { +public: + ElementOptionTypeColor(const Rml::String& tag); + virtual ~ElementOptionTypeColor(); + + std::string config_key; + HsvColorF hsv; + + void init_option(std::string& _config_key); +protected: + void set_value_label(int hsvIndex); + void set_config_store_rgb(); + void set_preview_block_rgb(RgbColor rgb); + + void ProcessEvent(Rml::Event& event) override; +}; + +} // namespace recompui +#endif diff --git a/src/ui/elements/ElementOptionTypeRadioTabs.cpp b/src/ui/elements/ElementOptionTypeRadioTabs.cpp index d9c92fd..4d35fd5 100644 --- a/src/ui/elements/ElementOptionTypeRadioTabs.cpp +++ b/src/ui/elements/ElementOptionTypeRadioTabs.cpp @@ -1,10 +1,7 @@ #include "ElementOptionTypeRadioTabs.h" -#include "librecomp/config_store.hpp" -#include "../config_options/ConfigRegistry.h" + #include -#include -#include using json = nlohmann::json; @@ -47,8 +44,8 @@ void ElementOptionTypeRadioTabs::set_cur_option(int opt) { void ElementOptionTypeRadioTabs::init_option(std::string& _config_key) { config_key = _config_key; - const json& option_json = get_json_from_key(config_key); - int opt = recomp::get_config_store_value(config_key); + const json& option_json = recomp::config::get_json_from_key(config_key); + int opt = recomp::config::get_config_store_value(config_key); const json& opt_array = option_json["values"]; for (int i = 0; i < opt_array.size(); i++) { @@ -57,7 +54,7 @@ void ElementOptionTypeRadioTabs::init_option(std::string& _config_key) { const std::string opt_id = radio_input_id + config_key + "--" + opt_val; const std::string translation_key = "translations/" + config_key + "/values/" + opt_val; - const std::string& opt_text = recomp::get_config_store_value(translation_key); + const std::string& opt_text = recomp::config::get_config_store_value(translation_key); Rml::Element *radio_el = AppendChild(GetOwnerDocument()->CreateElement("input")); @@ -88,7 +85,7 @@ void ElementOptionTypeRadioTabs::ProcessEvent(Rml::Event& event) Rml::Element *target = event.GetTargetElement(); auto val_variant = target->GetAttribute("value"); int new_value = val_variant->Get(); - recomp::set_config_store_value(config_key, new_value); + recomp::config::set_config_store_value(config_key, new_value); set_cur_option(new_value); } } diff --git a/src/ui/elements/ElementOptionTypeRadioTabs.h b/src/ui/elements/ElementOptionTypeRadioTabs.h index d38443e..05486d5 100644 --- a/src/ui/elements/ElementOptionTypeRadioTabs.h +++ b/src/ui/elements/ElementOptionTypeRadioTabs.h @@ -1,9 +1,7 @@ #ifndef RECOMPUI_ELEMENT_OPTION_TYPE_RADIO_TABS_H #define RECOMPUI_ELEMENT_OPTION_TYPE_RADIO_TABS_H -#include "RmlUi/Core/Element.h" -#include "RmlUi/Core/Elements/ElementFormControlInput.h" -#include "RmlUi/Core/EventListener.h" +#include "common.h" namespace recompui { diff --git a/src/ui/elements/ElementOptionTypeRange.cpp b/src/ui/elements/ElementOptionTypeRange.cpp index 7ee5fed..f857d12 100644 --- a/src/ui/elements/ElementOptionTypeRange.cpp +++ b/src/ui/elements/ElementOptionTypeRange.cpp @@ -1,7 +1,6 @@ #include "ElementOptionTypeRange.h" -#include "librecomp/config_store.hpp" -#include "../config_options/ConfigRegistry.h" + #include #include #include @@ -58,13 +57,13 @@ void ElementOptionTypeRange::set_value_label(int value) { void ElementOptionTypeRange::init_option(std::string& _config_key) { config_key = _config_key; - const json& option_json = get_json_from_key(config_key); + const json& option_json = recomp::config::get_json_from_key(config_key); - const int value = recomp::get_config_store_value(config_key); - suffix = get_string_in_json_with_default(option_json, "suffix", ""); - const int min = get_value_in_json(option_json, "min"); - const int max = get_value_in_json(option_json, "max"); - const int step = get_value_in_json_with_default(option_json, "step", 1); + const int value = recomp::config::get_config_store_value(config_key); + suffix = recomp::config::get_string_in_json_with_default(option_json, "suffix", ""); + const int min = recomp::config::get_value_in_json(option_json, "min"); + const int max = recomp::config::get_value_in_json(option_json, "max"); + const int step = recomp::config::get_value_in_json_with_default(option_json, "step", 1); Rml::ElementFormControlInput *range = (Rml::ElementFormControlInput *)GetElementById(range_input_id); range->SetAttribute("min", min); @@ -77,7 +76,6 @@ void ElementOptionTypeRange::init_option(std::string& _config_key) { void ElementOptionTypeRange::ProcessEvent(Rml::Event& event) { - // Forward clicks to the target. if (event == Rml::EventId::Change) { if (event.GetPhase() == Rml::EventPhase::Bubble || event.GetPhase() == Rml::EventPhase::Target) @@ -85,7 +83,7 @@ void ElementOptionTypeRange::ProcessEvent(Rml::Event& event) Rml::ElementFormControlInput *target = (Rml::ElementFormControlInput *)event.GetTargetElement(); auto val_s = target->GetValue(); int new_value = std::stoi(val_s); - recomp::set_config_store_value(config_key, new_value); + recomp::config::set_config_store_value(config_key, new_value); set_value_label(new_value); } } diff --git a/src/ui/elements/ElementOptionTypeRange.h b/src/ui/elements/ElementOptionTypeRange.h index 58661af..f619220 100644 --- a/src/ui/elements/ElementOptionTypeRange.h +++ b/src/ui/elements/ElementOptionTypeRange.h @@ -1,9 +1,7 @@ #ifndef RECOMPUI_ELEMENT_OPTION_TYPE_RANGE_H #define RECOMPUI_ELEMENT_OPTION_TYPE_RANGE_H -#include "RmlUi/Core/Element.h" -#include "RmlUi/Core/Elements/ElementFormControlInput.h" -#include "RmlUi/Core/EventListener.h" +#include "common.h" namespace recompui { diff --git a/src/ui/elements/common.h b/src/ui/elements/common.h new file mode 100644 index 0000000..5d91518 --- /dev/null +++ b/src/ui/elements/common.h @@ -0,0 +1,10 @@ +#ifndef RECOMPUI_ELEMENTS_COMMON +#define RECOMPUI_ELEMENTS_COMMON +// Common includes for custom recomp elements + +#include "recomp_ui.h" +#include "librecomp/config.hpp" +#include "json/json.hpp" +#include "RmlUi/Core.h" + +#endif diff --git a/src/ui/ui_elements.cpp b/src/ui/ui_elements.cpp index 77be3b6..768b2b7 100644 --- a/src/ui/ui_elements.cpp +++ b/src/ui/ui_elements.cpp @@ -1,5 +1,5 @@ #include "ui_elements.h" -#include "librecomp/config_store.hpp" +#include "librecomp/config.hpp" struct RecompElementConfig { Rml::String tag; @@ -13,14 +13,15 @@ static RecompElementConfig custom_elements[] = { CUSTOM_ELEMENT("recomp-config-group", recompui::ElementConfigGroup), CUSTOM_ELEMENT("recomp-config-option", recompui::ElementConfigOption), CUSTOM_ELEMENT("recomp-option-type-checkbox", recompui::ElementOptionTypeCheckbox), + CUSTOM_ELEMENT("recomp-option-type-color", recompui::ElementOptionTypeColor), CUSTOM_ELEMENT("recomp-option-type-radio-tabs", recompui::ElementOptionTypeRadioTabs), CUSTOM_ELEMENT("recomp-option-type-range", recompui::ElementOptionTypeRange), }; void recompui::register_custom_elements() { - recomp::set_config_store_value_and_default("ligma_balls", "hello!", "whats up"); - recomp::set_config_store_default_value("ligma_balls2", "12345"); - recomp::set_config_store_value("ligma_balls3", "hello!"); + recomp::config::set_config_store_value_and_default("ligma_balls", "hello!", "whats up"); + recomp::config::set_config_store_default_value("ligma_balls2", "12345"); + recomp::config::set_config_store_value("ligma_balls3", "hello!"); for (auto& element_config : custom_elements) { Rml::Factory::RegisterElementInstancer(element_config.tag, element_config.instancer.get()); } diff --git a/src/ui/ui_elements.h b/src/ui/ui_elements.h index 76cf625..99a05b1 100644 --- a/src/ui/ui_elements.h +++ b/src/ui/ui_elements.h @@ -7,6 +7,7 @@ #include "elements/ElementConfigOption.h" #include "elements/ElementConfigGroup.h" #include "elements/ElementOptionTypeCheckbox.h" +#include "elements/ElementOptionTypeColor.h" #include "elements/ElementOptionTypeRadioTabs.h" #include "elements/ElementOptionTypeRange.h" #include "elements/ElementDescription.h" diff --git a/src/ui/ui_renderer.cpp b/src/ui/ui_renderer.cpp index a6be11e..320e673 100644 --- a/src/ui/ui_renderer.cpp +++ b/src/ui/ui_renderer.cpp @@ -29,7 +29,7 @@ #include "RmlUi_Platform_SDL.h" #include "ui_elements.h" -#include "config_options/ConfigRegistry.h" +#include "librecomp/config.hpp" #include "InterfaceVS.hlsl.spirv.h" #include "InterfacePS.hlsl.spirv.h" @@ -1149,11 +1149,11 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) { std::filesystem::path test_conf_trans_path = "config_example.cheats.en_us.json"; if (std::filesystem::exists(test_conf_path)) { const std::string s = read_file_to_string(test_conf_path); - recompui::register_config(read_file_to_string(test_conf_path), "cheats"); + recomp::config::register_config(read_file_to_string(test_conf_path), "cheats"); printf("SUCC CONF\n"); if (std::filesystem::exists(test_conf_trans_path)) { - recompui::register_translation(read_file_to_string("config_example.cheats.en_us.json"), "cheats"); + recomp::config::register_translation(read_file_to_string("config_example.cheats.en_us.json"), "cheats"); printf("SUCC TRANSLATION\n"); } else { printf("FAIL TRANSLATION"); diff --git a/src/ui/util/hsv.cpp b/src/ui/util/hsv.cpp new file mode 100644 index 0000000..3de75ea --- /dev/null +++ b/src/ui/util/hsv.cpp @@ -0,0 +1,145 @@ +#include "hsv.h" +#include // std::min, std::max and std::clamp +#include + +namespace recompui { + +void HsvToRgb(HsvColor& hsv, RgbColor& rgb) +{ + unsigned char region, remainder, p, q, t; + + if (hsv.s == 0) + { + rgb.r = hsv.v; + rgb.g = hsv.v; + rgb.b = hsv.v; + return; + } + + region = hsv.h / 43; + remainder = (hsv.h - (region * 43)) * 6; + + p = (hsv.v * (255 - hsv.s)) >> 8; + q = (hsv.v * (255 - ((hsv.s * remainder) >> 8))) >> 8; + t = (hsv.v * (255 - ((hsv.s * (255 - remainder)) >> 8))) >> 8; + + switch (region) + { + case 0: + rgb.r = hsv.v; rgb.g = t; rgb.b = p; + break; + case 1: + rgb.r = q; rgb.g = hsv.v; rgb.b = p; + break; + case 2: + rgb.r = p; rgb.g = hsv.v; rgb.b = t; + break; + case 3: + rgb.r = p; rgb.g = q; rgb.b = hsv.v; + break; + case 4: + rgb.r = t; rgb.g = p; rgb.b = hsv.v; + break; + default: + rgb.r = hsv.v; rgb.g = p; rgb.b = q; + break; + } +} + +void RgbToHsv(RgbColor& rgb, HsvColor& hsv) +{ + unsigned char rgbMin, rgbMax; + + rgbMin = rgb.r < rgb.g ? (rgb.r < rgb.b ? rgb.r : rgb.b) : (rgb.g < rgb.b ? rgb.g : rgb.b); + rgbMax = rgb.r > rgb.g ? (rgb.r > rgb.b ? rgb.r : rgb.b) : (rgb.g > rgb.b ? rgb.g : rgb.b); + + hsv.v = rgbMax; + if (hsv.v == 0) + { + hsv.h = 0; + hsv.s = 0; + return; + } + + hsv.s = 255 * long(rgbMax - rgbMin) / hsv.v; + if (hsv.s == 0) + { + hsv.h = 0; + return; + } + + if (rgbMax == rgb.r) + hsv.h = 0 + 43 * (rgb.g - rgb.b) / (rgbMax - rgbMin); + else if (rgbMax == rgb.g) + hsv.h = 85 + 43 * (rgb.b - rgb.r) / (rgbMax - rgbMin); + else + hsv.h = 171 + 43 * (rgb.r - rgb.g) / (rgbMax - rgbMin); +} + +static unsigned char clamp_255(float f) { + return std::clamp((int)round(f), 0, 255); +} + +void HsvFToRgb(HsvColorF in, RgbColor& out) +{ + float hh, p, q, t, ff; + long i; + + unsigned char val = clamp_255(in.v * 255.0f); + + if (in.s <= 0.0) { + out.r = val; + out.g = val; + out.b = val; + return; + } + + hh = in.h; + if (hh >= 360.0f) hh = 0.0f; + hh /= 60.0f; + i = (float)hh; + ff = hh - i; + p = in.v * (1.0f - in.s); + q = in.v * (1.0f - (in.s * ff)); + t = in.v * (1.0f - (in.s * (1.0f - ff))); + + unsigned char up = clamp_255(p * 255.0f); + unsigned char uq = clamp_255(q * 255.0f); + unsigned char ut = clamp_255(t * 255.0f); + + switch (i) { + case 0: + out.r = val; + out.g = ut; + out.b = up; + return; + case 1: + out.r = uq; + out.g = val; + out.b = up; + return; + case 2: + out.r = up; + out.g = val; + out.b = ut; + return; + case 3: + out.r = up; + out.g = uq; + out.b = val; + return; + case 4: + out.r = ut; + out.g = up; + out.b = val; + return; + case 5: + default: + out.r = val; + out.g = up; + out.b = uq; + return; + } +} + +} diff --git a/src/ui/util/hsv.h b/src/ui/util/hsv.h new file mode 100644 index 0000000..3bd9bc7 --- /dev/null +++ b/src/ui/util/hsv.h @@ -0,0 +1,77 @@ +#ifndef RECOMP_UI_HSV +#define RECOMP_UI_HSV + + +namespace recompui { + typedef struct RgbColor + { + union { + struct { + unsigned char r; + unsigned char g; + unsigned char b; + }; + unsigned char data[3]; // Array access + }; + + // Operator[] to access members by index + unsigned char& operator[](int index) { + return data[index]; + } + + // Const version for read-only access + const unsigned char& operator[](int index) const { + return data[index]; + } + } RgbColor; + + typedef struct HsvColor + { + union { + struct { + unsigned char h; + unsigned char s; + unsigned char v; + }; + unsigned char data[3]; // Array access + }; + + // Operator[] to access members by index + unsigned char& operator[](int index) { + return data[index]; + } + + // Const version for read-only access + const unsigned char& operator[](int index) const { + return data[index]; + } + } HsvColor; + + typedef struct HsvColorF + { + union { + struct { + float h; // 0-360 + float s; // 0-1 + float v; // 0-1 + }; + float data[3]; // Array access + }; + + // Operator[] to access members by index + float& operator[](int index) { + return data[index]; + } + + // Const version for read-only access + const float& operator[](int index) const { + return data[index]; + } + } HsvColorF; + + void HsvToRgb(HsvColor& hsv, RgbColor& rgb); + void HsvFToRgb(HsvColorF in, RgbColor& out); + void RgbToHsv(RgbColor& rgb, HsvColor& hsv); +} + +#endif