diff --git a/CMakeLists.txt b/CMakeLists.txt index 5dd1af6..60ec520 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,7 @@ set(CLI_SOURCES set(GUI_SOURCES src/gui/GuiUIManager.cpp src/gui/UpdateDialog.cpp + src/gui/UITheme.cpp src/updater/AppUpdater.cpp ) diff --git a/src/crypto/encryption_type.h b/src/crypto/encryption_type.h deleted file mode 100644 index 255e378..0000000 --- a/src/crypto/encryption_type.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef ENCRYPTION_TYPE_H -#define ENCRYPTION_TYPE_H - -enum class EncryptionType { - None, - AES, - LFSR, - RSA -}; - -#endif // ENCRYPTION_TYPE_H diff --git a/src/gui/ComponentBase.h b/src/gui/ComponentBase.h index bbb1e73..185d0b1 100644 --- a/src/gui/ComponentBase.h +++ b/src/gui/ComponentBase.h @@ -4,6 +4,7 @@ #include #include #include +#include "../config/GlobalConfig.h" #include #include #include diff --git a/src/gui/GuiComponents.h b/src/gui/GuiComponents.h index 4c510d3..925357c 100644 --- a/src/gui/GuiComponents.h +++ b/src/gui/GuiComponents.h @@ -2,6 +2,7 @@ #define GUI_COMPONENTS_H #include "GuiComponent.h" +#include "UITheme.h" #include #include #include @@ -22,7 +23,13 @@ #include // for std::remove_if #include "EncryptionUtils.h" -// Base class for text display components +// Initialize the theme system when this header is included +static bool theme_initialized = []() { + UITheme::initialize(); + return true; +}(); + +// Enhanced container component using basic layout class ContainerComponent : public GuiComponent { private: std::vector> children; @@ -75,30 +82,40 @@ class TextComponentBase : public GuiComponent { const std::string& getText() const { return text; } }; -// Component for displaying a title +// Modern component for displaying a title with consistent styling class TitleComponent : public TextComponentBase { private: int fontSize; + std::string variant; public: TitleComponent(Fl_Group* parent, int x, int y, int w, int h, - const std::string& title, int fontSize = 20) - : TextComponentBase(parent, x, y, w, h, title), fontSize(fontSize) {} + const std::string& title, const std::string& variant = "headline2") + : TextComponentBase(parent, x, y, w, h, title), variant(variant) { + + if (variant == "headline1") fontSize = UITheme::Typography::HEADLINE_1; + else if (variant == "headline2") fontSize = UITheme::Typography::HEADLINE_2; + else if (variant == "headline3") fontSize = UITheme::Typography::HEADLINE_3; + else fontSize = UITheme::Typography::HEADLINE_2; + } void create() override { Fl_Box* titleBox = createWidget(x, y, w, h, text.c_str()); - titleBox->labelsize(fontSize); + UITheme::styleText(titleBox, variant); + titleBox->align(FL_ALIGN_CENTER); } }; -// Component for displaying a descriptive text +// Modern component for displaying descriptive text class DescriptionComponent : public TextComponentBase { public: DescriptionComponent(Fl_Group* parent, int x, int y, int w, int h, const std::string& text) : TextComponentBase(parent, x, y, w, h, text) {} void create() override { - createWidget(x, y, w, h, text.c_str()); + Fl_Box* textBox = createWidget(x, y, w, h, text.c_str()); + UITheme::styleText(textBox, "body2"); + textBox->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP); } }; @@ -120,80 +137,186 @@ class FormComponentBase : public GuiComponent { } }; -// Component for login form +// Enhanced login form component with modern styling and validation class LoginFormComponent : public FormComponentBase { private: TextCallback onLogin; Fl_Secret_Input* passwordInput; Fl_Button* loginButton; + Fl_Box* errorLabel; public: LoginFormComponent(Fl_Group* parent, int x, int y, int w, int h, TextCallback onLogin) : FormComponentBase(parent, x, y, w, h), onLogin(onLogin), - passwordInput(nullptr), loginButton(nullptr) {} + passwordInput(nullptr), loginButton(nullptr), errorLabel(nullptr) {} void create() override { + int cardPadding = UITheme::Spacing::EXTRA_LARGE; + int formY = y + UITheme::Typography::HEADLINE_3 + UITheme::Spacing::LARGE; + // Create password input - passwordInput = createWidget(x + 50, y, w - 100, INPUT_HEIGHT, "Master Password:"); + passwordInput = createWidget( + x + cardPadding, formY, + w - 2 * cardPadding, UITheme::Dimensions::INPUT_HEIGHT, + "Master Password:" + ); + UITheme::styleInput(passwordInput, false); // Create login button - loginButton = createWidget(centerX(BUTTON_WIDTH), y + 60, BUTTON_WIDTH, BUTTON_HEIGHT, "Login"); + int buttonY = formY + UITheme::Dimensions::INPUT_HEIGHT + UITheme::Spacing::MEDIUM; + loginButton = createWidget( + x + (w - UITheme::Dimensions::BUTTON_MIN_WIDTH) / 2, buttonY, + UITheme::Dimensions::BUTTON_MIN_WIDTH, UITheme::Dimensions::BUTTON_HEIGHT, + "Login" + ); + UITheme::styleButton(loginButton, "primary"); + CallbackHelper::setCallback(loginButton, this, [this](LoginFormComponent* comp) { + if (strlen(comp->passwordInput->value()) == 0) { + comp->showError("Password is required"); + return; + } + comp->onLogin(comp->passwordInput->value()); }); + + // Create error label (initially hidden) + errorLabel = createWidget( + x + cardPadding, buttonY + UITheme::Dimensions::BUTTON_HEIGHT + UITheme::Spacing::SMALL, + w - 2 * cardPadding, UITheme::Typography::CAPTION + UITheme::Spacing::SMALL, + "" + ); + UITheme::styleText(errorLabel, "caption"); + errorLabel->labelcolor(UITheme::Colors::ERROR); + errorLabel->hide(); + } + + void showError(const std::string& error) { + if (errorLabel) { + errorLabel->copy_label(error.c_str()); + errorLabel->show(); + errorLabel->redraw(); + } + if (passwordInput) { + UITheme::styleInput(passwordInput, true); + } + } + + void clearError() { + if (errorLabel) { + errorLabel->hide(); + } + if (passwordInput) { + UITheme::styleInput(passwordInput, false); + } } Fl_Secret_Input* getPasswordInput() const { return passwordInput; } Fl_Button* getLoginButton() const { return loginButton; } -}; - -// Component for password setup form +};// Enhanced password setup component with validation class PasswordSetupComponent : public FormComponentBase { private: PasswordCallback onSetup; Fl_Secret_Input* newPasswordInput; Fl_Secret_Input* confirmPasswordInput; Fl_Button* createButton; + Fl_Box* strengthIndicator; + Fl_Box* encryptionInfo; - static constexpr int INPUT_WIDTH = 200; - static constexpr int LABEL_WIDTH = 180; + static constexpr int INPUT_WIDTH = 300; public: PasswordSetupComponent(Fl_Group* parent, int x, int y, int w, int h, PasswordCallback onSetup) : FormComponentBase(parent, x, y, w, h), onSetup(onSetup), - newPasswordInput(nullptr), confirmPasswordInput(nullptr), createButton(nullptr) {} + newPasswordInput(nullptr), confirmPasswordInput(nullptr), + createButton(nullptr), strengthIndicator(nullptr), encryptionInfo(nullptr) {} void create() override { - // Create password inputs - newPasswordInput = createWidget(x + LABEL_WIDTH, y, INPUT_WIDTH, INPUT_HEIGHT, "New Master Password:"); - confirmPasswordInput = createWidget(x + LABEL_WIDTH, y + 50, INPUT_WIDTH, INPUT_HEIGHT, "Confirm Password:"); - // Always fetch the current default encryption from config at creation time + int cardPadding = UITheme::Spacing::EXTRA_LARGE; + int formY = y + UITheme::Typography::HEADLINE_3 + UITheme::Spacing::LARGE; + int currentY = formY; + + // Create new password input + newPasswordInput = createWidget( + x + cardPadding, currentY, + INPUT_WIDTH, UITheme::Dimensions::INPUT_HEIGHT, + "New Master Password:" + ); + UITheme::styleInput(newPasswordInput, false); + currentY += UITheme::Dimensions::INPUT_HEIGHT + UITheme::Spacing::MEDIUM; + + // Password strength indicator + strengthIndicator = createWidget( + x + cardPadding, currentY, + INPUT_WIDTH, UITheme::Typography::CAPTION, + "" + ); + UITheme::styleText(strengthIndicator, "caption"); + currentY += UITheme::Typography::CAPTION + UITheme::Spacing::MEDIUM; + + // Create confirm password input + confirmPasswordInput = createWidget( + x + cardPadding, currentY, + INPUT_WIDTH, UITheme::Dimensions::INPUT_HEIGHT, + "Confirm Password:" + ); + UITheme::styleInput(confirmPasswordInput, false); + currentY += UITheme::Dimensions::INPUT_HEIGHT + UITheme::Spacing::MEDIUM; + + // Encryption info ConfigManager& config = ConfigManager::getInstance(); EncryptionType encType = config.getDefaultEncryption(); const char* encTypeCStr = EncryptionUtils::getDisplayName(encType); std::string encTypeStr = encTypeCStr ? std::string(encTypeCStr) : "Unknown"; - std::string msg; - if (encTypeStr == "Unknown") { - msg = "Encryption: Unknown (Check .config!)"; - } else { - msg = "Encryption: " + encTypeStr + " (Default)"; - } - // Debug output - std::cerr << "[PasswordSetupComponent] encType=" << static_cast(encType) << ", encTypeStr='" << encTypeStr << "'\n"; - Fl_Box* encLabel = createWidget(x + LABEL_WIDTH, y + 100, INPUT_WIDTH, INPUT_HEIGHT, ""); - encLabel->copy_label(msg.c_str()); - - // Create button (moved up since we removed the dropdown) - createButton = createWidget(centerX(BUTTON_WIDTH), y + 130, BUTTON_WIDTH, BUTTON_HEIGHT, "Create"); + + std::string encMsg = "🔒 Encryption: " + encTypeStr + " (Default)"; + encryptionInfo = createWidget( + x + cardPadding, currentY, + INPUT_WIDTH, UITheme::Typography::BODY_2, + "" + ); + encryptionInfo->copy_label(encMsg.c_str()); + UITheme::styleText(encryptionInfo, "body2"); + currentY += UITheme::Typography::BODY_2 + UITheme::Spacing::LARGE; + + // Create button + createButton = createWidget( + x + (w - UITheme::Dimensions::BUTTON_MIN_WIDTH) / 2, currentY, + UITheme::Dimensions::BUTTON_MIN_WIDTH, UITheme::Dimensions::BUTTON_HEIGHT, + "Create Password" + ); + UITheme::styleButton(createButton, "primary"); + CallbackHelper::setCallback(createButton, this, [this](PasswordSetupComponent* comp) { - // Fetch the encryption type again in case config changed + if (!comp->validateInputs()) return; + EncryptionType encType = ConfigManager::getInstance().getDefaultEncryption(); - comp->onSetup(comp->newPasswordInput->value(), - comp->confirmPasswordInput->value(), - encType); + comp->onSetup(comp->newPasswordInput->value(), comp->confirmPasswordInput->value(), encType); }); } +private: + bool validateInputs() { + bool valid = true; + + // Check new password + if (strlen(newPasswordInput->value()) == 0) { + valid = false; + } else if (strlen(newPasswordInput->value()) < 8) { + valid = false; + } + + // Check confirm password + if (strlen(confirmPasswordInput->value()) == 0) { + valid = false; + } else if (strcmp(newPasswordInput->value(), confirmPasswordInput->value()) != 0) { + valid = false; + } + + return valid; + } + +public: Fl_Secret_Input* getNewPasswordInput() const { return newPasswordInput; } Fl_Secret_Input* getConfirmPasswordInput() const { return confirmPasswordInput; } Fl_Button* getCreateButton() const { return createButton; } diff --git a/src/gui/GuiUIManager.cpp b/src/gui/GuiUIManager.cpp index b7d4d5e..be5422a 100644 --- a/src/gui/GuiUIManager.cpp +++ b/src/gui/GuiUIManager.cpp @@ -246,58 +246,141 @@ void GuiUIManager::createSetupScreen() { void GuiUIManager::createMainScreen() { try { - createScreen("Password Manager", 600, 450, [this]() { - rootComponent->addChild( - mainWindow.get(), 0, 0, 600, 30, - [this]() { createAddCredentialDialog(); }, - [this]() { openSettingsDialog(); }, - [this]() { openUpdateDialog(); }, - []() { - if (fl_choice("Do you really want to exit?", "Cancel", "Exit", nullptr) == 1) { - exit(0); - } - }, - []() { - fl_message_title("About"); - std::string aboutMessage = "Password Manager " + VersionInfo::getCurrentVersion() + "\n" - "A secure, lightweight password management tool\n" - "2025 - nikitasmen"; - fl_message("%s", aboutMessage.c_str()); + // Create main window with better sizing + mainWindow = std::make_unique( + UITheme::Dimensions::MAIN_WINDOW_MIN_WIDTH, + UITheme::Dimensions::MAIN_WINDOW_MIN_HEIGHT, + "Password Manager" + ); + + // Configure window with proper resizing and responsive behavior + UITheme::configureWindow(mainWindow.get(), true); + + // Make the window responsive + UITheme::makeResponsive(mainWindow.get(), [this](int width, int height) { + this->updateMainScreenLayout(width, height); + }); + + // Create root component that will manage the layout + rootComponent = std::make_unique( + mainWindow.get(), 0, 0, mainWindow->w(), mainWindow->h() + ); + + // Add menu bar + rootComponent->addChild( + mainWindow.get(), 0, 0, mainWindow->w(), UITheme::Dimensions::MENU_HEIGHT, + [this]() { createAddCredentialDialog(); }, + [this]() { openSettingsDialog(); }, + [this]() { openUpdateDialog(); }, + []() { + if (fl_choice("Do you really want to exit?", "Cancel", "Exit", nullptr) == 1) { + exit(0); } - ); + }, + []() { + fl_message_title("About"); + std::string aboutMessage = "Password Manager " + VersionInfo::getCurrentVersion() + "\n" + "A secure, lightweight password management tool\n" + "Built with modern C++17 and FLTK\n" + "2025 - nikitasmen"; + fl_message("%s", aboutMessage.c_str()); + } + ); - // Create the clickable platforms display directly in the main window - clickablePlatformsDisplay = new ClickablePlatformsDisplay( - 20, 50, 560, 300 - ); - mainWindow->add(clickablePlatformsDisplay); - - // Set up click callback - clickablePlatformsDisplay->setClickCallback( - [this](ClickablePlatformsDisplay*, const std::string& platform) { - this->viewCredential(platform); - } - ); + // Create the clickable platforms display with responsive sizing + int contentY = UITheme::Dimensions::MENU_HEIGHT + UITheme::Spacing::MEDIUM; + int contentHeight = mainWindow->h() - contentY - UITheme::Spacing::MEDIUM; + + clickablePlatformsDisplay = new ClickablePlatformsDisplay( + UITheme::Spacing::MEDIUM, + contentY, + mainWindow->w() - 2 * UITheme::Spacing::MEDIUM, + contentHeight + ); + mainWindow->add(clickablePlatformsDisplay); + mainWindow->resizable(clickablePlatformsDisplay); + + // Set up click callback + clickablePlatformsDisplay->setClickCallback( + [this](ClickablePlatformsDisplay*, const std::string& platform) { + this->viewCredential(platform); + } + ); - rootComponent->addChild( - mainWindow.get(), 20, 360, 240, 25, - [this]() { - const char* platform = fl_input("Enter platform name to view:"); - if (platform && strlen(platform) > 0) { - viewCredential(platform); - } - }, - [this]() { - const char* platform = fl_input("Enter platform name to delete:"); - if (platform && strlen(platform) > 0) { - if (fl_choice("Are you sure you want to delete this credential?", "Cancel", "Yes", nullptr) == 1) { - deleteCredential(platform); - } + // Add action buttons with responsive positioning + int buttonY = mainWindow->h() - UITheme::Dimensions::BUTTON_HEIGHT - UITheme::Spacing::MEDIUM; + + rootComponent->addChild( + mainWindow.get(), UITheme::Spacing::MEDIUM, buttonY, + mainWindow->w() - 2 * UITheme::Spacing::MEDIUM, UITheme::Dimensions::BUTTON_HEIGHT, + [this]() { + const char* platform = fl_input("Enter platform name to view:"); + if (platform && strlen(platform) > 0) { + viewCredential(platform); + } + }, + [this]() { + const char* platform = fl_input("Enter platform name to delete:"); + if (platform && strlen(platform) > 0) { + if (fl_choice("Are you sure you want to delete this credential?", "Cancel", "Yes", nullptr) == 1) { + deleteCredential(platform); } } - ); + } + ); - }); + // Create the root component and initialize layout + rootComponent->create(); + + // Refresh platforms list + refreshPlatformsList(); + + // Set up window properties + mainWindow->end(); + mainWindow->show(); + + } catch (const std::exception& e) { + std::cerr << "Error creating main screen: " << e.what() << std::endl; + showMessage("Error", "Failed to create main screen: " + std::string(e.what()), true); + } +} + +void GuiUIManager::updateMainScreenLayout(int width, int height) { + try { + if (!mainWindow || !clickablePlatformsDisplay) return; + + // Update menu bar width + if (auto menuBar = dynamic_cast(mainWindow->child(0))) { + menuBar->size(width, UITheme::Dimensions::MENU_HEIGHT); + } + + // Update platforms display size and position + int contentY = UITheme::Dimensions::MENU_HEIGHT + UITheme::Spacing::MEDIUM; + int buttonAreaHeight = UITheme::Dimensions::BUTTON_HEIGHT + UITheme::Spacing::MEDIUM; + int contentHeight = height - contentY - buttonAreaHeight - UITheme::Spacing::MEDIUM; + + clickablePlatformsDisplay->resize( + UITheme::Spacing::MEDIUM, + contentY, + width - 2 * UITheme::Spacing::MEDIUM, + contentHeight + ); + + // Update action buttons position + int buttonY = height - UITheme::Dimensions::BUTTON_HEIGHT - UITheme::Spacing::MEDIUM; + for (int i = 1; i < mainWindow->children(); i++) { + Fl_Widget* child = mainWindow->child(i); + if (child != clickablePlatformsDisplay) { + child->resize( + UITheme::Spacing::MEDIUM, + buttonY, + width - 2 * UITheme::Spacing::MEDIUM, + UITheme::Dimensions::BUTTON_HEIGHT + ); + } + } + + mainWindow->redraw(); } catch (const std::exception& e) { std::cerr << "Error creating main screen: " << e.what() << std::endl; showMessage("Error", "Failed to create main screen: " + std::string(e.what()), true); @@ -309,8 +392,17 @@ void GuiUIManager::createAddCredentialDialog() { cleanupAddCredentialDialog(); try { - // Create the dialog window with more height to accommodate the encryption dropdown - addCredentialWindow = std::make_unique(450, 400, "Add New Credentials"); + // Create the dialog window with better responsive sizing + addCredentialWindow = std::make_unique( + UITheme::Dimensions::DIALOG_MIN_WIDTH, + 400, + "Add New Credentials" + ); + + // Configure dialog window with proper styling and resizing + UITheme::configureWindow(addCredentialWindow.get(), false); + addCredentialWindow->set_modal(); // Make it modal + addCredentialWindow->begin(); // Create root component for the dialog diff --git a/src/gui/GuiUIManager.h b/src/gui/GuiUIManager.h index 97a4a34..592bb7a 100644 --- a/src/gui/GuiUIManager.h +++ b/src/gui/GuiUIManager.h @@ -86,6 +86,7 @@ class GuiUIManager : public UIManager { void cleanupMainWindow(); void refreshPlatformsList(); void setWindowCloseHandler(Fl_Window* window, bool exitOnClose = false); + void updateMainScreenLayout(int width, int height); // Helper to reduce boilerplate in screen creation void createScreen(const std::string& title, int w, int h, std::function populateScreen); diff --git a/src/gui/LayoutSystem.h b/src/gui/LayoutSystem.h new file mode 100644 index 0000000..8a2d6c8 --- /dev/null +++ b/src/gui/LayoutSystem.h @@ -0,0 +1,356 @@ +#ifndef LAYOUT_SYSTEM_H +#define LAYOUT_SYSTEM_H + +#include "UITheme.h" +#include +#include +#include +#include +#include + +/** + * @class LayoutContainer + * @brief Base class for layout containers with automatic spacing and alignment + */ +class LayoutContainer : public Fl_Group { +protected: + int padding; + int spacing; + +public: + LayoutContainer(int x, int y, int w, int h, int padding = UITheme::Spacing::MEDIUM) + : Fl_Group(x, y, w, h), padding(padding), spacing(UITheme::Spacing::SMALL) { + box(FL_NO_BOX); + } + + virtual void layoutChildren() = 0; + + void setPadding(int newPadding) { + padding = newPadding; + layoutChildren(); + } + + void setSpacing(int newSpacing) { + spacing = newSpacing; + layoutChildren(); + } + + // Override add to trigger layout + void add(Fl_Widget* widget) { + Fl_Group::add(widget); + layoutChildren(); + } + + // Override resize to trigger layout + void resize(int x, int y, int w, int h) { + Fl_Group::resize(x, y, w, h); + layoutChildren(); + } +}; + +/** + * @class VBoxLayout + * @brief Vertical box layout - arranges children vertically with consistent spacing + */ +class VBoxLayout : public LayoutContainer { +private: + enum Alignment { LEFT, CENTER, RIGHT, STRETCH }; + Alignment alignment; + +public: + VBoxLayout(int x, int y, int w, int h, Alignment align = CENTER) + : LayoutContainer(x, y, w, h), alignment(align) {} + + void layoutChildren() override { + if (children() == 0) return; + + int contentWidth = w() - (2 * padding); + int currentY = y() + padding; + + for (int i = 0; i < children(); i++) { + Fl_Widget* child = this->child(i); + if (!child->visible()) continue; + + int childX, childW; + + switch (alignment) { + case LEFT: + childX = x() + padding; + childW = child->w(); + break; + case RIGHT: + childX = x() + w() - padding - child->w(); + childW = child->w(); + break; + case STRETCH: + childX = x() + padding; + childW = contentWidth; + break; + case CENTER: + default: + childX = x() + padding + (contentWidth - child->w()) / 2; + childW = child->w(); + break; + } + + child->resize(childX, currentY, childW, child->h()); + currentY += child->h() + spacing; + } + } + + void setAlignment(Alignment align) { + alignment = align; + layoutChildren(); + } + + // Calculate minimum height needed for all children + int getMinimumHeight() const { + int totalHeight = 2 * padding; + int visibleChildren = 0; + + for (int i = 0; i < children(); i++) { + if (this->child(i)->visible()) { + totalHeight += this->child(i)->h(); + visibleChildren++; + } + } + + if (visibleChildren > 0) { + totalHeight += (visibleChildren - 1) * spacing; + } + + return totalHeight; + } +}; + +/** + * @class HBoxLayout + * @brief Horizontal box layout - arranges children horizontally with consistent spacing + */ +class HBoxLayout : public LayoutContainer { +private: + enum Alignment { TOP, MIDDLE, BOTTOM, STRETCH }; + Alignment alignment; + +public: + HBoxLayout(int x, int y, int w, int h, Alignment align = MIDDLE) + : LayoutContainer(x, y, w, h), alignment(align) {} + + void layoutChildren() override { + if (children() == 0) return; + + int contentHeight = h() - (2 * padding); + int currentX = x() + padding; + + for (int i = 0; i < children(); i++) { + Fl_Widget* child = this->child(i); + if (!child->visible()) continue; + + int childY, childH; + + switch (alignment) { + case TOP: + childY = y() + padding; + childH = child->h(); + break; + case BOTTOM: + childY = y() + h() - padding - child->h(); + childH = child->h(); + break; + case STRETCH: + childY = y() + padding; + childH = contentHeight; + break; + case MIDDLE: + default: + childY = y() + padding + (contentHeight - child->h()) / 2; + childH = child->h(); + break; + } + + child->resize(currentX, childY, child->w(), childH); + currentX += child->w() + spacing; + } + } + + void setAlignment(Alignment align) { + alignment = align; + layoutChildren(); + } + + // Calculate minimum width needed for all children + int getMinimumWidth() const { + int totalWidth = 2 * padding; + int visibleChildren = 0; + + for (int i = 0; i < children(); i++) { + if (this->child(i)->visible()) { + totalWidth += this->child(i)->w(); + visibleChildren++; + } + } + + if (visibleChildren > 0) { + totalWidth += (visibleChildren - 1) * spacing; + } + + return totalWidth; + } +}; + +/** + * @class GridLayout + * @brief Grid layout - arranges children in a grid with consistent spacing + */ +class GridLayout : public LayoutContainer { +private: + int columns; + int rows; + bool autoRows; + +public: + GridLayout(int x, int y, int w, int h, int cols, int rows = -1) + : LayoutContainer(x, y, w, h), columns(cols), rows(rows), autoRows(rows == -1) {} + + void layoutChildren() override { + if (children() == 0 || columns <= 0) return; + + int actualRows = autoRows ? (children() + columns - 1) / columns : rows; + if (actualRows <= 0) return; + + int contentWidth = w() - (2 * padding); + int contentHeight = h() - (2 * padding); + + int cellWidth = (contentWidth - (columns - 1) * spacing) / columns; + int cellHeight = (contentHeight - (actualRows - 1) * spacing) / actualRows; + + for (int i = 0; i < children() && i < columns * actualRows; i++) { + Fl_Widget* child = this->child(i); + if (!child->visible()) continue; + + int col = i % columns; + int row = i / columns; + + int childX = x() + padding + col * (cellWidth + spacing); + int childY = y() + padding + row * (cellHeight + spacing); + + child->resize(childX, childY, cellWidth, cellHeight); + } + } + + void setColumns(int cols) { + columns = cols; + layoutChildren(); + } + + void setRows(int rowCount) { + rows = rowCount; + autoRows = (rowCount == -1); + layoutChildren(); + } +}; + +/** + * @class FormLayout + * @brief Specialized layout for forms with labels and inputs + */ +class FormLayout : public VBoxLayout { +private: + int labelWidth; + int inputSpacing; + +public: + FormLayout(int x, int y, int w, int h, int labelWidth = 120) + : VBoxLayout(x, y, w, h), labelWidth(labelWidth), + inputSpacing(UITheme::Spacing::MEDIUM) {} + + // Add a form row with label and input + void addFormRow(const std::string& labelText, Fl_Widget* input, + const std::string& helpText = "") { + + // Create a horizontal container for this row + int rowHeight = std::max(input->h(), UITheme::Dimensions::INPUT_HEIGHT); + if (!helpText.empty()) { + rowHeight += UITheme::Typography::CAPTION + UITheme::Spacing::TINY; + } + + HBoxLayout* row = new HBoxLayout(0, 0, w() - 2 * padding, rowHeight); + + // Create label + Fl_Box* label = new Fl_Box(0, 0, labelWidth, input->h(), labelText.c_str()); + UITheme::styleText(label, "body1"); + label->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); + row->add(label); + + // Add input + input->size(w() - 2 * padding - labelWidth - inputSpacing, input->h()); + row->add(input); + + // Add help text if provided + if (!helpText.empty()) { + Fl_Box* help = new Fl_Box(labelWidth + inputSpacing, input->h() + UITheme::Spacing::TINY, + w() - 2 * padding - labelWidth - inputSpacing, + UITheme::Typography::CAPTION, helpText.c_str()); + UITheme::styleText(help, "caption"); + help->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE); + row->add(help); + } + + add(row); + } + + void setLabelWidth(int width) { + labelWidth = width; + layoutChildren(); + } +}; + +/** + * @class ResponsiveLayout + * @brief Layout that adapts to different screen sizes + */ +class ResponsiveLayout : public LayoutContainer { +private: + enum BreakPoint { SMALL, MEDIUM, LARGE }; + BreakPoint currentBreakPoint; + + std::function layoutFunction; + + static constexpr int SMALL_BREAKPOINT = 480; + static constexpr int MEDIUM_BREAKPOINT = 768; + +public: + ResponsiveLayout(int x, int y, int w, int h) + : LayoutContainer(x, y, w, h) { + updateBreakPoint(); + } + + void layoutChildren() override { + updateBreakPoint(); + if (layoutFunction) { + layoutFunction(currentBreakPoint); + } + } + + void setLayoutFunction(std::function func) { + layoutFunction = func; + layoutChildren(); + } + + BreakPoint getCurrentBreakPoint() const { + return currentBreakPoint; + } + +private: + void updateBreakPoint() { + if (w() < SMALL_BREAKPOINT) { + currentBreakPoint = SMALL; + } else if (w() < MEDIUM_BREAKPOINT) { + currentBreakPoint = MEDIUM; + } else { + currentBreakPoint = LARGE; + } + } +}; + +#endif // LAYOUT_SYSTEM_H diff --git a/src/gui/ModernComponents.h b/src/gui/ModernComponents.h new file mode 100644 index 0000000..0d509d3 --- /dev/null +++ b/src/gui/ModernComponents.h @@ -0,0 +1,466 @@ +#ifndef MODERN_COMPONENTS_H +#define MODERN_COMPONENTS_H + +#include "GuiComponent.h" +#include "UITheme.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @class ModernCard + * @brief A modern card container with elevation and rounded corners + */ +class ModernCard : public Fl_Group { +private: + int elevation; + std::string title; + +public: + ModernCard(int x, int y, int w, int h, const std::string& title = "", int elevation = 1) + : Fl_Group(x, y, w, h), elevation(elevation), title(title) { + box(FL_FLAT_BOX); + color(UITheme::Colors::SURFACE); + } + + void draw() override { + // Draw card background with elevation + UITheme::drawCard(x(), y(), w(), h(), elevation); + + // Draw title if provided + if (!title.empty()) { + fl_color(UITheme::Colors::TEXT_PRIMARY); + fl_font(FL_HELVETICA_BOLD, UITheme::Typography::HEADLINE_3); + fl_draw(title.c_str(), x() + UITheme::Spacing::MEDIUM, + y() + UITheme::Spacing::MEDIUM + UITheme::Typography::HEADLINE_3); + } + + // Draw children + Fl_Group::draw(); + } + + void setElevation(int newElevation) { + elevation = newElevation; + redraw(); + } + + void setTitle(const std::string& newTitle) { + title = newTitle; + redraw(); + } +}; + +/** + * @class ModernButton + * @brief Enhanced button with hover effects, loading states, and modern styling + */ +class ModernButton : public Fl_Button { +private: + std::string variant; + bool isLoading; + bool isHovered; + std::function clickCallback; + +public: + ModernButton(int x, int y, int w, int h, const std::string& label, + const std::string& variant = "primary") + : Fl_Button(x, y, w, h, label.c_str()), variant(variant), + isLoading(false), isHovered(false) { + + UITheme::styleButton(this, variant); + + // Set up hover tracking + callback([](Fl_Widget* w, void* data) { + ModernButton* btn = static_cast(w); + if (btn->clickCallback && !btn->isLoading) { + btn->clickCallback(); + } + }); + } + + void draw() override { + // Custom drawing for modern appearance + Fl_Color bgColor; + + if (isLoading) { + bgColor = UITheme::Colors::TEXT_DISABLED; + } else if (variant == "primary") { + bgColor = isHovered ? UITheme::Colors::PRIMARY_LIGHT : UITheme::Colors::PRIMARY; + } else if (variant == "secondary") { + bgColor = isHovered ? UITheme::Colors::HOVER : UITheme::Colors::SURFACE_VARIANT; + } else { // outlined + bgColor = isHovered ? UITheme::Colors::HOVER : UITheme::Colors::BACKGROUND; + } + + // Draw button background + fl_color(bgColor); + fl_rectf(x(), y(), w(), h()); + + // Draw border for outlined variant + if (variant == "outlined") { + fl_color(UITheme::Colors::PRIMARY); + fl_rect(x(), y(), w(), h()); + } + + // Draw focus indicator + if (Fl::focus() == this) { + fl_color(UITheme::Colors::FOCUS); + fl_line_style(FL_SOLID, 2); + fl_rect(x() - 2, y() - 2, w() + 4, h() + 4); + fl_line_style(FL_SOLID, 1); + } + + // Draw label + fl_color(labelcolor()); + fl_font(labelfont(), labelsize()); + + if (isLoading) { + fl_draw("Loading...", x(), y(), w(), h(), FL_ALIGN_CENTER); + } else { + fl_draw(label(), x(), y(), w(), h(), FL_ALIGN_CENTER); + } + } + + int handle(int event) override { + switch (event) { + case FL_ENTER: + isHovered = true; + redraw(); + return 1; + case FL_LEAVE: + isHovered = false; + redraw(); + return 1; + case FL_PUSH: + take_focus(); + return 1; + default: + return Fl_Button::handle(event); + } + } + + void setLoading(bool loading) { + isLoading = loading; + redraw(); + } + + void setClickCallback(std::function callback) { + clickCallback = callback; + } + + void setVariant(const std::string& newVariant) { + variant = newVariant; + UITheme::styleButton(this, variant); + redraw(); + } +}; + +/** + * @class ModernInput + * @brief Enhanced input field with validation, placeholders, and modern styling + */ +class ModernInput : public Fl_Input { +private: + std::string placeholder; + std::string errorMessage; + bool hasError; + bool isFocused; + +public: + ModernInput(int x, int y, int w, int h, const std::string& label = "") + : Fl_Input(x, y, w, h, label.c_str()), hasError(false), isFocused(false) { + + UITheme::styleInput(this, false); + + // Set up focus tracking + when(FL_WHEN_CHANGED | FL_WHEN_ENTER_KEY | FL_WHEN_NOT_CHANGED); + } + + void draw() override { + // Custom drawing for modern appearance + UITheme::drawModernInput(this, x(), y(), w(), h(), hasError); + + // Draw the text content + Fl_Input::draw(); + + // Draw placeholder if empty and not focused + if (size() == 0 && !isFocused && !placeholder.empty()) { + fl_color(UITheme::Colors::TEXT_DISABLED); + fl_font(textfont(), textsize()); + fl_draw(placeholder.c_str(), x() + UITheme::Spacing::SMALL, + y() + UITheme::Spacing::SMALL, w() - UITheme::Spacing::MEDIUM, + h() - UITheme::Spacing::MEDIUM, FL_ALIGN_LEFT); + } + + // Draw error message below input + if (hasError && !errorMessage.empty()) { + fl_color(UITheme::Colors::ERROR); + fl_font(FL_HELVETICA, UITheme::Typography::CAPTION); + fl_draw(errorMessage.c_str(), x(), y() + h() + UITheme::Spacing::TINY, + w(), UITheme::Typography::CAPTION + UITheme::Spacing::TINY, + FL_ALIGN_LEFT); + } + } + + int handle(int event) override { + switch (event) { + case FL_FOCUS: + isFocused = true; + if (hasError) { + clearError(); + } + redraw(); + return Fl_Input::handle(event); + case FL_UNFOCUS: + isFocused = false; + redraw(); + return Fl_Input::handle(event); + default: + return Fl_Input::handle(event); + } + } + + void setPlaceholder(const std::string& text) { + placeholder = text; + redraw(); + } + + void setError(const std::string& message) { + hasError = true; + errorMessage = message; + UITheme::styleInput(this, true); + redraw(); + } + + void clearError() { + hasError = false; + errorMessage.clear(); + UITheme::styleInput(this, false); + redraw(); + } + + bool getHasError() const { return hasError; } + + // Calculate total height including error message space + int totalHeight() const { + return h() + (hasError ? UITheme::Typography::CAPTION + UITheme::Spacing::MEDIUM : 0); + } +}; + +/** + * @class ModernSecretInput + * @brief Enhanced secret input field with show/hide toggle and modern styling + */ +class ModernSecretInput : public Fl_Secret_Input { +private: + std::string placeholder; + std::string errorMessage; + bool hasError; + bool isFocused; + bool showPassword; + ModernButton* toggleButton; + +public: + ModernSecretInput(int x, int y, int w, int h, const std::string& label = "") + : Fl_Secret_Input(x, y, w, h, label.c_str()), hasError(false), + isFocused(false), showPassword(false), toggleButton(nullptr) { + + UITheme::styleInput(this, false); + + // Create show/hide toggle button + toggleButton = new ModernButton(x + w - 30, y + 5, 25, h - 10, "👁", "outlined"); + toggleButton->setClickCallback([this]() { + togglePasswordVisibility(); + }); + + when(FL_WHEN_CHANGED | FL_WHEN_ENTER_KEY | FL_WHEN_NOT_CHANGED); + } + + ~ModernSecretInput() { + delete toggleButton; + } + + void draw() override { + // Custom drawing for modern appearance + UITheme::drawModernInput(this, x(), y(), w(), h(), hasError); + + // Draw the text content + if (showPassword) { + // Temporarily convert to regular input for display + type(FL_NORMAL_INPUT); + Fl_Input::draw(); + type(FL_SECRET_INPUT); + } else { + Fl_Secret_Input::draw(); + } + + // Draw placeholder if empty and not focused + if (size() == 0 && !isFocused && !placeholder.empty()) { + fl_color(UITheme::Colors::TEXT_DISABLED); + fl_font(textfont(), textsize()); + fl_draw(placeholder.c_str(), x() + UITheme::Spacing::SMALL, + y() + UITheme::Spacing::SMALL, w() - 40, + h() - UITheme::Spacing::MEDIUM, FL_ALIGN_LEFT); + } + + // Draw toggle button + if (toggleButton) { + toggleButton->draw(); + } + + // Draw error message below input + if (hasError && !errorMessage.empty()) { + fl_color(UITheme::Colors::ERROR); + fl_font(FL_HELVETICA, UITheme::Typography::CAPTION); + fl_draw(errorMessage.c_str(), x(), y() + h() + UITheme::Spacing::TINY, + w(), UITheme::Typography::CAPTION + UITheme::Spacing::TINY, + FL_ALIGN_LEFT); + } + } + + int handle(int event) override { + // Let toggle button handle its events first + if (toggleButton && toggleButton->handle(event)) { + return 1; + } + + switch (event) { + case FL_FOCUS: + isFocused = true; + if (hasError) { + clearError(); + } + redraw(); + return Fl_Secret_Input::handle(event); + case FL_UNFOCUS: + isFocused = false; + redraw(); + return Fl_Secret_Input::handle(event); + default: + return Fl_Secret_Input::handle(event); + } + } + + void resize(int x, int y, int w, int h) override { + Fl_Secret_Input::resize(x, y, w, h); + if (toggleButton) { + toggleButton->resize(x + w - 30, y + 5, 25, h - 10); + } + } + + void setPlaceholder(const std::string& text) { + placeholder = text; + redraw(); + } + + void setError(const std::string& message) { + hasError = true; + errorMessage = message; + UITheme::styleInput(this, true); + redraw(); + } + + void clearError() { + hasError = false; + errorMessage.clear(); + UITheme::styleInput(this, false); + redraw(); + } + + bool getHasError() const { return hasError; } + + void togglePasswordVisibility() { + showPassword = !showPassword; + toggleButton->copy_label(showPassword ? "🙈" : "👁"); + redraw(); + } + + // Calculate total height including error message space + int totalHeight() const { + return h() + (hasError ? UITheme::Typography::CAPTION + UITheme::Spacing::MEDIUM : 0); + } +}; + +/** + * @class ModernProgressBar + * @brief Modern progress bar with smooth animations and status text + */ +class ModernProgressBar : public Fl_Progress { +private: + std::string statusText; + bool isIndeterminate; + +public: + ModernProgressBar(int x, int y, int w, int h) + : Fl_Progress(x, y, w, h), isIndeterminate(false) { + + selection_color(UITheme::Colors::PRIMARY); + color(UITheme::Colors::SURFACE_VARIANT); + minimum(0.0); + maximum(100.0); + value(0.0); + } + + void draw() override { + // Draw background + fl_color(color()); + fl_rectf(x(), y(), w(), h()); + + // Draw progress + if (isIndeterminate) { + // Animated indeterminate progress (simplified) + fl_color(selection_color()); + int progressWidth = w() / 4; + int offset = (int)(value()) % (w() + progressWidth); + fl_rectf(x() + offset - progressWidth, y(), progressWidth, h()); + } else { + // Regular progress bar + int progressWidth = (int)((value() / maximum()) * w()); + fl_color(selection_color()); + fl_rectf(x(), y(), progressWidth, h()); + } + + // Draw border + fl_color(UITheme::Colors::BORDER); + fl_rect(x(), y(), w(), h()); + + // Draw status text + if (!statusText.empty()) { + fl_color(UITheme::Colors::TEXT_PRIMARY); + fl_font(FL_HELVETICA, UITheme::Typography::CAPTION); + fl_draw(statusText.c_str(), x(), y() + h() + UITheme::Spacing::TINY, + w(), UITheme::Typography::CAPTION, FL_ALIGN_CENTER); + } + } + + void setStatusText(const std::string& text) { + statusText = text; + redraw(); + } + + void setIndeterminate(bool indeterminate) { + isIndeterminate = indeterminate; + redraw(); + } + + void updateProgress(double percent, const std::string& status = "") { + value(percent); + if (!status.empty()) { + setStatusText(status); + } + redraw(); + } +}; + +#endif // MODERN_COMPONENTS_H diff --git a/src/gui/UITheme.cpp b/src/gui/UITheme.cpp new file mode 100644 index 0000000..960b049 --- /dev/null +++ b/src/gui/UITheme.cpp @@ -0,0 +1,313 @@ +#include "UITheme.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +bool UITheme::initialized = false; + +void UITheme::initialize() { + if (initialized) return; + + // Set the default color scheme + Fl::set_color(FL_BACKGROUND_COLOR, Colors::BACKGROUND); + Fl::set_color(FL_BACKGROUND2_COLOR, Colors::SURFACE); + Fl::set_color(FL_FOREGROUND_COLOR, Colors::TEXT_PRIMARY); + Fl::set_color(FL_SELECTION_COLOR, Colors::PRIMARY); + + // Set default fonts and sizes + Fl::set_font(FL_HELVETICA, "Ubuntu"); + Fl::set_font(FL_HELVETICA_BOLD, "Ubuntu Bold"); + + initialized = true; +} + +void UITheme::styleButton(Fl_Widget* button, const std::string& variant) { + if (!button) return; + + button->labelsize(Typography::BUTTON); + button->labelfont(FL_HELVETICA_BOLD); + + if (variant == "primary") { + button->color(Colors::PRIMARY); + button->labelcolor(Colors::TEXT_PRIMARY); + button->selection_color(Colors::PRIMARY_DARK); + } else if (variant == "secondary") { + button->color(Colors::SURFACE_VARIANT); + button->labelcolor(Colors::TEXT_PRIMARY); + button->selection_color(Colors::HOVER); + } else if (variant == "outlined") { + button->color(Colors::BACKGROUND); + button->labelcolor(Colors::PRIMARY); + button->selection_color(Colors::HOVER); + button->box(FL_BORDER_BOX); + } + + // Set minimum dimensions + if (button->w() < Dimensions::BUTTON_MIN_WIDTH) { + button->size(Dimensions::BUTTON_MIN_WIDTH, button->h()); + } + if (button->h() < Dimensions::BUTTON_HEIGHT) { + button->size(button->w(), Dimensions::BUTTON_HEIGHT); + } +} + +void UITheme::styleInput(Fl_Widget* input, bool hasError) { + if (!input) return; + + input->labelsize(Typography::BODY_1); + + // Only set text properties if it's actually an input widget + Fl_Input* inputWidget = dynamic_cast(input); + if (inputWidget) { + inputWidget->textsize(Typography::BODY_1); + inputWidget->textfont(FL_HELVETICA); + + if (hasError) { + inputWidget->color(Colors::SURFACE); + inputWidget->textcolor(Colors::TEXT_PRIMARY); + inputWidget->selection_color(Colors::ERROR); + inputWidget->box(FL_DOWN_BOX); + } else { + inputWidget->color(Colors::SURFACE); + inputWidget->textcolor(Colors::TEXT_PRIMARY); + inputWidget->selection_color(Colors::PRIMARY); + inputWidget->box(FL_DOWN_BOX); + } + } + + // Set standard height + if (input->h() < Dimensions::INPUT_HEIGHT) { + input->size(input->w(), Dimensions::INPUT_HEIGHT); + } +} + +void UITheme::styleWindow(Fl_Window* window, bool isResizable, int minWidth, int minHeight) { + if (!window) return; + + window->color(Colors::BACKGROUND); + window->labelcolor(Colors::TEXT_PRIMARY); + window->labelfont(FL_HELVETICA_BOLD); + window->labelsize(Typography::HEADLINE_2); + + if (isResizable) { + window->resizable(window); + window->size_range(minWidth, minHeight); + } + + // Set up better window behavior + window->callback([](Fl_Widget* w, void* data) { + // Handle window close properly + if (Fl::event() == FL_CLOSE) { + w->hide(); + } + }); +} + +void UITheme::styleText(Fl_Widget* widget, const std::string& variant) { + if (!widget) return; + + widget->labelcolor(Colors::TEXT_PRIMARY); + widget->color(Colors::BACKGROUND); + + if (variant == "headline1") { + widget->labelsize(Typography::HEADLINE_1); + widget->labelfont(FL_HELVETICA_BOLD); + widget->labelcolor(Colors::TEXT_PRIMARY); + } else if (variant == "headline2") { + widget->labelsize(Typography::HEADLINE_2); + widget->labelfont(FL_HELVETICA_BOLD); + widget->labelcolor(Colors::TEXT_PRIMARY); + } else if (variant == "headline3") { + widget->labelsize(Typography::HEADLINE_3); + widget->labelfont(FL_HELVETICA_BOLD); + widget->labelcolor(Colors::TEXT_PRIMARY); + } else if (variant == "body1") { + widget->labelsize(Typography::BODY_1); + widget->labelfont(FL_HELVETICA); + widget->labelcolor(Colors::TEXT_PRIMARY); + } else if (variant == "body2") { + widget->labelsize(Typography::BODY_2); + widget->labelfont(FL_HELVETICA); + widget->labelcolor(Colors::TEXT_SECONDARY); + } else if (variant == "caption") { + widget->labelsize(Typography::CAPTION); + widget->labelfont(FL_HELVETICA); + widget->labelcolor(Colors::TEXT_SECONDARY); + } +} + +void UITheme::drawCard(int x, int y, int w, int h, int elevation) { + // Draw shadow based on elevation + if (elevation > 0) { + fl_color(fl_darker(Colors::BACKGROUND)); + for (int i = 0; i < elevation; i++) { + fl_rectf(x + i, y + i, w, h); + } + } + + // Draw card background + fl_color(Colors::SURFACE); + fl_rectf(x, y, w, h); + + // Draw subtle border + fl_color(Colors::BORDER); + fl_rect(x, y, w, h); +} + +void UITheme::drawRoundedRect(int x, int y, int w, int h, int radius, Fl_Color color) { + fl_color(color); + + // For now, use regular rectangles since FLTK doesn't have native rounded rect support + // In a real implementation, you'd use platform-specific drawing or a graphics library + fl_rectf(x, y, w, h); + + // Draw border to simulate rounded appearance + fl_color(fl_darker(color)); + fl_rect(x, y, w, h); +} + +Fl_Color UITheme::getContrastTextColor(Fl_Color backgroundColor) { + // Simple contrast calculation - in a real implementation you'd use proper luminance calculation + unsigned char r, g, b; + Fl::get_color(backgroundColor, r, g, b); + + int luminance = (r * 299 + g * 587 + b * 114) / 1000; + return (luminance > 128) ? Colors::BACKGROUND : Colors::TEXT_PRIMARY; +} + +Fl_Color UITheme::hexToFlColor(const std::string& hex) { + if (hex.empty() || hex[0] != '#' || hex.length() != 7) { + return FL_BLACK; + } + + std::string hexValue = hex.substr(1); + unsigned long color = std::stoul(hexValue, nullptr, 16); + + // Convert to FLTK color format (RRGGBB00) + return static_cast((color << 8) | 0x00); +} + +void UITheme::applyThemeToWindow(Fl_Window* window) { + if (!window) return; + + initialize(); + configureWindow(window); + + // Recursively style all child widgets + for (int i = 0; i < window->children(); i++) { + Fl_Widget* child = window->child(i); + + // Apply appropriate styling based on widget type + if (dynamic_cast(child)) { + UITheme::styleButton(child, "primary"); + } else if (dynamic_cast(child)) { + UITheme::styleInput(child, false); + } else if (dynamic_cast(child)) { + applyThemeToWindow(dynamic_cast(child)); + } else { + UITheme::styleText(child, "body1"); + } + } +} + +void UITheme::makeResponsive(Fl_Window* window, + std::function contentCallback) { + if (!window) return; + + // Store the callback for resize events + window->user_data(new std::function(contentCallback)); + + // Set up resize callback + window->callback([](Fl_Widget* w, void* data) { + if (Fl::event() == FL_CLOSE) { + w->hide(); + return; + } + + Fl_Window* win = static_cast(w); + auto* callback = static_cast*>(win->user_data()); + if (callback) { + (*callback)(win->w(), win->h()); + } + }); +} + +void UITheme::configureWindow(Fl_Window* window, bool isMain) { + if (!window) return; + + if (isMain) { + window->size_range(Dimensions::MAIN_WINDOW_MIN_WIDTH, Dimensions::MAIN_WINDOW_MIN_HEIGHT); + window->resizable(window); + + // Center on screen + centerWindow(window); + + // Set better default size + auto [screenW, screenH] = getScreenDimensions(); + int defaultW = std::min(1200, screenW * 3 / 4); + int defaultH = std::min(800, screenH * 3 / 4); + window->size(defaultW, defaultH); + + } else { + // Dialog windows + window->size_range(Dimensions::DIALOG_MIN_WIDTH, Dimensions::DIALOG_MIN_HEIGHT); + window->resizable(window); + centerWindow(window); + } + + // Apply consistent styling + styleWindow(window, true); +} + +void UITheme::centerWindow(Fl_Window* window) { + if (!window) return; + + auto [screenW, screenH] = getScreenDimensions(); + int x = (screenW - window->w()) / 2; + int y = (screenH - window->h()) / 2; + + // Ensure window stays on screen + x = std::max(0, std::min(x, screenW - window->w())); + y = std::max(0, std::min(y, screenH - window->h())); + + window->position(x, y); +} + +std::pair UITheme::getScreenDimensions() { + return {Fl::w(), Fl::h()}; +} + +void UITheme::drawModernButton(Fl_Widget* widget, int x, int y, int w, int h, const std::string& variant) { + // Custom button drawing implementation + Fl_Color buttonColor = (variant == "primary") ? Colors::PRIMARY : Colors::SURFACE_VARIANT; + + // Draw button background with slight rounding effect + drawRoundedRect(x, y, w, h, BorderRadius::BUTTON, buttonColor); + + // Draw hover state if needed + if (Fl::focus() == widget) { + fl_color(Colors::FOCUS); + fl_rect(x, y, w, h); + } +} + +void UITheme::drawModernInput(Fl_Widget* widget, int x, int y, int w, int h, bool hasError) { + // Custom input drawing implementation + Fl_Color inputColor = hasError ? fl_color_average(Colors::ERROR, Colors::SURFACE, 0.1f) : Colors::SURFACE; + + // Draw input background + fl_color(inputColor); + fl_rectf(x, y, w, h); + + // Draw border + Fl_Color borderColor = hasError ? Colors::ERROR : + (Fl::focus() == widget) ? Colors::BORDER_FOCUS : Colors::BORDER; + fl_color(borderColor); + fl_rect(x, y, w, h); +} diff --git a/src/gui/UITheme.h b/src/gui/UITheme.h new file mode 100644 index 0000000..eb78a46 --- /dev/null +++ b/src/gui/UITheme.h @@ -0,0 +1,229 @@ +#ifndef UI_THEME_H +#define UI_THEME_H + +#include +#include +#include +#include +#include +#include + +/** + * @class UITheme + * @brief Modern UI theme system for the password manager + * + * Provides a consistent, modern color scheme and styling system + * for all GUI components following Material Design principles. + */ +class UITheme { +public: + // Modern Color Palette - Dark theme with blue accent + struct Colors { + // Primary colors + static constexpr Fl_Color PRIMARY = 0x1976D200; // Material Blue 700 + static constexpr Fl_Color PRIMARY_LIGHT = 0x42A5F500; // Material Blue 400 + static constexpr Fl_Color PRIMARY_DARK = 0x0D47A100; // Material Blue 900 + + // Surface colors (dark theme) + static constexpr Fl_Color SURFACE = 0x1E1E1E00; // Dark gray + static constexpr Fl_Color SURFACE_VARIANT = 0x2C2C2C00; // Lighter dark gray + static constexpr Fl_Color BACKGROUND = 0x12121200; // Very dark gray + + // Text colors + static constexpr Fl_Color TEXT_PRIMARY = 0xFFFFFF00; // White + static constexpr Fl_Color TEXT_SECONDARY = 0xB0B0B000; // Light gray + static constexpr Fl_Color TEXT_DISABLED = 0x70707000; // Darker gray + + // Status colors + static constexpr Fl_Color SUCCESS = 0x4CAF5000; // Material Green 500 + static constexpr Fl_Color WARNING = 0xFF9800FF; // Material Orange 500 + static constexpr Fl_Color ERROR = 0xF4433600; // Material Red 500 + + // Interactive states + static constexpr Fl_Color HOVER = 0x333333FF; // Hover overlay + static constexpr Fl_Color PRESSED = 0x404040FF; // Pressed state + static constexpr Fl_Color FOCUS = 0x64B5F6FF; // Focus indicator + + // Border colors + static constexpr Fl_Color BORDER = 0x404040FF; // Default border + static constexpr Fl_Color BORDER_FOCUS = 0x1976D2FF; // Focused border + }; + + // Typography scale + struct Typography { + static constexpr int HEADLINE_1 = 24; // Large titles + static constexpr int HEADLINE_2 = 20; // Section headers + static constexpr int HEADLINE_3 = 18; // Subsection headers + static constexpr int BODY_1 = 14; // Primary body text + static constexpr int BODY_2 = 12; // Secondary body text + static constexpr int CAPTION = 10; // Small text, captions + static constexpr int BUTTON = 14; // Button text + }; + + // Spacing system (8pt grid) + struct Spacing { + static constexpr int UNIT = 8; // Base unit + static constexpr int TINY = UNIT / 2; // 4px + static constexpr int SMALL = UNIT; // 8px + static constexpr int MEDIUM = UNIT * 2; // 16px + static constexpr int LARGE = UNIT * 3; // 24px + static constexpr int EXTRA_LARGE = UNIT * 4; // 32px + static constexpr int EXTRA_HUGE = UNIT * 6; // 48px (renamed from HUGE) + }; + + // Component dimensions + struct Dimensions { + static constexpr int BUTTON_HEIGHT = 36; + static constexpr int INPUT_HEIGHT = 40; + static constexpr int MENU_HEIGHT = 28; + static constexpr int BUTTON_MIN_WIDTH = 88; + static constexpr int DIALOG_MIN_WIDTH = 480; + static constexpr int DIALOG_MIN_HEIGHT = 320; + static constexpr int MAIN_WINDOW_MIN_WIDTH = 800; + static constexpr int MAIN_WINDOW_MIN_HEIGHT = 600; + static constexpr int WINDOW_PADDING = 20; + static constexpr int FORM_MAX_WIDTH = 400; + }; + + // Border radius for rounded corners + struct BorderRadius { + static constexpr int SMALL = 4; + static constexpr int MEDIUM = 8; + static constexpr int LARGE = 12; + static constexpr int BUTTON = 6; + static constexpr int CARD = 8; + }; + + // Shadow definitions for depth + struct Shadow { + static constexpr int ELEVATION_1 = 1; // Subtle shadow + static constexpr int ELEVATION_2 = 2; // Card shadow + static constexpr int ELEVATION_3 = 4; // Dialog shadow + static constexpr int ELEVATION_4 = 8; // Menu shadow + }; + +public: + /** + * @brief Initialize the theme system + * Sets up custom colors and styling for FLTK + */ + static void initialize(); + + /** + * @brief Apply modern button styling + * @param button Button widget to style + * @param variant Button variant (primary, secondary, outlined) + */ + static void styleButton(Fl_Widget* button, const std::string& variant = "primary"); + + /** + * @brief Apply modern input field styling + * @param input Input widget to style + * @param hasError Whether the input has validation errors + */ + static void styleInput(Fl_Widget* input, bool hasError = false); + + /** + * @brief Apply window styling with resizable support + * @param window Window to style + * @param isResizable Whether the window should be resizable + * @param minWidth Minimum width for resizable windows + * @param minHeight Minimum height for resizable windows + */ + static void styleWindow(Fl_Window* window, bool isResizable = true, + int minWidth = Dimensions::DIALOG_MIN_WIDTH, + int minHeight = Dimensions::DIALOG_MIN_HEIGHT); + + /** + * @brief Apply text styling + * @param widget Text widget to style + * @param variant Text variant (headline1, body1, etc.) + */ + static void styleText(Fl_Widget* widget, const std::string& variant = "body1"); + + /** + * @brief Draw a modern card background + * @param x X position + * @param y Y position + * @param w Width + * @param h Height + * @param elevation Shadow elevation (0-4) + */ + static void drawCard(int x, int y, int w, int h, int elevation = 1); + + /** + * @brief Draw a modern rounded rectangle + * @param x X position + * @param y Y position + * @param w Width + * @param h Height + * @param radius Border radius + * @param color Fill color + */ + static void drawRoundedRect(int x, int y, int w, int h, int radius, Fl_Color color); + + /** + * @brief Get an appropriate text color for the given background + * @param backgroundColor Background color + * @return Appropriate text color (light or dark) + */ + static Fl_Color getContrastTextColor(Fl_Color backgroundColor); + + /** + * @brief Convert hex color to FLTK color + * @param hex Hex color string (e.g., "#FF0000") + * @return FLTK color value + */ + static Fl_Color hexToFlColor(const std::string& hex); + + /** + * @brief Create a responsive layout that adapts to window size + * @param window Window to make responsive + * @param contentCallback Function called when window is resized + */ + static void makeResponsive(Fl_Window* window, + std::function contentCallback); + + /** + * @brief Set up window with proper sizing constraints and resizing behavior + * @param window Window to configure + * @param isMain Whether this is the main application window + */ + static void configureWindow(Fl_Window* window, bool isMain = false); + + /** + * @brief Center window on screen + * @param window Window to center + */ + static void centerWindow(Fl_Window* window); + + /** + * @brief Apply theme to a window and all its child widgets recursively + * @param window Window to apply theme to + */ + static void applyThemeToWindow(Fl_Window* window); + + /** + * @brief Get screen dimensions + * @return std::pair of screen + */ + static std::pair getScreenDimensions(); + + /** + * @brief Custom input draw function for modern appearance + */ + private: + static bool initialized; + + /** + * @brief Custom button draw function for modern appearance + */ + static void drawModernButton(Fl_Widget* widget, int x, int y, int w, int h, const std::string& variant); + + /** + * @brief Custom input draw function for modern appearance + */ + static void drawModernInput(Fl_Widget* widget, int x, int y, int w, int h, bool hasError); +}; + +#endif // UI_THEME_H diff --git a/src/gui/UpdateDialog.cpp b/src/gui/UpdateDialog.cpp index 06d234b..ca72675 100644 --- a/src/gui/UpdateDialog.cpp +++ b/src/gui/UpdateDialog.cpp @@ -71,21 +71,33 @@ void UpdateDialog::setupUI() { // Progress bar (initially hidden) progressBar = new Fl_Progress(20, 290, 460, 25); - progressBar->minimum(0); - progressBar->maximum(100); - progressBar->value(0); + progressBar->selection_color(UITheme::Colors::PRIMARY); + progressBar->color(UITheme::Colors::SURFACE_VARIANT); progressBar->hide(); + window->add(progressBar); // Buttons checkButton = new Fl_Button(20, 330, 120, 30, "Check for Updates"); - checkButton->callback(checkButtonCallback, this); + UITheme::styleButton(checkButton, "primary"); + checkButton->callback([](Fl_Widget* w, void* data) { + static_cast(data)->onCheckForUpdates(); + }, this); + window->add(checkButton); downloadButton = new Fl_Button(160, 330, 120, 30, "Download Update"); - downloadButton->callback(downloadButtonCallback, this); - downloadButton->deactivate(); // Initially disabled + UITheme::styleButton(downloadButton, "primary"); + downloadButton->callback([](Fl_Widget* w, void* data) { + static_cast(data)->onDownloadUpdate(); + }, this); + downloadButton->hide(); + window->add(downloadButton); closeButton = new Fl_Button(360, 330, 120, 30, "Close"); - closeButton->callback(closeButtonCallback, this); + UITheme::styleButton(closeButton, "secondary"); + closeButton->callback([](Fl_Widget* w, void* data) { + static_cast(data)->onClose(); + }, this); + window->add(closeButton); window->end(); window->set_modal(); @@ -291,18 +303,13 @@ void UpdateDialog::onClose() { hide(); } -// Static callback functions for FLTK -void UpdateDialog::checkButtonCallback(Fl_Widget* widget, void* data) { - auto* dialog = static_cast(data); - dialog->onCheckForUpdates(); -} - -void UpdateDialog::downloadButtonCallback(Fl_Widget* widget, void* data) { - auto* dialog = static_cast(data); - dialog->onDownloadUpdate(); -} - -void UpdateDialog::closeButtonCallback(Fl_Widget* widget, void* data) { - auto* dialog = static_cast(data); - dialog->onClose(); +void UpdateDialog::onDownloadProgress(int percentage, const std::string& status) { + if (progressBar) { + progressBar->value(percentage); + progressBar->redraw(); + } + if (statusLabel) { + statusLabel->copy_label(status.c_str()); + statusLabel->redraw(); + } } diff --git a/src/gui/UpdateDialog.h b/src/gui/UpdateDialog.h index abf3d66..b5243bb 100644 --- a/src/gui/UpdateDialog.h +++ b/src/gui/UpdateDialog.h @@ -7,6 +7,7 @@ #include #include #include +#include "UITheme.h" #include #include #include "../updater/AppUpdater.h" @@ -52,8 +53,10 @@ class UpdateDialog { bool visible() const; private: - // UI Components + // UI Components - Using standard FLTK components with theming std::unique_ptr window; + + Fl_Box* titleLabel; Fl_Box* statusLabel; Fl_Box* versionLabel; Fl_Text_Display* releaseNotesDisplay; @@ -83,10 +86,8 @@ class UpdateDialog { void onDownloadUpdate(); void onClose(); - // Static callbacks for FLTK - static void checkButtonCallback(Fl_Widget* widget, void* data); - static void downloadButtonCallback(Fl_Widget* widget, void* data); - static void closeButtonCallback(Fl_Widget* widget, void* data); + // Progress callback for download + void onDownloadProgress(int percentage, const std::string& status); }; #endif // UPDATE_DIALOG_H