diff --git a/src/core/common/version.hpp b/src/core/common/version.hpp index b011ea6..65f69ed 100644 --- a/src/core/common/version.hpp +++ b/src/core/common/version.hpp @@ -6,7 +6,7 @@ namespace clrsync::core { -const std::string GIT_SEMVER = "0.1.7+git.g82998d6"; +const std::string GIT_SEMVER = "0.1.7+git.g4ada2c4"; const std::string version_string(); } // namespace clrsync::core diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 345fbd0..fd38bb6 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -17,6 +17,13 @@ set(GUI_SOURCES widgets/styled_checkbox.cpp widgets/autocomplete.cpp widgets/form_field.cpp + widgets/error_message.cpp + widgets/settings_buttons.cpp + widgets/section_header.cpp + widgets/link_button.cpp + widgets/centered_text.cpp + widgets/validation_message.cpp + widgets/template_controls.cpp layout/main_layout.cpp platform/windows/font_loader_windows.cpp platform/linux/font_loader_linux.cpp diff --git a/src/gui/views/about_window.cpp b/src/gui/views/about_window.cpp index 43b75e1..5fbcd4b 100644 --- a/src/gui/views/about_window.cpp +++ b/src/gui/views/about_window.cpp @@ -1,11 +1,11 @@ #include "gui/views/about_window.hpp" #include "core/common/version.hpp" +#include "gui/widgets/centered_text.hpp" #include "gui/widgets/colors.hpp" +#include "gui/widgets/link_button.hpp" #include "imgui.h" -about_window::about_window() -{ -} +about_window::about_window() = default; void about_window::render(const clrsync::core::palette &pal) { @@ -16,21 +16,13 @@ void about_window::render(const clrsync::core::palette &pal) if (ImGui::Begin("About clrsync", &m_visible, ImGuiWindowFlags_NoResize)) { - const float window_width = ImGui::GetContentRegionAvail().x; + using namespace clrsync::gui::widgets; - ImGui::PushFont(ImGui::GetFont()); - const char *title = "clrsync"; - const float title_size = ImGui::CalcTextSize(title).x; - ImGui::SetCursorPosX((window_width - title_size) * 0.5f); - ImVec4 title_color = clrsync::gui::widgets::palette_color(pal, "info", "accent"); - ImGui::TextColored(title_color, "%s", title); - ImGui::PopFont(); + ImVec4 title_color = palette_color(pal, "info", "accent"); + centered_text("clrsync", title_color); - std::string version = "Version " + clrsync::core::version_string(); - const float version_size = ImGui::CalcTextSize(version.c_str()).x; - ImGui::SetCursorPosX((window_width - version_size) * 0.5f); - ImVec4 subtitle_color = clrsync::gui::widgets::palette_color(pal, "editor_inactive", "foreground"); - ImGui::TextColored(subtitle_color, "%s", version.c_str()); + ImVec4 subtitle_color = palette_color(pal, "editor_inactive", "foreground"); + centered_text("Version " + clrsync::core::version_string(), subtitle_color); ImGui::Spacing(); ImGui::Separator(); @@ -40,47 +32,27 @@ void about_window::render(const clrsync::core::palette &pal) ImGui::Spacing(); ImGui::Spacing(); - ImGui::Separator(); ImGui::Spacing(); ImGui::Text("Links:"); - const float button_width = 200.0f; - const float spacing = ImGui::GetStyle().ItemSpacing.x; - const float total_width = 2.0f * button_width + spacing; - ImGui::SetCursorPosX((window_width - total_width) * 0.5f); + constexpr float button_width = 200.0f; + float spacing = ImGui::GetStyle().ItemSpacing.x; + float total_width = 2.0f * button_width + spacing; - if (ImGui::Button("GitHub Repository", ImVec2(button_width, 0))) - { -#ifdef _WIN32 - system("start https://github.com/obsqrbtz/clrsync"); -#elif __APPLE__ - system("open https://github.com/obsqrbtz/clrsync"); -#else - system("xdg-open https://github.com/obsqrbtz/clrsync"); -#endif - } - ImGui::SameLine(); - - if (ImGui::Button("Documentation", ImVec2(button_width, 0))) - { -#ifdef _WIN32 - system("start https://binarygoose.dev/projects/clrsync/overview/"); -#elif __APPLE__ - system("open https://binarygoose.dev/projects/clrsync/overview/"); -#else - system("xdg-open https://binarygoose.dev/projects/clrsync/overview/"); -#endif - } + centered_buttons(total_width, [button_width]() { + link_button("GitHub Repository", "https://github.com/obsqrbtz/clrsync", button_width); + ImGui::SameLine(); + link_button("Documentation", "https://binarygoose.dev/projects/clrsync/overview/", button_width); + }); ImGui::Spacing(); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); - ImVec4 license_color = clrsync::gui::widgets::palette_color(pal, "editor_inactive", "foreground"); - ImGui::TextColored(license_color, "MIT License"); + ImGui::TextColored(subtitle_color, "MIT License"); ImGui::TextWrapped( "Copyright (c) 2025 Daniel Dada\n\n" "Permission is hereby granted, free of charge, to any person obtaining a copy " diff --git a/src/gui/views/settings_window.cpp b/src/gui/views/settings_window.cpp index ac317ba..3d6ebf1 100644 --- a/src/gui/views/settings_window.cpp +++ b/src/gui/views/settings_window.cpp @@ -1,25 +1,29 @@ #include "gui/views/settings_window.hpp" #include "core/common/error.hpp" #include "core/config/config.hpp" -#include "gui/widgets/colors.hpp" +#include "gui/widgets/section_header.hpp" #include "gui/ui_manager.hpp" #include "imgui.h" -#include settings_window::settings_window(clrsync::gui::ui_manager* ui_mgr) - : m_font_size(14), m_selected_font_idx(0), m_settings_changed(false), m_current_tab(0), - m_ui_manager(ui_mgr) + : m_ui_manager(ui_mgr) { - m_default_theme[0] = '\0'; - m_palettes_path[0] = '\0'; - m_font[0] = '\0'; - if (m_ui_manager) m_available_fonts = m_ui_manager->get_system_fonts(); + setup_widgets(); load_settings(); } +void settings_window::setup_widgets() +{ + m_form.set_path_browse_callback([this](const std::string& current_path) -> std::string { + if (m_ui_manager) + return m_ui_manager->select_folder_dialog("Select Directory", current_path); + return ""; + }); +} + void settings_window::render() { if (!m_visible) @@ -29,8 +33,7 @@ void settings_window::render() ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); - ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoCollapse; - if (ImGui::Begin("Settings", &m_visible, window_flags)) + if (ImGui::Begin("Settings", &m_visible, ImGuiWindowFlags_NoCollapse)) { if (ImGui::BeginTabBar("SettingsTabs", ImGuiTabBarFlags_None)) { @@ -49,334 +52,188 @@ void settings_window::render() ImGui::EndTabBar(); } - render_status_messages(); + m_error.render(m_current_palette); ImGui::Separator(); - render_action_buttons(); + + clrsync::gui::widgets::settings_buttons_callbacks callbacks{ + .on_ok = [this]() { + apply_settings(); + if (!m_error.has_error()) + { + m_visible = false; + m_settings_changed = false; + } + }, + .on_apply = [this]() { + apply_settings(); + if (!m_error.has_error()) + m_settings_changed = false; + }, + .on_reset = [this]() { reset_to_defaults(); }, + .on_cancel = [this]() { + load_settings(); + m_visible = false; + } + }; + m_buttons.render(callbacks, m_settings_changed); } ImGui::End(); } void settings_window::load_settings() { - auto &cfg = clrsync::core::config::instance(); + auto& cfg = clrsync::core::config::instance(); - std::string default_theme = cfg.default_theme(); - strncpy(m_default_theme, default_theme.c_str(), sizeof(m_default_theme) - 1); - m_default_theme[sizeof(m_default_theme) - 1] = '\0'; - - std::string palettes_path = cfg.palettes_path(); - strncpy(m_palettes_path, palettes_path.c_str(), sizeof(m_palettes_path) - 1); - m_palettes_path[sizeof(m_palettes_path) - 1] = '\0'; - - std::string font = cfg.font(); - strncpy(m_font, font.c_str(), sizeof(m_font) - 1); - m_font[sizeof(m_font) - 1] = '\0'; + m_default_theme = cfg.default_theme(); + m_palettes_path = cfg.palettes_path(); + m_font = cfg.font(); + m_font_size = cfg.font_size(); m_selected_font_idx = 0; for (int i = 0; i < static_cast(m_available_fonts.size()); i++) { - if (m_available_fonts[i] == font) + if (m_available_fonts[i] == m_font) { m_selected_font_idx = i; break; } } - m_font_size = cfg.font_size(); - - m_error_message.clear(); + m_error.clear(); m_settings_changed = false; } void settings_window::apply_settings() { - auto &cfg = clrsync::core::config::instance(); + auto& cfg = clrsync::core::config::instance(); - if (strlen(m_default_theme) == 0) + if (m_default_theme.empty()) { - m_error_message = "Default theme cannot be empty"; + m_error.set("Default theme cannot be empty"); return; } - if (strlen(m_palettes_path) == 0) + if (m_palettes_path.empty()) { - m_error_message = "Palettes path cannot be empty"; + m_error.set("Palettes path cannot be empty"); return; } - if (strlen(m_font) == 0) + if (m_font.empty()) { - m_error_message = "Font cannot be empty"; + m_error.set("Font cannot be empty"); return; } if (m_font_size < 8 || m_font_size > 48) { - m_error_message = "Font size must be between 8 and 48"; + m_error.set("Font size must be between 8 and 48"); return; } auto result1 = cfg.set_default_theme(m_default_theme); if (!result1) { - m_error_message = "Failed to set default theme: " + result1.error().description(); + m_error.set("Failed to set default theme: " + result1.error().description()); return; } auto result2 = cfg.set_palettes_path(m_palettes_path); if (!result2) { - m_error_message = "Failed to set palettes path: " + result2.error().description(); + m_error.set("Failed to set palettes path: " + result2.error().description()); return; } auto result3 = cfg.set_font(m_font); if (!result3) { - m_error_message = "Failed to set font: " + result3.error().description(); + m_error.set("Failed to set font: " + result3.error().description()); return; } auto result4 = cfg.set_font_size(m_font_size); if (!result4) { - m_error_message = "Failed to set font size: " + result4.error().description(); + m_error.set("Failed to set font size: " + result4.error().description()); return; } - if (m_ui_manager) + if (m_ui_manager && !m_ui_manager->reload_font(m_font.c_str(), m_font_size)) { - if (!m_ui_manager->reload_font(m_font, m_font_size)) - { - m_error_message = "Failed to load font: " + std::string(m_font); - return; - } + m_error.set("Failed to load font: " + m_font); + return; } - m_error_message.clear(); + m_error.clear(); m_settings_changed = false; } void settings_window::render_general_tab() { - ImGui::Spacing(); + using namespace clrsync::gui::widgets; - auto accent_color = clrsync::gui::widgets::palette_color(m_current_palette, "accent"); - ImGui::TextColored(accent_color, "Theme Settings"); - ImGui::Separator(); - ImGui::Spacing(); + section_header("Theme Settings", m_current_palette); - ImGui::Text("Default Theme:"); - ImGui::SameLine(); - show_help_marker("The default color scheme to load on startup"); - ImGui::SetNextItemWidth(-100.0f); - if (ImGui::InputText("##default_theme", m_default_theme, sizeof(m_default_theme))) + form_field_config theme_cfg{ + .label = "Default Theme", + .tooltip = "The default color scheme to load on startup", + .field_width = -100.0f + }; + if (m_form.render_text(theme_cfg, m_default_theme)) m_settings_changed = true; - ImGui::Spacing(); + section_header("Path Settings", m_current_palette); - ImGui::TextColored(accent_color, "Path Settings"); - ImGui::Separator(); - ImGui::Spacing(); - - ImGui::Text("Palettes Directory:"); - ImGui::SameLine(); - show_help_marker("Directory where color palettes are stored\nSupports ~ for home directory"); - ImGui::SetNextItemWidth(-120.0f); - if (ImGui::InputText("##palettes_path", m_palettes_path, sizeof(m_palettes_path))) + form_field_config path_cfg{ + .label = "Palettes Directory", + .tooltip = "Directory where color palettes are stored\nSupports ~ for home directory", + .field_width = -1.0f, + .type = field_type::path + }; + if (m_form.render_path(path_cfg, m_palettes_path)) m_settings_changed = true; - ImGui::SameLine(); - if (ImGui::Button("Browse")) - { - std::string selected_path = - m_ui_manager->select_folder_dialog("Select Palettes Directory", m_palettes_path); - if (!selected_path.empty()) - { - strncpy(m_palettes_path, selected_path.c_str(), sizeof(m_palettes_path) - 1); - m_palettes_path[sizeof(m_palettes_path) - 1] = '\0'; - m_settings_changed = true; - } - } } void settings_window::render_appearance_tab() { - ImGui::Spacing(); + using namespace clrsync::gui::widgets; - auto accent_color = clrsync::gui::widgets::palette_color(m_current_palette, "accent"); - ImGui::TextColored(accent_color, "Font Settings"); - ImGui::Separator(); - ImGui::Spacing(); + section_header("Font Settings", m_current_palette); - ImGui::Text("Font Family:"); - ImGui::SameLine(); - show_help_marker("Select font family for the application interface"); - ImGui::SetNextItemWidth(-1.0f); - if (ImGui::BeginCombo("##font", m_font)) - { - for (int i = 0; i < static_cast(m_available_fonts.size()); i++) - { - bool is_selected = (i == m_selected_font_idx); - if (ImGui::Selectable(m_available_fonts[i].c_str(), is_selected)) - { - m_selected_font_idx = i; - strncpy(m_font, m_available_fonts[i].c_str(), sizeof(m_font) - 1); - m_font[sizeof(m_font) - 1] = '\0'; - m_settings_changed = true; - } - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - - ImGui::Spacing(); - - ImGui::Text("Font Size:"); - ImGui::SameLine(); - show_help_marker("Font size for the application interface (8-48)"); - ImGui::SetNextItemWidth(120.0f); - int old_size = m_font_size; - if (ImGui::SliderInt("##font_size", &m_font_size, 8, 48, "%d px")) - { - if (old_size != m_font_size) - m_settings_changed = true; - } - - ImGui::SameLine(); - if (ImGui::Button("Reset")) - { - m_font_size = 14; + form_field_config font_cfg{ + .label = "Font Family", + .tooltip = "Select font family for the application interface", + .field_width = -1.0f, + .type = field_type::combo + }; + if (m_form.render_combo(font_cfg, m_available_fonts, m_selected_font_idx, m_font)) m_settings_changed = true; - } -} -void settings_window::render_status_messages() -{ - if (!m_error_message.empty()) - { - ImGui::Spacing(); - - auto error_bg_color = clrsync::gui::widgets::palette_color(m_current_palette, "error"); - auto error_text_color = clrsync::gui::widgets::palette_color(m_current_palette, "on_error"); - - ImGui::PushStyleColor(ImGuiCol_ChildBg, error_bg_color); - ImGui::PushStyleColor(ImGuiCol_Border, error_bg_color); - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); - ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f); - - if (ImGui::BeginChild("##error_box", ImVec2(0, 0), - ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Borders)) - { - ImGui::PushStyleColor(ImGuiCol_Text, error_text_color); - ImGui::TextWrapped("Error: %s", m_error_message.c_str()); - ImGui::PopStyleColor(); - - ImGui::Spacing(); - - ImGui::PushStyleColor(ImGuiCol_Button, - ImVec4(error_bg_color.x * 0.8f, error_bg_color.y * 0.8f, - error_bg_color.z * 0.8f, error_bg_color.w)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, - ImVec4(error_bg_color.x * 0.6f, error_bg_color.y * 0.6f, - error_bg_color.z * 0.6f, error_bg_color.w)); - ImGui::PushStyleColor(ImGuiCol_Text, error_text_color); - - if (ImGui::Button("Dismiss##error")) - m_error_message.clear(); - - ImGui::PopStyleColor(3); - } - ImGui::EndChild(); - - ImGui::PopStyleVar(2); - ImGui::PopStyleColor(2); - } -} - -void settings_window::render_action_buttons() -{ ImGui::Spacing(); - float button_width = 100.0f; - float spacing = ImGui::GetStyle().ItemSpacing.x; - float window_width = ImGui::GetContentRegionAvail().x; - - float total_buttons_width = 4 * button_width + 3 * spacing; - float start_pos = (window_width - total_buttons_width) * 0.5f; - - if (start_pos > 0) - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_pos); - - if (ImGui::Button("OK", ImVec2(button_width, 0))) - { - apply_settings(); - if (m_error_message.empty()) - { - m_visible = false; - m_settings_changed = false; - } - } - - ImGui::SameLine(); - - bool apply_disabled = !m_settings_changed; - if (apply_disabled) - { - ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); - } - - if (ImGui::Button("Apply", ImVec2(button_width, 0)) && !apply_disabled) - { - apply_settings(); - if (m_error_message.empty()) - { - m_settings_changed = false; - } - } - - if (apply_disabled) - { - ImGui::PopStyleVar(); - } - - ImGui::SameLine(); - - if (ImGui::Button("Reset", ImVec2(button_width, 0))) - { - reset_to_defaults(); - } - - ImGui::SameLine(); - - if (ImGui::Button("Cancel", ImVec2(button_width, 0))) - { - load_settings(); - m_visible = false; - m_error_message.clear(); - m_settings_changed = false; - } -} - -void settings_window::show_help_marker(const char *desc) -{ - ImGui::TextDisabled("(?)"); - if (ImGui::BeginItemTooltip()) - { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::TextUnformatted(desc); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); - } + form_field_config size_cfg{ + .label = "Font Size", + .tooltip = "Font size for the application interface (8-48)", + .field_width = 120.0f, + .type = field_type::slider, + .min_value = 8.0f, + .max_value = 48.0f, + .format = "%d px", + .show_reset = true, + .default_value = 14 + }; + if (m_form.render_slider(size_cfg, m_font_size)) + m_settings_changed = true; } void settings_window::reset_to_defaults() { - strncpy(m_default_theme, "dark", sizeof(m_default_theme)); - strncpy(m_palettes_path, "~/.config/clrsync/palettes", sizeof(m_palettes_path)); - strncpy(m_font, "JetBrains Mono Nerd Font", sizeof(m_font)); + m_default_theme = "dark"; + m_palettes_path = "~/.config/clrsync/palettes"; + m_font = "JetBrains Mono Nerd Font"; m_font_size = 14; - m_error_message.clear(); + m_error.clear(); m_settings_changed = true; } diff --git a/src/gui/views/settings_window.hpp b/src/gui/views/settings_window.hpp index 194638d..3f50f3b 100644 --- a/src/gui/views/settings_window.hpp +++ b/src/gui/views/settings_window.hpp @@ -2,6 +2,9 @@ #define CLRSYNC_GUI_SETTINGS_WINDOW_HPP #include "core/palette/palette.hpp" +#include "gui/widgets/error_message.hpp" +#include "gui/widgets/form_field.hpp" +#include "gui/widgets/settings_buttons.hpp" #include #include @@ -15,52 +18,37 @@ class settings_window public: settings_window(clrsync::gui::ui_manager* ui_mgr); void render(); - void show() - { - m_visible = true; - } - void hide() - { - m_visible = false; - } - bool is_visible() const - { - return m_visible; - } + void show() { m_visible = true; } + void hide() { m_visible = false; } + bool is_visible() const { return m_visible; } + + void set_palette(const clrsync::core::palette& palette) { m_current_palette = palette; } private: void load_settings(); - void save_settings(); void apply_settings(); void render_general_tab(); void render_appearance_tab(); - void render_status_messages(); - void render_action_buttons(); - void show_help_marker(const char *desc); void reset_to_defaults(); - - public: - void set_palette(const clrsync::core::palette &palette) - { - m_current_palette = palette; - } + void setup_widgets(); bool m_visible{false}; + bool m_settings_changed{false}; - char m_default_theme[128]; - char m_palettes_path[512]; - char m_font[128]; - int m_font_size; + std::string m_default_theme; + std::string m_palettes_path; + std::string m_font; + int m_font_size{14}; + int m_selected_font_idx{0}; std::vector m_available_fonts; - int m_selected_font_idx; - - std::string m_error_message; - bool m_settings_changed; - int m_current_tab; clrsync::core::palette m_current_palette; clrsync::gui::ui_manager* m_ui_manager; + + clrsync::gui::widgets::form_field m_form; + clrsync::gui::widgets::error_message m_error; + clrsync::gui::widgets::settings_buttons m_buttons; }; #endif // CLRSYNC_GUI_SETTINGS_WINDOW_HPP \ No newline at end of file diff --git a/src/gui/views/template_editor.cpp b/src/gui/views/template_editor.cpp index efa1d41..41a6f26 100644 --- a/src/gui/views/template_editor.cpp +++ b/src/gui/views/template_editor.cpp @@ -20,8 +20,10 @@ const std::vector COLOR_FORMATS = { } template_editor::template_editor(clrsync::gui::ui_manager* ui_mgr) - : m_template_name("new_template"), m_ui_manager(ui_mgr) + : m_ui_manager(ui_mgr) { + m_control_state.name = "new_template"; + m_autocomplete_bg_color = ImVec4(0.12f, 0.12f, 0.15f, 0.98f); m_autocomplete_border_color = ImVec4(0.4f, 0.4f, 0.45f, 1.0f); m_autocomplete_selected_color = ImVec4(0.25f, 0.45f, 0.75f, 0.9f); @@ -49,6 +51,33 @@ template_editor::template_editor(clrsync::gui::ui_manager* ui_mgr) m_editor.SetText("# Enter your template here\n# Use {color_key} for color variables\n# " "Examples: {color.hex}, {color.rgb}, {color.r}\n\n"); m_editor.SetShowWhitespaces(false); + + setup_callbacks(); +} + +void template_editor::setup_callbacks() +{ + m_callbacks.on_new = [this]() { new_template(); }; + m_callbacks.on_save = [this]() { save_template(); }; + m_callbacks.on_delete = [this]() { delete_template(); }; + m_callbacks.on_enabled_changed = [this](bool enabled) { + m_template_controller.set_template_enabled(m_control_state.name, enabled); + }; + m_callbacks.on_browse_input = [this](const std::string& path) -> std::string { + return m_ui_manager->open_file_dialog("Select Template File", path); + }; + m_callbacks.on_browse_output = [this](const std::string& path) -> std::string { + return m_ui_manager->save_file_dialog("Select Output File", path); + }; + m_callbacks.on_input_path_changed = [this](const std::string& path) { + m_template_controller.set_template_input_path(m_control_state.name, path); + }; + m_callbacks.on_output_path_changed = [this](const std::string& path) { + m_template_controller.set_template_output_path(m_control_state.name, path); + }; + m_callbacks.on_reload_command_changed = [this](const std::string& cmd) { + m_template_controller.set_template_reload_command(m_control_state.name, cmd); + }; } void template_editor::apply_current_palette(const clrsync::core::palette &pal) @@ -361,8 +390,8 @@ void template_editor::render() } clrsync::gui::widgets::delete_confirmation_dialog( - "Delete Template?", m_template_name, "template", m_current_palette, [this]() { - bool success = m_template_controller.remove_template(m_template_name); + "Delete Template?", m_control_state.name, "template", m_current_palette, [this]() { + bool success = m_template_controller.remove_template(m_control_state.name); if (success) { new_template(); @@ -370,7 +399,7 @@ void template_editor::render() } else { - m_validation_error = "Failed to delete template"; + m_validation.set("Failed to delete template"); } }); @@ -379,216 +408,20 @@ void template_editor::render() void template_editor::render_controls() { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 8)); - - if (ImGui::Button(" + New ")) - { - new_template(); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Create a new template"); - if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyPressed(ImGuiKey_S)) { save_template(); } - ImGui::SameLine(); - if (ImGui::Button(" Save ")) - { - save_template(); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Save template (Ctrl+S)"); - - if (m_is_editing_existing) - { - ImGui::SameLine(); - auto error = clrsync::gui::widgets::palette_color(m_current_palette, "error"); - auto error_hover = ImVec4(error.x * 1.1f, error.y * 1.1f, error.z * 1.1f, error.w); - auto error_active = ImVec4(error.x * 0.8f, error.y * 0.8f, error.z * 0.8f, error.w); - auto on_error = clrsync::gui::widgets::palette_color(m_current_palette, "on_error"); - ImGui::PushStyleColor(ImGuiCol_Button, error); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, error_hover); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, error_active); - ImGui::PushStyleColor(ImGuiCol_Text, on_error); - if (ImGui::Button(" Delete ")) - { - delete_template(); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Delete this template"); - ImGui::PopStyleColor(4); - } - - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10); - - bool enabled_changed = false; - if (m_enabled) - { - ImVec4 success_color = clrsync::gui::widgets::palette_color(m_current_palette, "success", "accent"); - ImVec4 success_on_color = - clrsync::gui::widgets::palette_color(m_current_palette, "on_success", "on_surface"); - ImVec4 success_hover = - ImVec4(success_color.x * 1.2f, success_color.y * 1.2f, success_color.z * 1.2f, 0.6f); - ImGui::PushStyleColor(ImGuiCol_FrameBg, - ImVec4(success_color.x, success_color.y, success_color.z, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, success_hover); - ImGui::PushStyleColor(ImGuiCol_CheckMark, success_on_color); - } - else - { - ImVec4 error_color = clrsync::gui::widgets::palette_color(m_current_palette, "error", "accent"); - ImVec4 error_on_color = - clrsync::gui::widgets::palette_color(m_current_palette, "on_error", "on_surface"); - ImVec4 error_hover = - ImVec4(error_color.x * 1.2f, error_color.y * 1.2f, error_color.z * 1.2f, 0.6f); - ImGui::PushStyleColor(ImGuiCol_FrameBg, - ImVec4(error_color.x, error_color.y, error_color.z, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, error_hover); - ImGui::PushStyleColor(ImGuiCol_CheckMark, error_on_color); - } - - enabled_changed = ImGui::Checkbox("Enabled", &m_enabled); - ImGui::PopStyleColor(3); - - if (enabled_changed && m_is_editing_existing) - { - m_template_controller.set_template_enabled(m_template_name, m_enabled); - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Enable/disable this template for theme application"); - - ImGui::PopStyleVar(); - - ImGui::Spacing(); - - ImGui::AlignTextToFramePadding(); - ImGui::Text("Name:"); - ImGui::SameLine(80); - ImGui::SetNextItemWidth(180.0f); - char name_buf[256] = {0}; - snprintf(name_buf, sizeof(name_buf), "%s", m_template_name.c_str()); - if (ImGui::InputText("##template_name", name_buf, sizeof(name_buf))) - { - m_template_name = name_buf; - if (!m_template_name.empty()) - { - m_validation_error = ""; - } - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Unique name for this template"); - - ImGui::AlignTextToFramePadding(); - ImGui::Text("Input:"); - ImGui::SameLine(80); - ImGui::SetNextItemWidth(-120.0f); - char input_path_buf[512] = {0}; - snprintf(input_path_buf, sizeof(input_path_buf), "%s", m_input_path.c_str()); - if (ImGui::InputTextWithHint("##input_path", "Path to template file...", input_path_buf, - sizeof(input_path_buf))) - { - m_input_path = input_path_buf; - if (!m_input_path.empty()) - { - m_validation_error = ""; - } - if (m_is_editing_existing) - { - m_template_controller.set_template_input_path(m_template_name, m_input_path); - } - } - ImGui::SameLine(); - if (ImGui::Button("Browse##input")) - { - std::string selected_path = - m_ui_manager->open_file_dialog("Select Template File", m_input_path); - if (!selected_path.empty()) - { - m_input_path = selected_path; - if (m_is_editing_existing) - { - m_template_controller.set_template_input_path(m_template_name, m_input_path); - } - m_validation_error = ""; - } - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Path where the template source file is stored"); - - ImGui::AlignTextToFramePadding(); - ImGui::Text("Output:"); - ImGui::SameLine(80); - ImGui::SetNextItemWidth(-120.0f); - char path_buf[512] = {0}; - snprintf(path_buf, sizeof(path_buf), "%s", m_output_path.c_str()); - if (ImGui::InputTextWithHint("##output_path", "Path for generated config...", path_buf, - sizeof(path_buf))) - { - m_output_path = path_buf; - if (!m_output_path.empty()) - { - m_validation_error = ""; - } - if (m_is_editing_existing) - { - m_template_controller.set_template_output_path(m_template_name, m_output_path); - } - } - ImGui::SameLine(); - if (ImGui::Button("Browse##output")) - { - std::string selected_path = - m_ui_manager->save_file_dialog("Select Output File", m_output_path); - if (!selected_path.empty()) - { - m_output_path = selected_path; - if (m_is_editing_existing) - { - m_template_controller.set_template_output_path(m_template_name, m_output_path); - } - m_validation_error = ""; - } - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Path where the processed config will be written"); - - ImGui::AlignTextToFramePadding(); - ImGui::Text("Reload:"); - ImGui::SameLine(80); - ImGui::SetNextItemWidth(-FLT_MIN); - char reload_buf[512] = {0}; - snprintf(reload_buf, sizeof(reload_buf), "%s", m_reload_command.c_str()); - if (ImGui::InputTextWithHint("##reload_cmd", "Command to reload app (optional)...", reload_buf, - sizeof(reload_buf))) - { - m_reload_command = reload_buf; - if (m_is_editing_existing) - { - m_template_controller.set_template_reload_command(m_template_name, m_reload_command); - } - } - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("Shell command to run after applying theme (e.g., 'pkill -USR1 kitty')"); - - if (!m_validation_error.empty()) - { - ImGui::Spacing(); - ImVec4 error_color = clrsync::gui::widgets::palette_color(m_current_palette, "error", "accent"); - ImGui::PushStyleColor(ImGuiCol_Text, error_color); - ImGui::TextWrapped("%s", m_validation_error.c_str()); - ImGui::PopStyleColor(); - } + m_controls.render(m_control_state, m_callbacks, m_current_palette, m_validation); } void template_editor::render_editor() { ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 4)); - if (!m_is_editing_existing) + if (!m_control_state.is_editing_existing) { ImVec4 success_color = clrsync::gui::widgets::palette_color(m_current_palette, "success", "accent"); ImGui::PushStyleColor(ImGuiCol_Text, success_color); @@ -597,7 +430,7 @@ void template_editor::render_editor() } else { - ImGui::Text(" %s", m_template_name.c_str()); + ImGui::Text(" %s", m_control_state.name.c_str()); auto trim_right = [](const std::string &s) -> std::string { size_t end = s.find_last_not_of("\r\n"); return (end == std::string::npos) ? "" : s.substr(0, end + 1); @@ -697,7 +530,7 @@ void template_editor::render_template_list() ImGui::TextDisabled("(%d)", (int)m_template_controller.templates().size()); ImGui::Separator(); - if (!m_is_editing_existing) + if (!m_control_state.is_editing_existing) { ImVec4 success_color = clrsync::gui::widgets::palette_color(m_current_palette, "success", "accent"); ImVec4 success_bg = ImVec4(success_color.x, success_color.y, success_color.z, 0.5f); @@ -712,7 +545,7 @@ void template_editor::render_template_list() for (const auto &[key, tmpl] : templates) { - const bool selected = (m_template_name == key && m_is_editing_existing); + const bool selected = (m_control_state.name == key && m_control_state.is_editing_existing); if (!tmpl.enabled()) { @@ -797,44 +630,43 @@ bool template_editor::is_valid_path(const std::string &path) void template_editor::save_template() { - std::string trimmed_name = m_template_name; + std::string trimmed_name = m_control_state.name; trimmed_name.erase(0, trimmed_name.find_first_not_of(" \t\n\r")); trimmed_name.erase(trimmed_name.find_last_not_of(" \t\n\r") + 1); - std::string trimmed_input_path = m_input_path; + std::string trimmed_input_path = m_control_state.input_path; trimmed_input_path.erase(0, trimmed_input_path.find_first_not_of(" \t\n\r")); trimmed_input_path.erase(trimmed_input_path.find_last_not_of(" \t\n\r") + 1); - std::string trimmed_path = m_output_path; + std::string trimmed_path = m_control_state.output_path; trimmed_path.erase(0, trimmed_path.find_first_not_of(" \t\n\r")); trimmed_path.erase(trimmed_path.find_last_not_of(" \t\n\r") + 1); if (trimmed_name.empty()) { - m_validation_error = "Error: Template name cannot be empty!"; + m_validation.set("Error: Template name cannot be empty!"); return; } if (trimmed_input_path.empty()) { - m_validation_error = "Error: Input path cannot be empty!"; + m_validation.set("Error: Input path cannot be empty!"); return; } if (trimmed_path.empty()) { - m_validation_error = "Error: Output path cannot be empty!"; + m_validation.set("Error: Output path cannot be empty!"); return; } if (!is_valid_path(trimmed_path)) { - m_validation_error = - "Error: Output path is invalid! Must be a valid file path with directory."; + m_validation.set("Error: Output path is invalid! Must be a valid file path with directory."); return; } - m_validation_error = ""; + m_validation.clear(); auto &cfg = clrsync::core::config::instance(); @@ -849,7 +681,7 @@ void template_editor::save_template() } catch (const std::exception &e) { - m_validation_error = "Error: Could not create directory for input path"; + m_validation.set("Error: Could not create directory for input path"); return; } } @@ -859,7 +691,7 @@ void template_editor::save_template() std::ofstream out(template_file); if (!out.is_open()) { - m_validation_error = "Failed to write template file"; + m_validation.set("Failed to write template file"); return; } @@ -867,20 +699,20 @@ void template_editor::save_template() out.close(); clrsync::core::theme_template tmpl(trimmed_name, template_file.string(), trimmed_path); - tmpl.set_reload_command(m_reload_command); - tmpl.set_enabled(m_enabled); + tmpl.set_reload_command(m_control_state.reload_command); + tmpl.set_enabled(m_control_state.enabled); auto result = cfg.update_template(trimmed_name, tmpl); if (!result) { - m_validation_error = "Error saving template: " + result.error().description(); + m_validation.set("Error saving template: " + result.error().description()); return; } - m_template_name = trimmed_name; - m_input_path = trimmed_input_path; - m_output_path = trimmed_path; - m_is_editing_existing = true; + m_control_state.name = trimmed_name; + m_control_state.input_path = trimmed_input_path; + m_control_state.output_path = trimmed_path; + m_control_state.is_editing_existing = true; m_saved_content = m_editor.GetText(); m_has_unsaved_changes = false; @@ -895,13 +727,13 @@ void template_editor::load_template(const std::string &name) if (it != templates.end()) { const auto &tmpl = it->second; - m_template_name = name; - m_input_path = tmpl.template_path(); - m_output_path = tmpl.output_path(); - m_reload_command = tmpl.reload_command(); - m_enabled = tmpl.enabled(); - m_is_editing_existing = true; - m_validation_error = ""; + m_control_state.name = name; + m_control_state.input_path = tmpl.template_path(); + m_control_state.output_path = tmpl.output_path(); + m_control_state.reload_command = tmpl.reload_command(); + m_control_state.enabled = tmpl.enabled(); + m_control_state.is_editing_existing = true; + m_validation.clear(); std::ifstream in(tmpl.template_path()); if (in.is_open()) @@ -920,31 +752,31 @@ void template_editor::load_template(const std::string &name) } else { - m_validation_error = "Error loading template: Failed to open file"; + m_validation.set("Error loading template: Failed to open file"); } } } void template_editor::new_template() { - m_template_name = "new_template"; + m_control_state.name = "new_template"; std::string default_content = "# Enter your template here\n# Use {color_key} for color variables\n# " "Examples: {color.hex}, {color.rgb}, {color.r}\n\n"; m_editor.SetText(default_content); m_saved_content = default_content; - m_input_path = ""; - m_output_path = ""; - m_reload_command = ""; - m_enabled = true; - m_is_editing_existing = false; - m_validation_error = ""; + m_control_state.input_path = ""; + m_control_state.output_path = ""; + m_control_state.reload_command = ""; + m_control_state.enabled = true; + m_control_state.is_editing_existing = false; + m_validation.clear(); m_has_unsaved_changes = false; } void template_editor::delete_template() { - if (!m_is_editing_existing || m_template_name.empty()) + if (!m_control_state.is_editing_existing || m_control_state.name.empty()) return; m_show_delete_confirmation = true; diff --git a/src/gui/views/template_editor.hpp b/src/gui/views/template_editor.hpp index 761c860..299f7c3 100644 --- a/src/gui/views/template_editor.hpp +++ b/src/gui/views/template_editor.hpp @@ -4,6 +4,8 @@ #include "color_text_edit/TextEditor.h" #include "core/palette/palette.hpp" #include "gui/controllers/template_controller.hpp" +#include "gui/widgets/template_controls.hpp" +#include "gui/widgets/validation_message.hpp" #include "imgui.h" #include #include @@ -32,22 +34,20 @@ class template_editor void new_template(); void delete_template(); void refresh_templates(); + void setup_callbacks(); bool is_valid_path(const std::string &path); template_controller m_template_controller; TextEditor m_editor; - std::string m_template_name; - std::string m_input_path; - std::string m_output_path; - std::string m_reload_command; - std::string m_validation_error; - std::string m_saved_content; - bool m_has_unsaved_changes = false; + clrsync::gui::widgets::template_control_state m_control_state; + clrsync::gui::widgets::template_controls m_controls; + clrsync::gui::widgets::template_control_callbacks m_callbacks; + clrsync::gui::widgets::validation_message m_validation; - bool m_enabled{true}; - bool m_is_editing_existing{false}; + std::string m_saved_content; + bool m_has_unsaved_changes{false}; bool m_show_delete_confirmation{false}; bool m_show_autocomplete{false}; diff --git a/src/gui/widgets/centered_text.cpp b/src/gui/widgets/centered_text.cpp new file mode 100644 index 0000000..b622e69 --- /dev/null +++ b/src/gui/widgets/centered_text.cpp @@ -0,0 +1,23 @@ +#include "centered_text.hpp" + +namespace clrsync::gui::widgets +{ + +void centered_text(const std::string& text, const ImVec4& color) +{ + float window_width = ImGui::GetContentRegionAvail().x; + float text_width = ImGui::CalcTextSize(text.c_str()).x; + ImGui::SetCursorPosX((window_width - text_width) * 0.5f); + ImGui::TextColored(color, "%s", text.c_str()); +} + +void centered_buttons(float total_width, const std::function& render_buttons) +{ + float window_width = ImGui::GetContentRegionAvail().x; + float start_pos = (window_width - total_width) * 0.5f; + if (start_pos > 0) + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_pos); + render_buttons(); +} + +} // namespace clrsync::gui::widgets diff --git a/src/gui/widgets/centered_text.hpp b/src/gui/widgets/centered_text.hpp new file mode 100644 index 0000000..d37616e --- /dev/null +++ b/src/gui/widgets/centered_text.hpp @@ -0,0 +1,16 @@ +#ifndef CLRSYNC_GUI_WIDGETS_CENTERED_TEXT_HPP +#define CLRSYNC_GUI_WIDGETS_CENTERED_TEXT_HPP + +#include "imgui.h" +#include +#include + +namespace clrsync::gui::widgets +{ + +void centered_text(const std::string& text, const ImVec4& color = ImVec4(1, 1, 1, 1)); +void centered_buttons(float total_width, const std::function& render_buttons); + +} // namespace clrsync::gui::widgets + +#endif // CLRSYNC_GUI_WIDGETS_CENTERED_TEXT_HPP diff --git a/src/gui/widgets/error_message.cpp b/src/gui/widgets/error_message.cpp new file mode 100644 index 0000000..102b214 --- /dev/null +++ b/src/gui/widgets/error_message.cpp @@ -0,0 +1,71 @@ +#include "error_message.hpp" +#include "colors.hpp" +#include "imgui.h" + +namespace clrsync::gui::widgets +{ + +void error_message::set(const std::string& message) +{ + m_message = message; +} + +void error_message::clear() +{ + m_message.clear(); +} + +bool error_message::has_error() const +{ + return !m_message.empty(); +} + +const std::string& error_message::get() const +{ + return m_message; +} + +void error_message::render(const core::palette& palette) +{ + if (m_message.empty()) + return; + + ImGui::Spacing(); + + auto error_bg_color = palette_color(palette, "error"); + auto error_text_color = palette_color(palette, "on_error"); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, error_bg_color); + ImGui::PushStyleColor(ImGuiCol_Border, error_bg_color); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f); + + if (ImGui::BeginChild("##error_box", ImVec2(0, 0), + ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Borders)) + { + ImGui::PushStyleColor(ImGuiCol_Text, error_text_color); + ImGui::TextWrapped("Error: %s", m_message.c_str()); + ImGui::PopStyleColor(); + + ImGui::Spacing(); + + ImGui::PushStyleColor(ImGuiCol_Button, + ImVec4(error_bg_color.x * 0.8f, error_bg_color.y * 0.8f, + error_bg_color.z * 0.8f, error_bg_color.w)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, + ImVec4(error_bg_color.x * 0.6f, error_bg_color.y * 0.6f, + error_bg_color.z * 0.6f, error_bg_color.w)); + ImGui::PushStyleColor(ImGuiCol_Text, error_text_color); + + if (ImGui::Button("Dismiss##error")) + m_message.clear(); + + ImGui::PopStyleColor(3); + } + ImGui::EndChild(); + + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(2); +} + +} // namespace clrsync::gui::widgets diff --git a/src/gui/widgets/error_message.hpp b/src/gui/widgets/error_message.hpp new file mode 100644 index 0000000..b3df35a --- /dev/null +++ b/src/gui/widgets/error_message.hpp @@ -0,0 +1,25 @@ +#ifndef CLRSYNC_GUI_WIDGETS_ERROR_MESSAGE_HPP +#define CLRSYNC_GUI_WIDGETS_ERROR_MESSAGE_HPP + +#include "core/palette/palette.hpp" +#include + +namespace clrsync::gui::widgets +{ + +class error_message +{ + public: + void set(const std::string& message); + void clear(); + bool has_error() const; + const std::string& get() const; + void render(const core::palette& palette); + + private: + std::string m_message; +}; + +} // namespace clrsync::gui::widgets + +#endif // CLRSYNC_GUI_WIDGETS_ERROR_MESSAGE_HPP diff --git a/src/gui/widgets/form_field.cpp b/src/gui/widgets/form_field.cpp index f84811a..850c579 100644 --- a/src/gui/widgets/form_field.cpp +++ b/src/gui/widgets/form_field.cpp @@ -181,4 +181,66 @@ void form_field::set_path_browse_callback(const std::function 0) + ImGui::SetNextItemWidth(config.field_width); + else if (config.field_width < 0) + ImGui::SetNextItemWidth(config.field_width); + + std::string id = "##" + config.label; + std::string format = config.format.empty() ? "%d" : config.format.c_str(); + int old_value = value; + bool changed = ImGui::SliderInt(id.c_str(), &value, (int)config.min_value, (int)config.max_value, format.c_str()); + + if (config.show_reset) + { + ImGui::SameLine(); + std::string reset_id = "Reset##" + config.label; + if (ImGui::Button(reset_id.c_str())) + { + value = config.default_value; + changed = (old_value != value); + } + } + + render_tooltip(config); + return changed; +} + +bool form_field::render_combo(const form_field_config& config, const std::vector& items, int& selected_idx, std::string& value) +{ + render_label(config); + + if (config.field_width > 0) + ImGui::SetNextItemWidth(config.field_width); + else if (config.field_width < 0) + ImGui::SetNextItemWidth(config.field_width); + + std::string id = "##" + config.label; + bool changed = false; + + if (ImGui::BeginCombo(id.c_str(), value.c_str())) + { + for (int i = 0; i < static_cast(items.size()); i++) + { + bool is_selected = (i == selected_idx); + if (ImGui::Selectable(items[i].c_str(), is_selected)) + { + selected_idx = i; + value = items[i]; + changed = true; + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + render_tooltip(config); + return changed; +} + } // namespace clrsync::gui::widgets \ No newline at end of file diff --git a/src/gui/widgets/form_field.hpp b/src/gui/widgets/form_field.hpp index 1018f2b..433c276 100644 --- a/src/gui/widgets/form_field.hpp +++ b/src/gui/widgets/form_field.hpp @@ -3,6 +3,7 @@ #include #include +#include namespace clrsync::gui::widgets { @@ -12,7 +13,9 @@ enum class field_type text, text_with_hint, number, + slider, path, + combo, readonly_text }; @@ -27,6 +30,9 @@ struct form_field_config std::string hint; float min_value = 0.0f; float max_value = 100.0f; + std::string format; + bool show_reset = false; + int default_value = 0; }; class form_field @@ -41,6 +47,12 @@ class form_field bool render_number(const form_field_config& config, int& value); bool render_number(const form_field_config& config, float& value); + // Render a slider field + bool render_slider(const form_field_config& config, int& value); + + // Render a combo box field + bool render_combo(const form_field_config& config, const std::vector& items, int& selected_idx, std::string& value); + // Render a path input field with browse button bool render_path(const form_field_config& config, std::string& value); diff --git a/src/gui/widgets/link_button.cpp b/src/gui/widgets/link_button.cpp new file mode 100644 index 0000000..877ed9b --- /dev/null +++ b/src/gui/widgets/link_button.cpp @@ -0,0 +1,28 @@ +#include "link_button.hpp" +#include "imgui.h" +#include + +namespace clrsync::gui::widgets +{ + +void open_url(const std::string& url) +{ +#ifdef _WIN32 + std::string cmd = "start " + url; +#elif __APPLE__ + std::string cmd = "open " + url; +#else + std::string cmd = "xdg-open " + url; +#endif + std::system(cmd.c_str()); +} + +bool link_button(const std::string& label, const std::string& url, float width) +{ + bool clicked = ImGui::Button(label.c_str(), ImVec2(width, 0)); + if (clicked) + open_url(url); + return clicked; +} + +} // namespace clrsync::gui::widgets diff --git a/src/gui/widgets/link_button.hpp b/src/gui/widgets/link_button.hpp new file mode 100644 index 0000000..928a0be --- /dev/null +++ b/src/gui/widgets/link_button.hpp @@ -0,0 +1,14 @@ +#ifndef CLRSYNC_GUI_WIDGETS_LINK_BUTTON_HPP +#define CLRSYNC_GUI_WIDGETS_LINK_BUTTON_HPP + +#include + +namespace clrsync::gui::widgets +{ + +void open_url(const std::string& url); +bool link_button(const std::string& label, const std::string& url, float width = 0.0f); + +} // namespace clrsync::gui::widgets + +#endif // CLRSYNC_GUI_WIDGETS_LINK_BUTTON_HPP diff --git a/src/gui/widgets/section_header.cpp b/src/gui/widgets/section_header.cpp new file mode 100644 index 0000000..8e6c32d --- /dev/null +++ b/src/gui/widgets/section_header.cpp @@ -0,0 +1,17 @@ +#include "section_header.hpp" +#include "colors.hpp" +#include "imgui.h" + +namespace clrsync::gui::widgets +{ + +void section_header(const std::string& title, const core::palette& palette) +{ + ImGui::Spacing(); + auto accent_color = palette_color(palette, "accent"); + ImGui::TextColored(accent_color, "%s", title.c_str()); + ImGui::Separator(); + ImGui::Spacing(); +} + +} // namespace clrsync::gui::widgets diff --git a/src/gui/widgets/section_header.hpp b/src/gui/widgets/section_header.hpp new file mode 100644 index 0000000..c7dc465 --- /dev/null +++ b/src/gui/widgets/section_header.hpp @@ -0,0 +1,14 @@ +#ifndef CLRSYNC_GUI_WIDGETS_SECTION_HEADER_HPP +#define CLRSYNC_GUI_WIDGETS_SECTION_HEADER_HPP + +#include "core/palette/palette.hpp" +#include + +namespace clrsync::gui::widgets +{ + +void section_header(const std::string& title, const core::palette& palette); + +} // namespace clrsync::gui::widgets + +#endif // CLRSYNC_GUI_WIDGETS_SECTION_HEADER_HPP diff --git a/src/gui/widgets/settings_buttons.cpp b/src/gui/widgets/settings_buttons.cpp new file mode 100644 index 0000000..8198575 --- /dev/null +++ b/src/gui/widgets/settings_buttons.cpp @@ -0,0 +1,56 @@ +#include "settings_buttons.hpp" +#include "imgui.h" + +namespace clrsync::gui::widgets +{ + +void settings_buttons::render(const settings_buttons_callbacks& callbacks, bool apply_enabled) +{ + ImGui::Spacing(); + + float spacing = ImGui::GetStyle().ItemSpacing.x; + float window_width = ImGui::GetContentRegionAvail().x; + float total_buttons_width = 4 * m_button_width + 3 * spacing; + float start_pos = (window_width - total_buttons_width) * 0.5f; + + if (start_pos > 0) + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + start_pos); + + if (ImGui::Button("OK", ImVec2(m_button_width, 0))) + { + if (callbacks.on_ok) + callbacks.on_ok(); + } + + ImGui::SameLine(); + + if (!apply_enabled) + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.5f); + + if (ImGui::Button("Apply", ImVec2(m_button_width, 0)) && apply_enabled) + { + if (callbacks.on_apply) + callbacks.on_apply(); + } + + if (!apply_enabled) + ImGui::PopStyleVar(); + + ImGui::SameLine(); + + if (ImGui::Button("Reset", ImVec2(m_button_width, 0))) + { + if (callbacks.on_reset) + callbacks.on_reset(); + } + + ImGui::SameLine(); + + if (ImGui::Button("Cancel", ImVec2(m_button_width, 0))) + { + if (callbacks.on_cancel) + callbacks.on_cancel(); + } +} + +} // namespace clrsync::gui::widgets diff --git a/src/gui/widgets/settings_buttons.hpp b/src/gui/widgets/settings_buttons.hpp new file mode 100644 index 0000000..ea83cdc --- /dev/null +++ b/src/gui/widgets/settings_buttons.hpp @@ -0,0 +1,30 @@ +#ifndef CLRSYNC_GUI_WIDGETS_SETTINGS_BUTTONS_HPP +#define CLRSYNC_GUI_WIDGETS_SETTINGS_BUTTONS_HPP + +#include + +namespace clrsync::gui::widgets +{ + +struct settings_buttons_callbacks +{ + std::function on_ok; + std::function on_apply; + std::function on_reset; + std::function on_cancel; +}; + +class settings_buttons +{ + public: + void render(const settings_buttons_callbacks& callbacks, bool apply_enabled); + + void set_button_width(float width) { m_button_width = width; } + + private: + float m_button_width = 100.0f; +}; + +} // namespace clrsync::gui::widgets + +#endif // CLRSYNC_GUI_WIDGETS_SETTINGS_BUTTONS_HPP diff --git a/src/gui/widgets/template_controls.cpp b/src/gui/widgets/template_controls.cpp new file mode 100644 index 0000000..9af6139 --- /dev/null +++ b/src/gui/widgets/template_controls.cpp @@ -0,0 +1,181 @@ +#include "template_controls.hpp" +#include "colors.hpp" +#include "styled_checkbox.hpp" +#include "imgui.h" + +namespace clrsync::gui::widgets +{ + +template_controls::template_controls() = default; + +void template_controls::render(template_control_state& state, + const template_control_callbacks& callbacks, + const core::palette& palette, + validation_message& validation) +{ + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 8)); + + render_action_buttons(state, callbacks, palette); + + ImGui::PopStyleVar(); + + ImGui::Spacing(); + + render_fields(state, callbacks); + + validation.render(palette); +} + +void template_controls::render_action_buttons(template_control_state& state, + const template_control_callbacks& callbacks, + const core::palette& palette) +{ + if (ImGui::Button(" + New ")) + { + if (callbacks.on_new) + callbacks.on_new(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Create a new template"); + + ImGui::SameLine(); + if (ImGui::Button(" Save ")) + { + if (callbacks.on_save) + callbacks.on_save(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Save template (Ctrl+S)"); + + if (state.is_editing_existing) + { + ImGui::SameLine(); + auto error = palette_color(palette, "error"); + auto error_hover = ImVec4(error.x * 1.1f, error.y * 1.1f, error.z * 1.1f, error.w); + auto error_active = ImVec4(error.x * 0.8f, error.y * 0.8f, error.z * 0.8f, error.w); + auto on_error = palette_color(palette, "on_error"); + ImGui::PushStyleColor(ImGuiCol_Button, error); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, error_hover); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, error_active); + ImGui::PushStyleColor(ImGuiCol_Text, on_error); + if (ImGui::Button(" Delete ")) + { + if (callbacks.on_delete) + callbacks.on_delete(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Delete this template"); + ImGui::PopStyleColor(4); + } + + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10); + + bool old_enabled = state.enabled; + styled_checkbox checkbox; + checkbox.render("Enabled", &state.enabled, palette, + state.enabled ? checkbox_style::success : checkbox_style::error); + + if (old_enabled != state.enabled && state.is_editing_existing) + { + if (callbacks.on_enabled_changed) + callbacks.on_enabled_changed(state.enabled); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Enable/disable this template for theme application"); +} + +void template_controls::render_fields(template_control_state& state, + const template_control_callbacks& callbacks) +{ + ImGui::AlignTextToFramePadding(); + ImGui::Text("Name:"); + ImGui::SameLine(80); + ImGui::SetNextItemWidth(180.0f); + char name_buf[256] = {0}; + snprintf(name_buf, sizeof(name_buf), "%s", state.name.c_str()); + if (ImGui::InputText("##template_name", name_buf, sizeof(name_buf))) + { + state.name = name_buf; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Unique name for this template"); + + ImGui::AlignTextToFramePadding(); + ImGui::Text("Input:"); + ImGui::SameLine(80); + ImGui::SetNextItemWidth(-120.0f); + char input_path_buf[512] = {0}; + snprintf(input_path_buf, sizeof(input_path_buf), "%s", state.input_path.c_str()); + if (ImGui::InputTextWithHint("##input_path", "Path to template file...", input_path_buf, + sizeof(input_path_buf))) + { + state.input_path = input_path_buf; + if (state.is_editing_existing && callbacks.on_input_path_changed) + callbacks.on_input_path_changed(state.input_path); + } + ImGui::SameLine(); + if (ImGui::Button("Browse##input")) + { + if (callbacks.on_browse_input) + { + std::string selected = callbacks.on_browse_input(state.input_path); + if (!selected.empty()) + { + state.input_path = selected; + if (state.is_editing_existing && callbacks.on_input_path_changed) + callbacks.on_input_path_changed(state.input_path); + } + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Path where the template source file is stored"); + + ImGui::AlignTextToFramePadding(); + ImGui::Text("Output:"); + ImGui::SameLine(80); + ImGui::SetNextItemWidth(-120.0f); + char path_buf[512] = {0}; + snprintf(path_buf, sizeof(path_buf), "%s", state.output_path.c_str()); + if (ImGui::InputTextWithHint("##output_path", "Path for generated config...", path_buf, + sizeof(path_buf))) + { + state.output_path = path_buf; + if (state.is_editing_existing && callbacks.on_output_path_changed) + callbacks.on_output_path_changed(state.output_path); + } + ImGui::SameLine(); + if (ImGui::Button("Browse##output")) + { + if (callbacks.on_browse_output) + { + std::string selected = callbacks.on_browse_output(state.output_path); + if (!selected.empty()) + { + state.output_path = selected; + if (state.is_editing_existing && callbacks.on_output_path_changed) + callbacks.on_output_path_changed(state.output_path); + } + } + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Path where the processed config will be written"); + + ImGui::AlignTextToFramePadding(); + ImGui::Text("Reload:"); + ImGui::SameLine(80); + ImGui::SetNextItemWidth(-FLT_MIN); + char reload_buf[512] = {0}; + snprintf(reload_buf, sizeof(reload_buf), "%s", state.reload_command.c_str()); + if (ImGui::InputTextWithHint("##reload_cmd", "Command to reload app (optional)...", reload_buf, + sizeof(reload_buf))) + { + state.reload_command = reload_buf; + if (state.is_editing_existing && callbacks.on_reload_command_changed) + callbacks.on_reload_command_changed(state.reload_command); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Shell command to run after applying theme (e.g., 'pkill -USR1 kitty')"); +} + +} // namespace clrsync::gui::widgets diff --git a/src/gui/widgets/template_controls.hpp b/src/gui/widgets/template_controls.hpp new file mode 100644 index 0000000..7c2fb1a --- /dev/null +++ b/src/gui/widgets/template_controls.hpp @@ -0,0 +1,59 @@ +#ifndef CLRSYNC_GUI_WIDGETS_TEMPLATE_CONTROLS_HPP +#define CLRSYNC_GUI_WIDGETS_TEMPLATE_CONTROLS_HPP + +#include "core/palette/palette.hpp" +#include "gui/widgets/form_field.hpp" +#include "gui/widgets/validation_message.hpp" +#include +#include + +namespace clrsync::gui::widgets +{ + +struct template_control_state +{ + std::string name; + std::string input_path; + std::string output_path; + std::string reload_command; + bool enabled{true}; + bool is_editing_existing{false}; +}; + +struct template_control_callbacks +{ + std::function on_new; + std::function on_save; + std::function on_delete; + std::function on_enabled_changed; + std::function on_browse_input; + std::function on_browse_output; + std::function on_input_path_changed; + std::function on_output_path_changed; + std::function on_reload_command_changed; +}; + +class template_controls +{ + public: + template_controls(); + + void render(template_control_state& state, + const template_control_callbacks& callbacks, + const core::palette& palette, + validation_message& validation); + + private: + void render_action_buttons(template_control_state& state, + const template_control_callbacks& callbacks, + const core::palette& palette); + + void render_fields(template_control_state& state, + const template_control_callbacks& callbacks); + + form_field m_form; +}; + +} // namespace clrsync::gui::widgets + +#endif // CLRSYNC_GUI_WIDGETS_TEMPLATE_CONTROLS_HPP diff --git a/src/gui/widgets/validation_message.cpp b/src/gui/widgets/validation_message.cpp new file mode 100644 index 0000000..625ec48 --- /dev/null +++ b/src/gui/widgets/validation_message.cpp @@ -0,0 +1,40 @@ +#include "validation_message.hpp" +#include "colors.hpp" +#include "imgui.h" + +namespace clrsync::gui::widgets +{ + +void validation_message::set(const std::string& message) +{ + m_message = message; +} + +void validation_message::clear() +{ + m_message.clear(); +} + +bool validation_message::has_error() const +{ + return !m_message.empty(); +} + +const std::string& validation_message::get() const +{ + return m_message; +} + +void validation_message::render(const core::palette& palette) +{ + if (m_message.empty()) + return; + + ImGui::Spacing(); + ImVec4 error_color = palette_color(palette, "error", "accent"); + ImGui::PushStyleColor(ImGuiCol_Text, error_color); + ImGui::TextWrapped("%s", m_message.c_str()); + ImGui::PopStyleColor(); +} + +} // namespace clrsync::gui::widgets diff --git a/src/gui/widgets/validation_message.hpp b/src/gui/widgets/validation_message.hpp new file mode 100644 index 0000000..4e9d83f --- /dev/null +++ b/src/gui/widgets/validation_message.hpp @@ -0,0 +1,25 @@ +#ifndef CLRSYNC_GUI_WIDGETS_VALIDATION_MESSAGE_HPP +#define CLRSYNC_GUI_WIDGETS_VALIDATION_MESSAGE_HPP + +#include "core/palette/palette.hpp" +#include + +namespace clrsync::gui::widgets +{ + +class validation_message +{ + public: + void set(const std::string& message); + void clear(); + bool has_error() const; + const std::string& get() const; + void render(const core::palette& palette); + + private: + std::string m_message; +}; + +} // namespace clrsync::gui::widgets + +#endif // CLRSYNC_GUI_WIDGETS_VALIDATION_MESSAGE_HPP