Explorer tab:
- New block explorer tab with search, chain stats, mempool info,
recent blocks table, block detail modal with tx expansion
- Sidebar nav entry, i18n strings, ui.toml layout values
Bootstrap fixes:
- Move wizard Done handler into render() — was dead code, preventing
startEmbeddedDaemon() and tryConnect() from firing post-wizard
- Stop deleting BDB database/ dir during cleanup — caused LSN mismatch
that salvaged wallet.dat into wallet.{timestamp}.bak
- Add banlist.dat, db.log, .lock to cleanup file list
- Fatal extraction failure for blocks/ and chainstate/ files
- Verification progress: split SHA-256 (0-50%) and MD5 (50-100%)
Theme system:
- Expand overlay merge to apply ALL sections (tabs, dialogs, components,
screens, flat sections), not just theme+backdrop+effects
- Add screens and security section parsing to UISchema
- Build-time theme expansion via expand_themes.py (CMake + build.sh)
Other:
- Version bump to 1.1.0
- WalletState::clear() resets all fields (sync, daemon info, etc.)
- Sidebar item-height 42 → 36
916 lines
34 KiB
C++
916 lines
34 KiB
C++
// 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();
|
|
styleCache_.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 screens as a 3-level section (screens.loading, screens.first-run, etc.)
|
|
if (auto* screens = root["screens"].as_table()) {
|
|
parseSections(static_cast<const void*>(screens), "screens");
|
|
}
|
|
|
|
// 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", "security"}) {
|
|
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;
|
|
++generation_;
|
|
|
|
// 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();
|
|
styleCache_.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 screens as a 3-level section (screens.loading, screens.first-run, etc.)
|
|
if (auto* screens = root["screens"].as_table()) {
|
|
parseSections(static_cast<const void*>(screens), "screens");
|
|
}
|
|
|
|
// 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", "security"}) {
|
|
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 — all sections (theme + layout + effects)
|
|
// ============================================================================
|
|
|
|
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 breakpoints + globals
|
|
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));
|
|
}
|
|
|
|
// Merge tabs, dialogs, components (3-level sections)
|
|
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");
|
|
}
|
|
|
|
// Merge screens (3-level section)
|
|
if (auto* screens = root["screens"].as_table()) {
|
|
parseSections(static_cast<const void*>(screens), "screens");
|
|
}
|
|
|
|
// Merge all flat sections (2-level)
|
|
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", "security"}) {
|
|
if (auto* sec = root[flatSection].as_table()) {
|
|
parseFlatSection(static_cast<const void*>(sec), flatSection);
|
|
}
|
|
}
|
|
|
|
overlayPath_ = path;
|
|
// Track overlay file for hot-reload
|
|
currentPath_ = path;
|
|
++generation_;
|
|
styleCache_.clear(); // Invalidate after overlay merges new elements
|
|
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;
|
|
|
|
// Clear style cache before snapshot so we re-read from TOML
|
|
styleCache_.clear();
|
|
|
|
// 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());
|
|
|
|
// Save overlay path before reloading — loadFromFile/loadFromString clear it
|
|
std::string savedOverlay = overlayPath_;
|
|
|
|
// If an overlay is active, reload base first then re-merge overlay
|
|
if (!savedOverlay.empty() && !basePath_.empty()) {
|
|
loadFromFile(basePath_);
|
|
mergeOverlayFromFile(savedOverlay);
|
|
} else if (!savedOverlay.empty() && !embeddedTomlStr_.empty()) {
|
|
// Embedded base (e.g. Windows single-file): reload from stored string
|
|
loadFromString(embeddedTomlStr_, "embedded-reload");
|
|
mergeOverlayFromFile(savedOverlay);
|
|
} 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;
|
|
}
|
|
|
|
const DrawElementStyle& UISchema::drawElement(const std::string& section, const std::string& name) const {
|
|
static const DrawElementStyle s_empty{};
|
|
|
|
std::string key = section + "." + name;
|
|
|
|
// Return from cache if already parsed
|
|
auto cit = styleCache_.find(key);
|
|
if (cit != styleCache_.end()) {
|
|
return cit->second;
|
|
}
|
|
|
|
// Parse from TOML and cache
|
|
const void* elem = findElement(section, name);
|
|
if (!elem) {
|
|
return s_empty;
|
|
}
|
|
|
|
auto [it, _] = styleCache_.emplace(std::move(key), DrawElementStyle{});
|
|
detail::parseDrawElementStyle(elem, it->second);
|
|
return it->second;
|
|
}
|
|
|
|
} // namespace schema
|
|
} // namespace ui
|
|
} // namespace dragonx
|