// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include "color_var_resolver.h" #include "element_styles.h" #include "imgui.h" #include #include #include #include namespace dragonx { namespace ui { namespace schema { /** * @brief Responsive breakpoint state */ enum class Breakpoint { Compact, // Small window (e.g., < 500px wide) Normal, // Default Expanded // Large window (e.g., > 900px wide) }; /** * @brief Unified UI schema singleton * * Loads theme palette, globals, and per-section element styles from a single * TOML file. Provides merged lookups (section → globals → fallback) and * poll-based hot-reload. * * Usage: * auto& UI = UISchema::instance(); * UI.loadFromFile("res/themes/ui.toml"); * * // In main loop: * UI.pollForChanges(); * UI.applyIfDirty(); * * // Lookups: * auto btn = UI.button("dialogs.about", "close-button"); * auto win = UI.window("dialogs.about"); * ImU32 col = UI.resolveColor("var(--primary)"); */ class UISchema { public: /** * @brief Get the singleton instance */ static UISchema& instance(); /** * @brief Load a unified skin file * @param path Path to TOML skin file (e.g., "res/themes/ui.toml") * @return true if loaded successfully */ bool loadFromFile(const std::string& path); /** * @brief Load from a TOML string (e.g., embedded data fallback) * @param tomlStr Raw TOML text * @param label Human-readable label for log messages * @return true if parsed successfully */ bool loadFromString(const std::string& tomlStr, const std::string& label = "embedded"); /** * @brief Merge a theme overlay on top of the currently loaded base. * * Only replaces visual properties (theme palette/elevation/images and * backdrop section). Layout values from the base are preserved. * * @param path Path to the overlay TOML (e.g., dark.toml, light.toml) * @return true if overlay was parsed and merged successfully */ bool mergeOverlayFromFile(const std::string& path); /** * @brief Check if a schema file has been loaded successfully */ bool isLoaded() const { return loaded_; } /** * @brief Get the path of the currently loaded file */ const std::string& currentPath() const { return currentPath_; } /** * @brief Get the base layout file path (ui.toml) */ const std::string& basePath() const { return basePath_; } /** * @brief Check if the base theme was loaded from an embedded string. * When true, reloadBase() uses the stored embedded data instead of a file. */ bool hasEmbeddedBase() const { return !embeddedTomlStr_.empty(); } /** * @brief Reload the base theme (ui.toml) from file or embedded data. * Used by SkinManager before merging an overlay to ensure layout values * are always restored from the authoritative base, even on platforms * where ui.toml is embedded (e.g., single-file Windows distribution). * @return true if base was reloaded successfully */ bool reloadBase(); /** * @brief Get the active overlay file path (empty if no overlay) */ const std::string& overlayPath() const { return overlayPath_; } // ==================================================================== // Hot-reload // ==================================================================== /** * @brief Poll for file changes (call from main loop) * * Checks file modification time every pollInterval_ seconds. * Sets dirty flag if file has changed. */ void pollForChanges(); /** * @brief Check if the schema needs re-applying */ bool isDirty() const { return dirty_; } /** * @brief Re-parse and apply if file has changed */ void applyIfDirty(); /** * @brief Check and consume the fonts-changed flag. * Returns true once after a hot-reload that changed font sizes. */ bool consumeFontsChanged() { bool v = fonts_changed_; fonts_changed_ = false; return v; } /** * @brief Re-apply Material Design colors from schema palette to ImGui * * Builds a ColorTheme from the current palette and applies to ImGui style. * Called automatically during hot-reload. */ void reapplyColorsToImGui(); /** * @brief Set the poll interval in seconds (default 0.5) */ void setPollInterval(float seconds) { pollInterval_ = seconds; } // ==================================================================== // Color resolution // ==================================================================== /** * @brief Get the color variable resolver (for direct access) */ const ColorVarResolver& colors() const { return colorResolver_; } /** * @brief Resolve a color string to ImU32 * @param ref Color string: "var(--primary)", "#hex", "rgba(...)", etc. * @param fallback Value if resolution fails */ ImU32 resolveColor(const std::string& ref, ImU32 fallback = IM_COL32(0,0,0,0)) const; // ==================================================================== // Font resolution // ==================================================================== /** * @brief Resolve a font name string to ImFont* * @param fontName e.g., "button", "button-sm", "button-lg", "h4", "body1" * @return ImFont* pointer, or nullptr if name is empty/unknown */ ImFont* resolveFont(const std::string& fontName) const; // ==================================================================== // Responsive // ==================================================================== /** * @brief Get current breakpoint based on window/viewport size */ Breakpoint currentBreakpoint() const; /** * @brief Get the breakpoint config */ const BreakpointConfig& breakpoints() const { return breakpoints_; } // ==================================================================== // Element lookups — merged (section → globals → C++ default) // ==================================================================== /** * @brief Look up a button style * @param section Dot-separated section path: "dialogs.about", "tabs.send" * @param name Element name within the section: "close-button" * @return Merged ButtonStyle (responsive overrides already applied) */ ButtonStyle button(const std::string& section, const std::string& name) const; /** * @brief Look up an input style */ InputStyle input(const std::string& section, const std::string& name) const; /** * @brief Look up a label style */ LabelStyle label(const std::string& section, const std::string& name) const; /** * @brief Look up a table style */ TableStyle table(const std::string& section, const std::string& name) const; /** * @brief Look up a checkbox style */ CheckboxStyle checkbox(const std::string& section, const std::string& name) const; /** * @brief Look up a combo style */ ComboStyle combo(const std::string& section, const std::string& name) const; /** * @brief Look up a slider style */ SliderStyle slider(const std::string& section, const std::string& name) const; /** * @brief Look up a window/dialog style * @param section Section path: "dialogs.about" * @param name Defaults to "window" — the window block within that section */ WindowStyle window(const std::string& section, const std::string& name = "window") const; /** * @brief Look up a separator style */ SeparatorStyle separator(const std::string& section, const std::string& name) const; /** * @brief Look up a DrawList custom element style * * Returns a cached const reference. The cache is invalidated on * hot-reload (applyIfDirty) and full loads. For missing elements * a static empty sentinel is returned. */ const DrawElementStyle& drawElement(const std::string& section, const std::string& name) const; /** * @brief Find a raw stored element by section and name. * Returns an opaque pointer to the stored data (toml::table* at runtime), * or nullptr if not found. Consumer must cast via * static_cast(ptr) after including . */ const void* findElement(const std::string& section, const std::string& name) const; // ==================================================================== // Global defaults access // ==================================================================== const ButtonStyle& defaultButton() const { return globalButton_; } const InputStyle& defaultInput() const { return globalInput_; } const LabelStyle& defaultLabel() const { return globalLabel_; } const TableStyle& defaultTable() const { return globalTable_; } const CheckboxStyle& defaultCheckbox() const { return globalCheckbox_; } const ComboStyle& defaultCombo() const { return globalCombo_; } const SliderStyle& defaultSlider() const { return globalSlider_; } const WindowStyle& defaultWindow() const { return globalWindow_; } const SeparatorStyle& defaultSeparator() const { return globalSeparator_; } // ==================================================================== // Theme metadata // ==================================================================== const std::string& themeName() const { return themeName_; } bool isDarkTheme() const { return darkTheme_; } /** * @brief Monotonically increasing generation counter. * Incremented on every loadFromFile / mergeOverlay / applyIfDirty. * Downstream caches compare against this to invalidate. */ uint32_t generation() const { return generation_; } /** * @brief Get theme-specified background image path (empty = use default) */ const std::string& backgroundImagePath() const { return backgroundImagePath_; } /** * @brief Get theme-specified logo image path (empty = use default) */ const std::string& logoImagePath() const { return logoImagePath_; } private: UISchema() = default; ~UISchema() = default; UISchema(const UISchema&) = delete; UISchema& operator=(const UISchema&) = delete; // Parse top-level sections from a TOML table (passed as opaque void*) void parseTheme(const void* dataObj); void parseGlobals(const void* dataObj); void parseSections(const void* dataObj, const std::string& prefix); void parseFlatSection(const void* dataObj, const std::string& prefix); void parseBreakpoints(const void* dataObj); // ==================================================================== // State // ==================================================================== bool loaded_ = false; std::string currentPath_; ///< Path currently loaded (base or overlay for hot-reload) std::string basePath_; ///< Path to the base layout file (ui.toml) std::string overlayPath_; ///< Path to active overlay file (empty if base-only) std::string embeddedTomlStr_; ///< Raw TOML string from loadFromString (for reload) // Hot-reload float pollInterval_ = 0.5f; double lastPollTime_ = 0; std::filesystem::file_time_type lastModTime_; std::filesystem::file_time_type baseModTime_; ///< Track base file changes when overlay active bool dirty_ = false; bool fonts_changed_ = false; // Theme std::string themeName_; bool darkTheme_ = true; std::string backgroundImagePath_; std::string logoImagePath_; ColorVarResolver colorResolver_; uint32_t generation_ = 0; ///< bumped on every load/merge/reload // Breakpoints BreakpointConfig breakpoints_; // Global defaults ButtonStyle globalButton_; InputStyle globalInput_; LabelStyle globalLabel_; TableStyle globalTable_; CheckboxStyle globalCheckbox_; ComboStyle globalCombo_; SliderStyle globalSlider_; WindowStyle globalWindow_; SeparatorStyle globalSeparator_; // Per-section element storage // Key: "section.elementName" → stored TOML table (accessed via std::any_cast) // This avoids storing every possible style struct up front — // we only parse what's actually looked up. struct StoredElement { std::any data; // holds toml::table at runtime }; std::unordered_map elements_; // Parsed DrawElementStyle cache — avoids repeated TOML table iteration. // Populated lazily on first drawElement() lookup, cleared on reload. mutable std::unordered_map styleCache_; }; // Convenience alias inline UISchema& UI() { return UISchema::instance(); } } // namespace schema } // namespace ui } // namespace dragonx