// 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 #include #include #include #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(theme)); } if (auto* bp = root["breakpoints"].as_table()) { parseBreakpoints(static_cast(bp)); } if (auto* globals = root["globals"].as_table()) { parseGlobals(static_cast(globals)); } // Parse tabs, dialogs, components sections → store elements if (auto* tabs = root["tabs"].as_table()) { parseSections(static_cast(tabs), "tabs"); } if (auto* dialogs = root["dialogs"].as_table()) { parseSections(static_cast(dialogs), "dialogs"); } if (auto* components = root["components"].as_table()) { parseSections(static_cast(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(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(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(theme)); } if (auto* bp = root["breakpoints"].as_table()) { parseBreakpoints(static_cast(bp)); } if (auto* globals = root["globals"].as_table()) { parseGlobals(static_cast(globals)); } if (auto* tabs = root["tabs"].as_table()) { parseSections(static_cast(tabs), "tabs"); } if (auto* dialogs = root["dialogs"].as_table()) { parseSections(static_cast(dialogs), "dialogs"); } if (auto* components = root["components"].as_table()) { parseSections(static_cast(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(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(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(theme)); } // Merge breakpoints + globals if (auto* bp = root["breakpoints"].as_table()) { parseBreakpoints(static_cast(bp)); } if (auto* globals = root["globals"].as_table()) { parseGlobals(static_cast(globals)); } // Merge tabs, dialogs, components (3-level sections) if (auto* tabs = root["tabs"].as_table()) { parseSections(static_cast(tabs), "tabs"); } if (auto* dialogs = root["dialogs"].as_table()) { parseSections(static_cast(dialogs), "dialogs"); } if (auto* components = root["components"].as_table()) { parseSections(static_cast(components), "components"); } // Merge screens (3-level section) if (auto* screens = root["screens"].as_table()) { parseSections(static_cast(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(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(dataObj); if (auto name = t["name"].value()) { themeName_ = *name; } if (auto dark = t["dark"].value()) { darkTheme_ = *dark; } // Parse palette → ColorVarResolver if (auto* palette = t["palette"].as_table()) { std::unordered_map paletteMap; for (auto&& [key, val] : *palette) { if (!val.is_string()) continue; ImU32 color = 0; std::string colorStr = *val.value(); // 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(); 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()) { backgroundImagePath_ = *bg; } if (auto logo = (*images)["logo"].value()) { logoImagePath_ = *logo; } } } // ============================================================================ // Globals parsing // ============================================================================ void UISchema::parseGlobals(const void* dataObj) { const toml::table& t = *static_cast(dataObj); if (auto* btn = t["button"].as_table()) { detail::parseButtonStyle(static_cast(btn), globalButton_); } if (auto* inp = t["input"].as_table()) { detail::parseInputStyle(static_cast(inp), globalInput_); } if (auto* lbl = t["label"].as_table()) { detail::parseLabelStyle(static_cast(lbl), globalLabel_); } if (auto* tbl = t["table"].as_table()) { detail::parseTableStyle(static_cast(tbl), globalTable_); } if (auto* cb = t["checkbox"].as_table()) { detail::parseCheckboxStyle(static_cast(cb), globalCheckbox_); } if (auto* cmb = t["combo"].as_table()) { detail::parseComboStyle(static_cast(cmb), globalCombo_); } if (auto* sld = t["slider"].as_table()) { detail::parseSliderStyle(static_cast(sld), globalSlider_); } if (auto* win = t["window"].as_table()) { detail::parseWindowStyle(static_cast(win), globalWindow_); } if (auto* sep = t["separator"].as_table()) { detail::parseSeparatorStyle(static_cast(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(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(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().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(); 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().value_or(0.0)); wrapped.insert("height", (*arr)[1].value().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(&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(elem); Breakpoint bp = currentBreakpoint(); if (bp == Breakpoint::Compact) { if (auto* compactTable = t["@compact"].as_table()) { ResponsiveButtonOverride ovr; detail::parseResponsiveButtonOverride(static_cast(compactTable), ovr); applyResponsiveButton(result, ovr); } } else if (bp == Breakpoint::Expanded) { if (auto* expandedTable = t["@expanded"].as_table()) { ResponsiveButtonOverride ovr; detail::parseResponsiveButtonOverride(static_cast(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