Files
ObsidianDragon/src/ui/schema/ui_schema.cpp
dan_s 9e94952e0a v1.1.0: explorer tab, bootstrap fixes, full theme overlay merge
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
2026-03-17 18:49:46 -05:00

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