- Replace all hardcoded English strings with TR() translation keys across every tab, dialog, and component (~20 UI files) - Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with complete translations (~37k lines added) - Improve i18n loader with exe-relative path fallback and English base fallback for missing keys - Add pool-side hashrate polling via pool stats API in xmrig_manager - Introduce Layout::beginFrame() per-frame caching and refresh balance layout config only on schema generation change - Offload daemon output parsing to worker thread - Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
384 lines
13 KiB
C++
384 lines
13 KiB
C++
// 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 <string>
|
|
#include <unordered_map>
|
|
#include <any>
|
|
#include <filesystem>
|
|
|
|
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<const toml::table*>(ptr) after including <toml++/toml.hpp>.
|
|
*/
|
|
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<std::string, StoredElement> elements_;
|
|
|
|
// Parsed DrawElementStyle cache — avoids repeated TOML table iteration.
|
|
// Populated lazily on first drawElement() lookup, cleared on reload.
|
|
mutable std::unordered_map<std::string, DrawElementStyle> styleCache_;
|
|
};
|
|
|
|
// Convenience alias
|
|
inline UISchema& UI() { return UISchema::instance(); }
|
|
|
|
} // namespace schema
|
|
} // namespace ui
|
|
} // namespace dragonx
|