ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency. Built with Dear ImGui, SDL3, and OpenGL3/DX11. Features: - Send/receive shielded and transparent transactions - Autoshield with merged transaction display - Built-in CPU mining (xmrig) - Peer management and network monitoring - Wallet encryption with PIN lock - QR code generation for receive addresses - Transaction history with pagination - Console for direct RPC commands - Cross-platform (Linux, Windows)
This commit is contained in:
852
src/ui/schema/ui_schema.cpp
Normal file
852
src/ui/schema/ui_schema.cpp
Normal file
@@ -0,0 +1,852 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "ui_schema.h"
|
||||
#include "skin_manager.h"
|
||||
#include "color_var_resolver.h"
|
||||
#include "element_styles.h"
|
||||
#include "../material/typography.h"
|
||||
#include "../material/color_theme.h"
|
||||
#include "../theme.h"
|
||||
#include "../theme_loader.h"
|
||||
#include "../effects/imgui_acrylic.h"
|
||||
#include <toml++/toml.hpp>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstdio>
|
||||
#include "../../util/logger.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace schema {
|
||||
|
||||
// ============================================================================
|
||||
// Singleton
|
||||
// ============================================================================
|
||||
|
||||
UISchema& UISchema::instance() {
|
||||
static UISchema s;
|
||||
return s;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Load
|
||||
// ============================================================================
|
||||
|
||||
bool UISchema::loadFromFile(const std::string& path) {
|
||||
toml::table root;
|
||||
try {
|
||||
root = toml::parse_file(path);
|
||||
} catch (const toml::parse_error& e) {
|
||||
DEBUG_LOGF("[UISchema] TOML parse error in %s: %s\n", path.c_str(), e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear previous data
|
||||
elements_.clear();
|
||||
backgroundImagePath_.clear();
|
||||
logoImagePath_.clear();
|
||||
|
||||
// Parse top-level sections
|
||||
if (auto* theme = root["theme"].as_table()) {
|
||||
parseTheme(static_cast<const void*>(theme));
|
||||
}
|
||||
|
||||
if (auto* bp = root["breakpoints"].as_table()) {
|
||||
parseBreakpoints(static_cast<const void*>(bp));
|
||||
}
|
||||
|
||||
if (auto* globals = root["globals"].as_table()) {
|
||||
parseGlobals(static_cast<const void*>(globals));
|
||||
}
|
||||
|
||||
// Parse tabs, dialogs, components sections → store elements
|
||||
if (auto* tabs = root["tabs"].as_table()) {
|
||||
parseSections(static_cast<const void*>(tabs), "tabs");
|
||||
}
|
||||
|
||||
if (auto* dialogs = root["dialogs"].as_table()) {
|
||||
parseSections(static_cast<const void*>(dialogs), "dialogs");
|
||||
}
|
||||
|
||||
if (auto* components = root["components"].as_table()) {
|
||||
parseSections(static_cast<const void*>(components), "components");
|
||||
}
|
||||
|
||||
// Parse flat sections (2-level: sectionName.elementName → {style object})
|
||||
for (const auto& flatSection : {"business", "animations", "console",
|
||||
"backdrop", "shutdown", "notifications", "status-bar",
|
||||
"qr-code", "content-area", "style", "responsive",
|
||||
"spacing", "spacing-tokens", "button", "input", "fonts",
|
||||
"inline-dialogs", "sidebar", "panels", "typography", "effects"}) {
|
||||
if (auto* sec = root[flatSection].as_table()) {
|
||||
parseFlatSection(static_cast<const void*>(sec), flatSection);
|
||||
}
|
||||
}
|
||||
|
||||
currentPath_ = path;
|
||||
if (basePath_.empty()) {
|
||||
basePath_ = path; // Only set on first load (the true base)
|
||||
}
|
||||
overlayPath_.clear(); // No overlay yet
|
||||
loaded_ = true;
|
||||
dirty_ = false;
|
||||
|
||||
// Record initial modification time
|
||||
try {
|
||||
lastModTime_ = std::filesystem::last_write_time(path);
|
||||
if (path == basePath_) {
|
||||
baseModTime_ = lastModTime_;
|
||||
}
|
||||
} catch (const std::filesystem::filesystem_error&) {
|
||||
// Non-fatal
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[UISchema] Loaded: %s (theme: %s, %s, %zu elements, gen=%u)\n",
|
||||
path.c_str(), themeName_.c_str(),
|
||||
darkTheme_ ? "dark" : "light",
|
||||
elements_.size(), generation_);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UISchema::loadFromString(const std::string& tomlStr, const std::string& label) {
|
||||
toml::table root;
|
||||
try {
|
||||
root = toml::parse(tomlStr);
|
||||
} catch (const toml::parse_error& e) {
|
||||
DEBUG_LOGF("[UISchema] TOML parse error (%s): %s\n", label.c_str(), e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store the raw TOML string so the base can be reloaded later
|
||||
// (needed when no file-based basePath_ is available, e.g., Windows embedded)
|
||||
embeddedTomlStr_ = tomlStr;
|
||||
|
||||
// Clear previous data
|
||||
elements_.clear();
|
||||
backgroundImagePath_.clear();
|
||||
logoImagePath_.clear();
|
||||
|
||||
// Parse top-level sections
|
||||
if (auto* theme = root["theme"].as_table()) {
|
||||
parseTheme(static_cast<const void*>(theme));
|
||||
}
|
||||
|
||||
if (auto* bp = root["breakpoints"].as_table()) {
|
||||
parseBreakpoints(static_cast<const void*>(bp));
|
||||
}
|
||||
|
||||
if (auto* globals = root["globals"].as_table()) {
|
||||
parseGlobals(static_cast<const void*>(globals));
|
||||
}
|
||||
|
||||
if (auto* tabs = root["tabs"].as_table()) {
|
||||
parseSections(static_cast<const void*>(tabs), "tabs");
|
||||
}
|
||||
|
||||
if (auto* dialogs = root["dialogs"].as_table()) {
|
||||
parseSections(static_cast<const void*>(dialogs), "dialogs");
|
||||
}
|
||||
|
||||
if (auto* components = root["components"].as_table()) {
|
||||
parseSections(static_cast<const void*>(components), "components");
|
||||
}
|
||||
|
||||
// Parse flat sections (2-level: sectionName.elementName → {style object})
|
||||
for (const auto& flatSection : {"business", "animations", "console",
|
||||
"backdrop", "shutdown", "notifications", "status-bar",
|
||||
"qr-code", "content-area", "style", "responsive",
|
||||
"spacing", "spacing-tokens", "button", "input", "fonts",
|
||||
"inline-dialogs", "sidebar", "panels", "typography", "effects"}) {
|
||||
if (auto* sec = root[flatSection].as_table()) {
|
||||
parseFlatSection(static_cast<const void*>(sec), flatSection);
|
||||
}
|
||||
}
|
||||
|
||||
overlayPath_.clear(); // No overlay when loading a full base
|
||||
currentPath_ = label; // Track what is loaded (for logging)
|
||||
loaded_ = true;
|
||||
dirty_ = false;
|
||||
++generation_;
|
||||
|
||||
DEBUG_LOGF("[UISchema] Loaded from %s (theme: %s, %s, %zu elements)\n",
|
||||
label.c_str(), themeName_.c_str(),
|
||||
darkTheme_ ? "dark" : "light",
|
||||
elements_.size());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Overlay merge (visual-only: theme + backdrop)
|
||||
// ============================================================================
|
||||
|
||||
bool UISchema::mergeOverlayFromFile(const std::string& path) {
|
||||
if (!loaded_) {
|
||||
DEBUG_LOGF("[UISchema] mergeOverlay called before base load — falling back to full load\n");
|
||||
return loadFromFile(path);
|
||||
}
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) {
|
||||
DEBUG_LOGF("[UISchema] Failed to open overlay: %s\n", path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
toml::table root;
|
||||
try {
|
||||
root = toml::parse_file(path);
|
||||
} catch (const toml::parse_error& e) {
|
||||
DEBUG_LOGF("[UISchema] TOML parse error in overlay %s: %s\n", path.c_str(), e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Merge theme section (palette, elevation, images, dark flag, name)
|
||||
if (auto* theme = root["theme"].as_table()) {
|
||||
parseTheme(static_cast<const void*>(theme));
|
||||
}
|
||||
|
||||
// Merge backdrop section (gradient colors, alpha/transparency values)
|
||||
if (auto* sec = root["backdrop"].as_table()) {
|
||||
parseFlatSection(static_cast<const void*>(sec), "backdrop");
|
||||
}
|
||||
|
||||
// Merge effects section (theme visual effects configuration)
|
||||
if (auto* sec = root["effects"].as_table()) {
|
||||
parseFlatSection(static_cast<const void*>(sec), "effects");
|
||||
}
|
||||
|
||||
overlayPath_ = path;
|
||||
// Track overlay file for hot-reload
|
||||
currentPath_ = path;
|
||||
++generation_;
|
||||
try {
|
||||
lastModTime_ = std::filesystem::last_write_time(path);
|
||||
} catch (const std::filesystem::filesystem_error&) {}
|
||||
|
||||
DEBUG_LOGF("[UISchema] Merged overlay: %s (theme: %s, %s)\n",
|
||||
path.c_str(), themeName_.c_str(),
|
||||
darkTheme_ ? "dark" : "light");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Reload base theme (from file or embedded string)
|
||||
// ============================================================================
|
||||
|
||||
bool UISchema::reloadBase() {
|
||||
// Prefer file-based reload when a basePath is available
|
||||
if (!basePath_.empty()) {
|
||||
return loadFromFile(basePath_);
|
||||
}
|
||||
// Fallback: reload from stored embedded string
|
||||
if (!embeddedTomlStr_.empty()) {
|
||||
return loadFromString(embeddedTomlStr_, "embedded-reload");
|
||||
}
|
||||
DEBUG_LOGF("[UISchema] reloadBase: no base path or embedded data available\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Theme parsing
|
||||
// ============================================================================
|
||||
|
||||
void UISchema::parseTheme(const void* dataObj) {
|
||||
const toml::table& t = *static_cast<const toml::table*>(dataObj);
|
||||
|
||||
if (auto name = t["name"].value<std::string>()) {
|
||||
themeName_ = *name;
|
||||
}
|
||||
if (auto dark = t["dark"].value<bool>()) {
|
||||
darkTheme_ = *dark;
|
||||
}
|
||||
|
||||
// Parse palette → ColorVarResolver
|
||||
if (auto* palette = t["palette"].as_table()) {
|
||||
std::unordered_map<std::string, ImU32> paletteMap;
|
||||
for (auto&& [key, val] : *palette) {
|
||||
if (!val.is_string()) continue;
|
||||
ImU32 color = 0;
|
||||
std::string colorStr = *val.value<std::string>();
|
||||
|
||||
// Palette values are direct hex or rgba — not var() references
|
||||
if (ColorVarResolver::parseHex(colorStr, color)) {
|
||||
paletteMap[std::string(key.str())] = color;
|
||||
} else if (ColorVarResolver::parseRgba(colorStr, color)) {
|
||||
paletteMap[std::string(key.str())] = color;
|
||||
} else if (colorStr == "transparent") {
|
||||
paletteMap[std::string(key.str())] = IM_COL32(0, 0, 0, 0);
|
||||
} else {
|
||||
DEBUG_LOGF("[UISchema] Warning: unparseable palette color '%s': %s\n",
|
||||
std::string(key.str()).c_str(), colorStr.c_str());
|
||||
}
|
||||
}
|
||||
colorResolver_.setPalette(paletteMap);
|
||||
}
|
||||
|
||||
// Parse elevation (optional)
|
||||
if (auto* elevation = t["elevation"].as_table()) {
|
||||
// Elevation colors go into the same palette
|
||||
auto paletteMap = colorResolver_.palette();
|
||||
for (auto&& [key, val] : *elevation) {
|
||||
if (!val.is_string()) continue;
|
||||
ImU32 color = 0;
|
||||
std::string colorStr = *val.value<std::string>();
|
||||
if (ColorVarResolver::parseHex(colorStr, color)) {
|
||||
paletteMap[std::string(key.str())] = color;
|
||||
}
|
||||
}
|
||||
colorResolver_.setPalette(paletteMap);
|
||||
}
|
||||
|
||||
// Parse image overrides (resolved relative to theme directory by SkinManager)
|
||||
if (auto* images = t["images"].as_table()) {
|
||||
if (auto bg = (*images)["background_image"].value<std::string>()) {
|
||||
backgroundImagePath_ = *bg;
|
||||
}
|
||||
if (auto logo = (*images)["logo"].value<std::string>()) {
|
||||
logoImagePath_ = *logo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Globals parsing
|
||||
// ============================================================================
|
||||
|
||||
void UISchema::parseGlobals(const void* dataObj) {
|
||||
const toml::table& t = *static_cast<const toml::table*>(dataObj);
|
||||
|
||||
if (auto* btn = t["button"].as_table()) {
|
||||
detail::parseButtonStyle(static_cast<const void*>(btn), globalButton_);
|
||||
}
|
||||
if (auto* inp = t["input"].as_table()) {
|
||||
detail::parseInputStyle(static_cast<const void*>(inp), globalInput_);
|
||||
}
|
||||
if (auto* lbl = t["label"].as_table()) {
|
||||
detail::parseLabelStyle(static_cast<const void*>(lbl), globalLabel_);
|
||||
}
|
||||
if (auto* tbl = t["table"].as_table()) {
|
||||
detail::parseTableStyle(static_cast<const void*>(tbl), globalTable_);
|
||||
}
|
||||
if (auto* cb = t["checkbox"].as_table()) {
|
||||
detail::parseCheckboxStyle(static_cast<const void*>(cb), globalCheckbox_);
|
||||
}
|
||||
if (auto* cmb = t["combo"].as_table()) {
|
||||
detail::parseComboStyle(static_cast<const void*>(cmb), globalCombo_);
|
||||
}
|
||||
if (auto* sld = t["slider"].as_table()) {
|
||||
detail::parseSliderStyle(static_cast<const void*>(sld), globalSlider_);
|
||||
}
|
||||
if (auto* win = t["window"].as_table()) {
|
||||
detail::parseWindowStyle(static_cast<const void*>(win), globalWindow_);
|
||||
}
|
||||
if (auto* sep = t["separator"].as_table()) {
|
||||
detail::parseSeparatorStyle(static_cast<const void*>(sep), globalSeparator_);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Section parsing — store TOML tables for lazy access
|
||||
// ============================================================================
|
||||
|
||||
void UISchema::parseSections(const void* dataObj, const std::string& prefix) {
|
||||
const toml::table& t = *static_cast<const toml::table*>(dataObj);
|
||||
|
||||
for (auto&& [sectionName, sectionNode] : t) {
|
||||
auto* sectionTable = sectionNode.as_table();
|
||||
if (!sectionTable) continue;
|
||||
|
||||
std::string sectionPath = prefix + "." + std::string(sectionName.str());
|
||||
|
||||
for (auto&& [elemName, elemNode] : *sectionTable) {
|
||||
auto* elemTable = elemNode.as_table();
|
||||
if (!elemTable) continue;
|
||||
|
||||
std::string key = sectionPath + "." + std::string(elemName.str());
|
||||
elements_[key] = StoredElement{ toml::table(*elemTable) };
|
||||
|
||||
// Recurse into nested sub-sections (3rd level)
|
||||
// e.g., tabs.balance.classic.logo-opacity → stored as
|
||||
// "tabs.balance.classic.logo-opacity"
|
||||
for (auto&& [innerName, innerNode] : *elemTable) {
|
||||
if (auto* innerTable = innerNode.as_table()) {
|
||||
std::string innerKey = key + "." + std::string(innerName.str());
|
||||
elements_[innerKey] = StoredElement{ toml::table(*innerTable) };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UISchema::parseFlatSection(const void* dataObj, const std::string& prefix) {
|
||||
const toml::table& t = *static_cast<const toml::table*>(dataObj);
|
||||
|
||||
for (auto&& [elemName, elemNode] : t) {
|
||||
std::string key = prefix + "." + std::string(elemName.str());
|
||||
|
||||
if (auto* elemTable = elemNode.as_table()) {
|
||||
// Check if this is a leaf element (values are primitives)
|
||||
// or a nested sub-section (values are objects)
|
||||
bool hasNestedObjects = false;
|
||||
for (auto&& [innerName, innerNode] : *elemTable) {
|
||||
if (innerNode.is_table()) {
|
||||
hasNestedObjects = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNestedObjects) {
|
||||
// Nested sub-section (e.g., inline-dialogs.about.{width,height,...})
|
||||
// Store each inner object as prefix.elemName.innerName
|
||||
for (auto&& [innerName, innerNode] : *elemTable) {
|
||||
if (auto* innerTable = innerNode.as_table()) {
|
||||
std::string innerKey = key + "." + std::string(innerName.str());
|
||||
elements_[innerKey] = StoredElement{ toml::table(*innerTable) };
|
||||
}
|
||||
}
|
||||
// Also store the sub-section itself as a flat element
|
||||
elements_[key] = StoredElement{ toml::table(*elemTable) };
|
||||
} else {
|
||||
// Leaf element (e.g., business.block-reward: {"size": 1.5625})
|
||||
elements_[key] = StoredElement{ toml::table(*elemTable) };
|
||||
}
|
||||
} else if (elemNode.is_integer() || elemNode.is_floating_point()) {
|
||||
// Auto-wrap scalar number → {size = value}
|
||||
toml::table wrapped;
|
||||
wrapped.insert("size", elemNode.value<double>().value_or(0.0));
|
||||
elements_[key] = StoredElement{ std::move(wrapped) };
|
||||
} else if (elemNode.is_string()) {
|
||||
// Auto-wrap string → {font = value} or {color = value}
|
||||
std::string s = *elemNode.value<std::string>();
|
||||
toml::table wrapped;
|
||||
if (s.find("var(") == 0 || s.find("#") == 0 || s.find("rgba") == 0) {
|
||||
wrapped.insert("color", s);
|
||||
} else {
|
||||
wrapped.insert("font", s);
|
||||
}
|
||||
elements_[key] = StoredElement{ std::move(wrapped) };
|
||||
} else if (auto* arr = elemNode.as_array(); arr && arr->size() >= 2) {
|
||||
// Auto-wrap [x, y] array → {width = x, height = y}
|
||||
toml::table wrapped;
|
||||
wrapped.insert("width", (*arr)[0].value<double>().value_or(0.0));
|
||||
wrapped.insert("height", (*arr)[1].value<double>().value_or(0.0));
|
||||
elements_[key] = StoredElement{ std::move(wrapped) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Breakpoint parsing
|
||||
// ============================================================================
|
||||
|
||||
void UISchema::parseBreakpoints(const void* jsonObj) {
|
||||
detail::parseBreakpointConfig(jsonObj, breakpoints_);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Element lookup — find stored TOML table by key
|
||||
// ============================================================================
|
||||
|
||||
const void* UISchema::findElement(const std::string& section, const std::string& name) const {
|
||||
std::string key = section + "." + name;
|
||||
auto it = elements_.find(key);
|
||||
if (it == elements_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
// Return pointer to the stored toml::table via std::any_cast
|
||||
return std::any_cast<toml::table>(&it->second.data);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hot-reload
|
||||
// ============================================================================
|
||||
|
||||
void UISchema::pollForChanges() {
|
||||
if (!loaded_ || currentPath_.empty()) return;
|
||||
|
||||
double now = ImGui::GetTime();
|
||||
if (now - lastPollTime_ < pollInterval_) return;
|
||||
lastPollTime_ = now;
|
||||
|
||||
try {
|
||||
auto mtime = std::filesystem::last_write_time(currentPath_);
|
||||
if (mtime != lastModTime_) {
|
||||
lastModTime_ = mtime;
|
||||
dirty_ = true;
|
||||
}
|
||||
// Also check base file when an overlay is active
|
||||
if (!dirty_ && !overlayPath_.empty() && !basePath_.empty() && basePath_ != currentPath_) {
|
||||
auto btime = std::filesystem::last_write_time(basePath_);
|
||||
if (btime != baseModTime_) {
|
||||
baseModTime_ = btime;
|
||||
dirty_ = true;
|
||||
}
|
||||
}
|
||||
} catch (const std::filesystem::filesystem_error&) {
|
||||
// File might be mid-write — ignore
|
||||
}
|
||||
}
|
||||
|
||||
void UISchema::applyIfDirty() {
|
||||
if (!dirty_) return;
|
||||
dirty_ = false;
|
||||
|
||||
// Snapshot font sizes before reload for change detection
|
||||
static const char* fontKeys[] = {
|
||||
"h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"subtitle1", "subtitle2", "body1", "body2",
|
||||
"button", "button-sm", "button-lg",
|
||||
"caption", "overline", "scale"
|
||||
};
|
||||
float prevFonts[16];
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
prevFonts[i] = drawElement("fonts", fontKeys[i]).size;
|
||||
}
|
||||
|
||||
// Snapshot image paths before reload for change detection
|
||||
std::string prevBgImage = backgroundImagePath_;
|
||||
std::string prevLogoImage = logoImagePath_;
|
||||
|
||||
DEBUG_LOGF("[UISchema] Hot-reload: re-parsing %s\n", currentPath_.c_str());
|
||||
|
||||
// If an overlay is active, reload base first then re-merge overlay
|
||||
if (!overlayPath_.empty() && !basePath_.empty()) {
|
||||
loadFromFile(basePath_);
|
||||
mergeOverlayFromFile(overlayPath_);
|
||||
} else {
|
||||
loadFromFile(currentPath_);
|
||||
}
|
||||
|
||||
// Detect font size changes
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
float cur = drawElement("fonts", fontKeys[i]).size;
|
||||
if (cur != prevFonts[i]) {
|
||||
fonts_changed_ = true;
|
||||
DEBUG_LOGF("[UISchema] Font sizes changed, atlas rebuild needed\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-apply Material Design colors to ImGui from the reloaded palette
|
||||
reapplyColorsToImGui();
|
||||
|
||||
// Detect image path changes and update SkinManager + trigger reload
|
||||
if (backgroundImagePath_ != prevBgImage || logoImagePath_ != prevLogoImage) {
|
||||
DEBUG_LOGF("[UISchema] Hot-reload: image paths changed (bg: '%s' → '%s', logo: '%s' → '%s')\n",
|
||||
prevBgImage.c_str(), backgroundImagePath_.c_str(),
|
||||
prevLogoImage.c_str(), logoImagePath_.c_str());
|
||||
|
||||
auto& skinMgr = SkinManager::instance();
|
||||
std::string activeId = skinMgr.activeSkinId();
|
||||
if (!activeId.empty()) {
|
||||
// Re-resolve image paths from the new filenames
|
||||
skinMgr.resolveAndReloadImages(activeId, currentPath_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Color resolution
|
||||
// ============================================================================
|
||||
|
||||
ImU32 UISchema::resolveColor(const std::string& ref, ImU32 fallback) const {
|
||||
return colorResolver_.resolve(ref, fallback);
|
||||
}
|
||||
|
||||
void UISchema::reapplyColorsToImGui() {
|
||||
// Build a ColorTheme from the current palette
|
||||
const auto& pal = colorResolver_.palette();
|
||||
|
||||
auto get = [&](const std::string& key, ImU32 fallback = 0) -> ImU32 {
|
||||
auto it = pal.find(key);
|
||||
return it != pal.end() ? it->second : fallback;
|
||||
};
|
||||
|
||||
material::ColorTheme theme{};
|
||||
theme.primary = get("--primary");
|
||||
theme.primaryVariant = get("--primary-variant");
|
||||
theme.primaryLight = get("--primary-light");
|
||||
theme.secondary = get("--secondary");
|
||||
theme.secondaryVariant = get("--secondary-variant");
|
||||
theme.secondaryLight = get("--secondary-light");
|
||||
theme.background = get("--background");
|
||||
theme.surface = get("--surface");
|
||||
theme.surfaceVariant = get("--surface-variant");
|
||||
theme.onPrimary = get("--on-primary");
|
||||
theme.onSecondary = get("--on-secondary");
|
||||
theme.onBackground = get("--on-background");
|
||||
theme.onSurface = get("--on-surface");
|
||||
theme.onSurfaceMedium = get("--on-surface-medium");
|
||||
theme.onSurfaceDisabled= get("--on-surface-disabled");
|
||||
theme.error = get("--error");
|
||||
theme.onError = get("--on-error");
|
||||
theme.success = get("--success");
|
||||
theme.onSuccess = get("--on-success");
|
||||
theme.warning = get("--warning");
|
||||
theme.onWarning = get("--on-warning");
|
||||
theme.divider = get("--divider");
|
||||
theme.outline = get("--outline");
|
||||
theme.scrim = get("--scrim");
|
||||
|
||||
// Fill missing fields with defaults
|
||||
ThemeLoader::computeDefaults(theme, darkTheme_);
|
||||
|
||||
// Apply to ImGui and update acrylic theme
|
||||
material::ApplyColorThemeToImGui(theme);
|
||||
SetCurrentAcrylicTheme(ThemeLoader::deriveAcrylicTheme(theme));
|
||||
|
||||
// Background colors changed — re-capture on next frame
|
||||
effects::ImGuiAcrylic::InvalidateCapture();
|
||||
|
||||
DEBUG_LOGF("[UISchema] Hot-reload: re-applied colors to ImGui\n");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Font resolution
|
||||
// ============================================================================
|
||||
|
||||
ImFont* UISchema::resolveFont(const std::string& fontName) const {
|
||||
if (fontName.empty()) return nullptr;
|
||||
|
||||
auto& typo = material::Typography::instance();
|
||||
if (!typo.isLoaded()) return nullptr;
|
||||
|
||||
// Match font name strings to Typography accessors
|
||||
if (fontName == "h1") return typo.h1();
|
||||
if (fontName == "h2") return typo.h2();
|
||||
if (fontName == "h3") return typo.h3();
|
||||
if (fontName == "h4") return typo.h4();
|
||||
if (fontName == "h5") return typo.h5();
|
||||
if (fontName == "h6") return typo.h6();
|
||||
if (fontName == "subtitle1") return typo.subtitle1();
|
||||
if (fontName == "subtitle2") return typo.subtitle2();
|
||||
if (fontName == "body1") return typo.body1();
|
||||
if (fontName == "body2") return typo.body2();
|
||||
if (fontName == "button") return typo.button();
|
||||
if (fontName == "button-sm") return typo.buttonSm();
|
||||
if (fontName == "button-lg") return typo.buttonLg();
|
||||
if (fontName == "caption") return typo.caption();
|
||||
if (fontName == "overline") return typo.overline();
|
||||
|
||||
DEBUG_LOGF("[UISchema] Warning: unknown font name '%s'\n", fontName.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Responsive breakpoint
|
||||
// ============================================================================
|
||||
|
||||
Breakpoint UISchema::currentBreakpoint() const {
|
||||
ImVec2 size = ImGui::GetMainViewport()->Size;
|
||||
|
||||
// Check compact (must satisfy ALL specified constraints)
|
||||
const auto& c = breakpoints_.compact;
|
||||
bool isCompact = false;
|
||||
if (c.maxWidth > 0 || c.maxHeight > 0) {
|
||||
isCompact = true;
|
||||
if (c.maxWidth > 0 && size.x > c.maxWidth) isCompact = false;
|
||||
if (c.maxHeight > 0 && size.y > c.maxHeight) isCompact = false;
|
||||
}
|
||||
if (isCompact) return Breakpoint::Compact;
|
||||
|
||||
// Check expanded
|
||||
const auto& e = breakpoints_.expanded;
|
||||
bool isExpanded = false;
|
||||
if (e.minWidth > 0 || e.minHeight > 0) {
|
||||
isExpanded = true;
|
||||
if (e.minWidth > 0 && size.x < e.minWidth) isExpanded = false;
|
||||
if (e.minHeight > 0 && size.y < e.minHeight) isExpanded = false;
|
||||
}
|
||||
if (isExpanded) return Breakpoint::Expanded;
|
||||
|
||||
return Breakpoint::Normal;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Element lookups with merge
|
||||
// ============================================================================
|
||||
|
||||
ButtonStyle UISchema::button(const std::string& section, const std::string& name) const {
|
||||
// Start with global defaults
|
||||
ButtonStyle result = globalButton_;
|
||||
|
||||
// Overlay section-specific values
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
ButtonStyle sectionStyle;
|
||||
detail::parseButtonStyle(elem, sectionStyle);
|
||||
mergeButton(result, sectionStyle);
|
||||
|
||||
// Check for responsive overrides
|
||||
const toml::table& t = *static_cast<const toml::table*>(elem);
|
||||
Breakpoint bp = currentBreakpoint();
|
||||
if (bp == Breakpoint::Compact) {
|
||||
if (auto* compactTable = t["@compact"].as_table()) {
|
||||
ResponsiveButtonOverride ovr;
|
||||
detail::parseResponsiveButtonOverride(static_cast<const void*>(compactTable), ovr);
|
||||
applyResponsiveButton(result, ovr);
|
||||
}
|
||||
} else if (bp == Breakpoint::Expanded) {
|
||||
if (auto* expandedTable = t["@expanded"].as_table()) {
|
||||
ResponsiveButtonOverride ovr;
|
||||
detail::parseResponsiveButtonOverride(static_cast<const void*>(expandedTable), ovr);
|
||||
applyResponsiveButton(result, ovr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
InputStyle UISchema::input(const std::string& section, const std::string& name) const {
|
||||
InputStyle result = globalInput_;
|
||||
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
InputStyle sectionStyle;
|
||||
detail::parseInputStyle(elem, sectionStyle);
|
||||
mergeInput(result, sectionStyle);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
LabelStyle UISchema::label(const std::string& section, const std::string& name) const {
|
||||
LabelStyle result = globalLabel_;
|
||||
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
LabelStyle sectionStyle;
|
||||
detail::parseLabelStyle(elem, sectionStyle);
|
||||
mergeLabel(result, sectionStyle);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
TableStyle UISchema::table(const std::string& section, const std::string& name) const {
|
||||
TableStyle result = globalTable_;
|
||||
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
TableStyle sectionStyle;
|
||||
detail::parseTableStyle(elem, sectionStyle);
|
||||
|
||||
// Merge table fields
|
||||
if (sectionStyle.minHeight >= 0) result.minHeight = sectionStyle.minHeight;
|
||||
if (sectionStyle.heightRatio >= 0) result.heightRatio = sectionStyle.heightRatio;
|
||||
if (sectionStyle.bottomReserve >= 0) result.bottomReserve = sectionStyle.bottomReserve;
|
||||
if (sectionStyle.rowHeight >= 0) result.rowHeight = sectionStyle.rowHeight;
|
||||
if (!sectionStyle.headerFont.empty()) result.headerFont = sectionStyle.headerFont;
|
||||
if (!sectionStyle.cellFont.empty()) result.cellFont = sectionStyle.cellFont;
|
||||
if (!sectionStyle.borderColor.empty()) result.borderColor = sectionStyle.borderColor;
|
||||
if (!sectionStyle.stripeColor.empty()) result.stripeColor = sectionStyle.stripeColor;
|
||||
// Columns: section columns override/extend global columns
|
||||
for (auto& [colName, colStyle] : sectionStyle.columns) {
|
||||
result.columns[colName] = colStyle;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
CheckboxStyle UISchema::checkbox(const std::string& section, const std::string& name) const {
|
||||
CheckboxStyle result = globalCheckbox_;
|
||||
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
CheckboxStyle sectionStyle;
|
||||
detail::parseCheckboxStyle(elem, sectionStyle);
|
||||
if (!sectionStyle.font.empty()) result.font = sectionStyle.font;
|
||||
if (!sectionStyle.color.empty()) result.color = sectionStyle.color;
|
||||
if (!sectionStyle.checkColor.empty()) result.checkColor = sectionStyle.checkColor;
|
||||
if (!sectionStyle.background.empty()) result.background = sectionStyle.background;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ComboStyle UISchema::combo(const std::string& section, const std::string& name) const {
|
||||
ComboStyle result = globalCombo_;
|
||||
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
ComboStyle sectionStyle;
|
||||
detail::parseComboStyle(elem, sectionStyle);
|
||||
if (sectionStyle.width > 0) result.width = sectionStyle.width;
|
||||
if (!sectionStyle.font.empty()) result.font = sectionStyle.font;
|
||||
if (!sectionStyle.color.empty()) result.color = sectionStyle.color;
|
||||
if (!sectionStyle.background.empty()) result.background = sectionStyle.background;
|
||||
if (sectionStyle.borderRadius >= 0) result.borderRadius = sectionStyle.borderRadius;
|
||||
if (sectionStyle.truncate > 0) result.truncate = sectionStyle.truncate;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
SliderStyle UISchema::slider(const std::string& section, const std::string& name) const {
|
||||
SliderStyle result = globalSlider_;
|
||||
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
SliderStyle sectionStyle;
|
||||
detail::parseSliderStyle(elem, sectionStyle);
|
||||
if (sectionStyle.width > 0) result.width = sectionStyle.width;
|
||||
if (!sectionStyle.trackColor.empty()) result.trackColor = sectionStyle.trackColor;
|
||||
if (!sectionStyle.fillColor.empty()) result.fillColor = sectionStyle.fillColor;
|
||||
if (!sectionStyle.thumbColor.empty()) result.thumbColor = sectionStyle.thumbColor;
|
||||
if (sectionStyle.thumbRadius >= 0) result.thumbRadius = sectionStyle.thumbRadius;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
WindowStyle UISchema::window(const std::string& section, const std::string& name) const {
|
||||
WindowStyle result = globalWindow_;
|
||||
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
WindowStyle sectionStyle;
|
||||
detail::parseWindowStyle(elem, sectionStyle);
|
||||
mergeWindow(result, sectionStyle);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
SeparatorStyle UISchema::separator(const std::string& section, const std::string& name) const {
|
||||
SeparatorStyle result = globalSeparator_;
|
||||
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
SeparatorStyle sectionStyle;
|
||||
detail::parseSeparatorStyle(elem, sectionStyle);
|
||||
if (!sectionStyle.color.empty()) result.color = sectionStyle.color;
|
||||
if (sectionStyle.thickness >= 0) result.thickness = sectionStyle.thickness;
|
||||
if (sectionStyle.margin[0] > 0 || sectionStyle.margin[1] > 0) {
|
||||
result.margin[0] = sectionStyle.margin[0];
|
||||
result.margin[1] = sectionStyle.margin[1];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
DrawElementStyle UISchema::drawElement(const std::string& section, const std::string& name) const {
|
||||
DrawElementStyle result;
|
||||
|
||||
const void* elem = findElement(section, name);
|
||||
if (elem) {
|
||||
detail::parseDrawElementStyle(elem, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace schema
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user