ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency. Built with Dear ImGui, SDL3, and OpenGL3/DX11. Features: - Send/receive shielded and transparent transactions - Autoshield with merged transaction display - Built-in CPU mining (xmrig) - Peer management and network monitoring - Wallet encryption with PIN lock - QR code generation for receive addresses - Transaction history with pagination - Console for direct RPC commands - Cross-platform (Linux, Windows)
This commit is contained in:
800
src/ui/schema/skin_manager.cpp
Normal file
800
src/ui/schema/skin_manager.cpp
Normal file
@@ -0,0 +1,800 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "skin_manager.h"
|
||||
#include "ui_schema.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../resources/embedded_resources.h"
|
||||
#include "../theme.h"
|
||||
#include "../material/color_theme.h"
|
||||
#include "../effects/theme_effects.h"
|
||||
#include "../effects/imgui_acrylic.h"
|
||||
|
||||
#include <toml++/toml.hpp>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include "../../util/logger.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
namespace schema {
|
||||
|
||||
// ============================================================================
|
||||
// Singleton
|
||||
// ============================================================================
|
||||
|
||||
SkinManager& SkinManager::instance() {
|
||||
static SkinManager s;
|
||||
return s;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Directory helpers
|
||||
// ============================================================================
|
||||
|
||||
std::string SkinManager::getBundledSkinsDirectory() {
|
||||
// Bundled skins live in res/themes/ next to the executable
|
||||
fs::path exe_dir = util::getExecutableDirectory();
|
||||
fs::path themes_dir = exe_dir / "res" / "themes";
|
||||
|
||||
if (fs::exists(themes_dir)) {
|
||||
return themes_dir.string();
|
||||
}
|
||||
|
||||
// Fallback: current working directory
|
||||
themes_dir = fs::current_path() / "res" / "themes";
|
||||
if (fs::exists(themes_dir)) {
|
||||
return themes_dir.string();
|
||||
}
|
||||
|
||||
// No on-disk themes dir found (single-file Windows distribution).
|
||||
// Extract embedded overlay themes to the config directory.
|
||||
fs::path configDir = util::Platform::getObsidianDragonDir();
|
||||
fs::path extractedDir = configDir / "bundled-themes";
|
||||
int extracted = resources::extractBundledThemes(extractedDir.string());
|
||||
if (extracted > 0) {
|
||||
DEBUG_LOGF("[SkinManager] Extracted %d embedded bundled themes to %s\n",
|
||||
extracted, extractedDir.string().c_str());
|
||||
}
|
||||
if (fs::exists(extractedDir)) {
|
||||
return extractedDir.string();
|
||||
}
|
||||
|
||||
return (exe_dir / "res" / "themes").string();
|
||||
}
|
||||
|
||||
std::string SkinManager::getUserSkinsDirectory() {
|
||||
// User themes in ObsidianDragon config directory — folder-based
|
||||
fs::path configDir = util::Platform::getObsidianDragonDir();
|
||||
return (configDir / "themes").string();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scan for skins
|
||||
// ============================================================================
|
||||
|
||||
void SkinManager::scanDirectory(const std::string& dir, bool bundled) {
|
||||
if (!fs::exists(dir) || !fs::is_directory(dir)) {
|
||||
DEBUG_LOGF("[SkinManager] Directory does not exist: %s\n", dir.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[SkinManager] Scanning %s directory: %s\n", bundled ? "bundled" : "user", dir.c_str());
|
||||
|
||||
if (bundled) {
|
||||
// Bundled skins: all .toml files in res/themes/ except ui.toml
|
||||
// (ui.toml is the base theme and is always loaded as "dragonx")
|
||||
for (const auto& entry : fs::directory_iterator(dir)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
|
||||
fs::path p = entry.path();
|
||||
if (p.extension() != ".toml") continue;
|
||||
|
||||
std::string stem = p.stem().string();
|
||||
|
||||
// Skip ui.toml - it's the base theme handled separately as "dragonx"
|
||||
if (stem == "ui") continue;
|
||||
|
||||
// Try to parse and extract metadata
|
||||
toml::table root;
|
||||
try {
|
||||
root = toml::parse_file(p.string());
|
||||
} catch (...) {
|
||||
DEBUG_LOGF("[SkinManager] Skipping '%s': invalid TOML\n", p.filename().string().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* theme = root["theme"].as_table();
|
||||
if (!theme) {
|
||||
DEBUG_LOGF("[SkinManager] Skipping '%s': no [theme] section\n", p.filename().string().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
SkinInfo info;
|
||||
info.path = p.string();
|
||||
info.bundled = true;
|
||||
|
||||
// ID = filename stem (e.g. "dark" from dark.toml)
|
||||
info.id = stem;
|
||||
|
||||
if (auto name = (*theme)["name"].value<std::string>()) {
|
||||
info.name = *name;
|
||||
} else {
|
||||
info.name = info.id;
|
||||
}
|
||||
|
||||
if (auto author = (*theme)["author"].value<std::string>()) {
|
||||
info.author = *author;
|
||||
}
|
||||
|
||||
if (auto dark = (*theme)["dark"].value<bool>()) {
|
||||
info.dark = *dark;
|
||||
}
|
||||
|
||||
// Resolve image paths from theme.images (bundled: res/img/)
|
||||
fs::path imgDir = p.parent_path().parent_path() / "img";
|
||||
std::string bgFilename;
|
||||
std::string logoFilename;
|
||||
|
||||
if (auto* images = (*theme)["images"].as_table()) {
|
||||
if (auto bg = (*images)["background_image"].value<std::string>()) {
|
||||
bgFilename = *bg;
|
||||
}
|
||||
if (auto logo = (*images)["logo"].value<std::string>()) {
|
||||
logoFilename = *logo;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bgFilename.empty()) {
|
||||
fs::path bgPath = imgDir / bgFilename;
|
||||
if (fs::exists(bgPath)) {
|
||||
info.backgroundImagePath = bgPath.string();
|
||||
}
|
||||
}
|
||||
|
||||
if (!logoFilename.empty()) {
|
||||
fs::path logoImgPath = imgDir / logoFilename;
|
||||
if (fs::exists(logoImgPath)) {
|
||||
info.logoPath = logoImgPath.string();
|
||||
}
|
||||
}
|
||||
|
||||
skins_.push_back(std::move(info));
|
||||
}
|
||||
} else {
|
||||
// User themes: each subfolder must contain a theme.toml
|
||||
for (const auto& entry : fs::directory_iterator(dir)) {
|
||||
if (!entry.is_directory()) continue;
|
||||
|
||||
fs::path themeDir = entry.path();
|
||||
fs::path themeToml = themeDir / "theme.toml";
|
||||
|
||||
if (!fs::exists(themeToml)) {
|
||||
DEBUG_LOGF("[SkinManager] Skipping folder '%s': no theme.toml found\n",
|
||||
themeDir.filename().string().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[SkinManager] Found theme folder: %s (theme.toml exists)\n", themeDir.filename().string().c_str());
|
||||
|
||||
// Validate the theme file
|
||||
auto validation = validateSkinFile(themeToml.string());
|
||||
|
||||
// Parse metadata even from invalid themes (so they show in the list)
|
||||
toml::table root;
|
||||
try {
|
||||
root = toml::parse_file(themeToml.string());
|
||||
} catch (...) {
|
||||
// Still add as invalid
|
||||
SkinInfo info;
|
||||
info.id = themeDir.filename().string();
|
||||
info.name = info.id;
|
||||
info.path = themeToml.string();
|
||||
info.directory = themeDir.string();
|
||||
info.bundled = false;
|
||||
info.valid = false;
|
||||
info.validationError = "Invalid TOML";
|
||||
skins_.push_back(std::move(info));
|
||||
continue;
|
||||
}
|
||||
|
||||
SkinInfo info;
|
||||
info.id = themeDir.filename().string();
|
||||
info.path = themeToml.string();
|
||||
info.directory = themeDir.string();
|
||||
info.bundled = false;
|
||||
info.valid = validation.valid;
|
||||
info.validationError = validation.error;
|
||||
|
||||
// Extract metadata from theme section
|
||||
if (auto* theme = root["theme"].as_table()) {
|
||||
|
||||
if (auto name = (*theme)["name"].value<std::string>()) {
|
||||
info.name = *name;
|
||||
} else {
|
||||
info.name = info.id;
|
||||
}
|
||||
|
||||
if (auto author = (*theme)["author"].value<std::string>()) {
|
||||
info.author = *author;
|
||||
}
|
||||
|
||||
if (auto dark = (*theme)["dark"].value<bool>()) {
|
||||
info.dark = *dark;
|
||||
}
|
||||
|
||||
// Resolve image paths (from TOML)
|
||||
fs::path imgDir = themeDir / "img";
|
||||
std::string bgFilename;
|
||||
std::string logoFilename;
|
||||
|
||||
if (auto* images = (*theme)["images"].as_table()) {
|
||||
if (auto bg = (*images)["background_image"].value<std::string>()) {
|
||||
bgFilename = *bg;
|
||||
}
|
||||
if (auto logo = (*images)["logo"].value<std::string>()) {
|
||||
logoFilename = *logo;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if image files exist
|
||||
if (!bgFilename.empty()) {
|
||||
fs::path bgPath = imgDir / bgFilename;
|
||||
if (fs::exists(bgPath)) {
|
||||
info.backgroundImagePath = bgPath.string();
|
||||
}
|
||||
}
|
||||
|
||||
if (!logoFilename.empty()) {
|
||||
fs::path logoImgPath = imgDir / logoFilename;
|
||||
if (fs::exists(logoImgPath)) {
|
||||
info.logoPath = logoImgPath.string();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info.name = info.id;
|
||||
}
|
||||
|
||||
skins_.push_back(std::move(info));
|
||||
}
|
||||
|
||||
// Also scan for loose .toml files (unified format with [theme.palette])
|
||||
for (const auto& entry : fs::directory_iterator(dir)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
|
||||
fs::path p = entry.path();
|
||||
if (p.extension() != ".toml") continue;
|
||||
|
||||
toml::table root;
|
||||
try {
|
||||
root = toml::parse_file(p.string());
|
||||
} catch (...) {
|
||||
DEBUG_LOGF("[SkinManager] Skipping '%s': invalid TOML\n", p.filename().string().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
SkinInfo info;
|
||||
info.path = p.string();
|
||||
info.id = p.stem().string();
|
||||
info.bundled = false;
|
||||
|
||||
// Check for unified format ([theme] with [theme.palette])
|
||||
auto* theme = root["theme"].as_table();
|
||||
if (theme) {
|
||||
if (auto name = (*theme)["name"].value<std::string>()) {
|
||||
info.name = *name;
|
||||
} else {
|
||||
info.name = info.id;
|
||||
}
|
||||
if (auto author = (*theme)["author"].value<std::string>()) {
|
||||
info.author = *author;
|
||||
}
|
||||
if (auto dark = (*theme)["dark"].value<bool>()) {
|
||||
info.dark = *dark;
|
||||
}
|
||||
|
||||
auto validation = validateSkinFile(p.string());
|
||||
info.valid = validation.valid;
|
||||
info.validationError = validation.error;
|
||||
|
||||
// Resolve image paths (look in same directory as the .toml file)
|
||||
fs::path imgDir = p.parent_path();
|
||||
std::string bgFilename;
|
||||
std::string logoFilename;
|
||||
|
||||
if (auto* images = (*theme)["images"].as_table()) {
|
||||
if (auto bg = (*images)["background_image"].value<std::string>()) {
|
||||
bgFilename = *bg;
|
||||
}
|
||||
if (auto logo = (*images)["logo"].value<std::string>()) {
|
||||
logoFilename = *logo;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bgFilename.empty()) {
|
||||
fs::path bgPath = imgDir / bgFilename;
|
||||
if (fs::exists(bgPath)) {
|
||||
info.backgroundImagePath = bgPath.string();
|
||||
}
|
||||
}
|
||||
if (!logoFilename.empty()) {
|
||||
fs::path logoImgPath = imgDir / logoFilename;
|
||||
if (fs::exists(logoImgPath)) {
|
||||
info.logoPath = logoImgPath.string();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
DEBUG_LOGF("[SkinManager] Skipping '%s': unrecognized TOML format\n",
|
||||
p.filename().string().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
skins_.push_back(std::move(info));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SkinManager::refresh() {
|
||||
skins_.clear();
|
||||
|
||||
// Scan bundled skins (res/ directory)
|
||||
scanDirectory(getBundledSkinsDirectory(), true);
|
||||
|
||||
// Scan user skins
|
||||
std::string userDir = getUserSkinsDirectory();
|
||||
if (fs::exists(userDir)) {
|
||||
scanDirectory(userDir, false);
|
||||
}
|
||||
|
||||
// Ensure the base "dragonx" theme always appears (it's ui.toml, the main theme).
|
||||
// Other bundled themes are discovered automatically from res/themes/*.toml.
|
||||
{
|
||||
bool found = false;
|
||||
for (const auto& s : skins_) {
|
||||
if (s.id == "dragonx") { found = true; break; }
|
||||
}
|
||||
if (!found) {
|
||||
SkinInfo info;
|
||||
info.id = "dragonx";
|
||||
info.name = "DragonX";
|
||||
info.author = "The Hush Developers";
|
||||
info.dark = true;
|
||||
info.bundled = true;
|
||||
info.valid = true;
|
||||
// Try to set path to ui.toml if it exists
|
||||
fs::path uiPath = fs::path(getBundledSkinsDirectory()) / "ui.toml";
|
||||
if (fs::exists(uiPath)) {
|
||||
info.path = uiPath.string();
|
||||
}
|
||||
skins_.push_back(std::move(info));
|
||||
DEBUG_LOGF("[SkinManager] Injected base theme: dragonx\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: "dragonx" first, then bundled grouped by mode (dark then light), then user
|
||||
std::sort(skins_.begin(), skins_.end(), [](const SkinInfo& a, const SkinInfo& b) {
|
||||
// DragonX always first
|
||||
if (a.id == "dragonx") return true;
|
||||
if (b.id == "dragonx") return false;
|
||||
|
||||
// Bundled before user
|
||||
if (a.bundled != b.bundled) return a.bundled;
|
||||
|
||||
// Group: dark themes first, then light themes
|
||||
if (a.dark != b.dark) return a.dark;
|
||||
|
||||
// Alphabetical by name within each group
|
||||
return a.name < b.name;
|
||||
});
|
||||
|
||||
DEBUG_LOGF("[SkinManager] Found %zu skins\n", skins_.size());
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Find
|
||||
// ============================================================================
|
||||
|
||||
const SkinManager::SkinInfo* SkinManager::findById(const std::string& id) const {
|
||||
for (const auto& skin : skins_) {
|
||||
if (skin.id == id) return &skin;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Validation
|
||||
// ============================================================================
|
||||
|
||||
SkinManager::ValidationResult SkinManager::validateSkinFile(const std::string& path) {
|
||||
ValidationResult result;
|
||||
|
||||
// 1. Must be valid TOML
|
||||
toml::table root;
|
||||
try {
|
||||
root = toml::parse_file(path);
|
||||
} catch (const toml::parse_error& e) {
|
||||
result.error = std::string("Invalid TOML: ") + e.what();
|
||||
return result;
|
||||
}
|
||||
|
||||
// 3. Must contain "theme" table
|
||||
auto* theme = root["theme"].as_table();
|
||||
if (!theme) {
|
||||
result.error = "Missing or invalid 'theme' section";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 4. theme.name must be a non-empty string
|
||||
auto name = (*theme)["name"].value<std::string>();
|
||||
if (!name || name->empty()) {
|
||||
result.error = "theme.name must be a non-empty string";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 5. theme.palette must exist with at least --primary and --background
|
||||
auto* palette = (*theme)["palette"].as_table();
|
||||
if (!palette) {
|
||||
result.error = "Missing theme.palette table";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!palette->contains("--primary")) {
|
||||
result.error = "Palette missing required '--primary' color";
|
||||
return result;
|
||||
}
|
||||
if (!palette->contains("--background")) {
|
||||
result.error = "Palette missing required '--background' color";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 6. If globals exists, must be a table
|
||||
if (root.contains("globals") && !root["globals"].is_table()) {
|
||||
result.error = "'globals' must be a table";
|
||||
return result;
|
||||
}
|
||||
|
||||
result.valid = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Import
|
||||
// ============================================================================
|
||||
|
||||
bool SkinManager::importSkin(const std::string& sourcePath) {
|
||||
fs::path srcPath(sourcePath);
|
||||
std::string userDir = getUserSkinsDirectory();
|
||||
|
||||
try {
|
||||
fs::create_directories(userDir);
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
DEBUG_LOGF("[SkinManager] Failed to create themes directory: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fs::is_directory(srcPath)) {
|
||||
// Import a theme folder — copy entire folder into themes/
|
||||
fs::path themeToml = srcPath / "theme.toml";
|
||||
if (!fs::exists(themeToml)) {
|
||||
DEBUG_LOGF("[SkinManager] Import folder has no theme.toml: %s\n", sourcePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
auto validation = validateSkinFile(themeToml.string());
|
||||
if (!validation.valid) {
|
||||
DEBUG_LOGF("[SkinManager] Import validation failed: %s\n", validation.error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::path destDir = fs::path(userDir) / srcPath.filename();
|
||||
try {
|
||||
fs::copy(srcPath, destDir, fs::copy_options::recursive | fs::copy_options::overwrite_existing);
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
DEBUG_LOGF("[SkinManager] Failed to copy theme folder: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[SkinManager] Imported theme folder: %s → %s\n", sourcePath.c_str(), destDir.string().c_str());
|
||||
} else {
|
||||
// Import a single .toml file — create a folder for it
|
||||
auto validation = validateSkinFile(sourcePath);
|
||||
if (!validation.valid) {
|
||||
DEBUG_LOGF("[SkinManager] Import validation failed: %s\n", validation.error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string folderName = srcPath.stem().string();
|
||||
fs::path destDir = fs::path(userDir) / folderName;
|
||||
try {
|
||||
fs::create_directories(destDir);
|
||||
fs::copy_file(srcPath, destDir / "theme.toml", fs::copy_options::overwrite_existing);
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
DEBUG_LOGF("[SkinManager] Failed to import skin file: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[SkinManager] Imported skin file as folder: %s → %s\n", sourcePath.c_str(), destDir.string().c_str());
|
||||
}
|
||||
|
||||
refresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Remove
|
||||
// ============================================================================
|
||||
|
||||
bool SkinManager::removeSkin(const std::string& id) {
|
||||
const SkinInfo* skin = findById(id);
|
||||
if (!skin) {
|
||||
DEBUG_LOGF("[SkinManager] Skin not found: %s\n", id.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skin->bundled) {
|
||||
DEBUG_LOGF("[SkinManager] Cannot remove bundled skin: %s\n", id.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!skin->directory.empty() && fs::is_directory(skin->directory)) {
|
||||
// Folder-based theme — remove the entire directory
|
||||
fs::remove_all(skin->directory);
|
||||
} else {
|
||||
// Legacy flat file
|
||||
fs::remove(skin->path);
|
||||
}
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
DEBUG_LOGF("[SkinManager] Failed to remove skin: %s\n", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[SkinManager] Removed skin: %s\n", id.c_str());
|
||||
|
||||
// If we removed the active skin, fall back to default
|
||||
if (activeSkinId_ == id) {
|
||||
setActiveSkin("dragonx");
|
||||
}
|
||||
|
||||
refresh();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Activate skin
|
||||
// ============================================================================
|
||||
|
||||
bool SkinManager::setActiveSkin(const std::string& id) {
|
||||
const SkinInfo* skin = findById(id);
|
||||
if (!skin) {
|
||||
DEBUG_LOGF("[SkinManager] Skin not found: %s\n", id.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!skin->valid) {
|
||||
DEBUG_LOGF("[SkinManager] Skin is invalid: %s (%s)\n", id.c_str(), skin->validationError.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool loaded = false;
|
||||
|
||||
// For skin files: always reload base layout first, then merge visual
|
||||
// overlay on top. This ensures overlays only change palette + backdrop
|
||||
// while inheriting all layout values from ui.toml.
|
||||
if (!skin->path.empty()) {
|
||||
auto& schema = UISchema::instance();
|
||||
std::string basePath = schema.basePath();
|
||||
|
||||
if (!basePath.empty() || schema.hasEmbeddedBase()) {
|
||||
if (!basePath.empty() && basePath == skin->path) {
|
||||
// Switching back to the base theme: full reload, clear overlay
|
||||
schema.reloadBase();
|
||||
schema.reapplyColorsToImGui();
|
||||
loaded = true;
|
||||
} else {
|
||||
// Switching to a non-base skin: reload base then merge overlay
|
||||
if (schema.reloadBase()) {
|
||||
if (schema.mergeOverlayFromFile(skin->path)) {
|
||||
schema.reapplyColorsToImGui();
|
||||
loaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: no base path or embedded data, full load of skin file
|
||||
if (!loaded && schema.loadFromFile(skin->path)) {
|
||||
schema.reapplyColorsToImGui();
|
||||
loaded = true;
|
||||
}
|
||||
} else if (!id.empty()) {
|
||||
// Skin with no path (e.g., "dragonx" on Windows with embedded ui.toml):
|
||||
// just reload the base to restore the default theme
|
||||
auto& schema = UISchema::instance();
|
||||
if (schema.hasEmbeddedBase() || !schema.basePath().empty()) {
|
||||
schema.reloadBase();
|
||||
schema.reapplyColorsToImGui();
|
||||
loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to built-in C++ themes (works even without theme files)
|
||||
if (!loaded) {
|
||||
if (!SetThemeById(id)) {
|
||||
DEBUG_LOGF("[SkinManager] Failed to load skin: %s\n", id.c_str());
|
||||
return false;
|
||||
}
|
||||
DEBUG_LOGF("[SkinManager] Loaded via built-in theme fallback: %s\n", id.c_str());
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
activeSkinId_ = id;
|
||||
DEBUG_LOGF("[SkinManager] Activated skin: %s (%s)\n", id.c_str(), skin->name.c_str());
|
||||
|
||||
// Reload theme visual effects config from the new skin's [effects] section
|
||||
effects::ThemeEffects::instance().loadFromTheme();
|
||||
|
||||
// Resolve image paths from UISchema (which parsed [theme] images from the TOML).
|
||||
// The UISchema stores relative filenames (e.g. "backgrounds/texture/drgx_bg.png");
|
||||
// resolve them to absolute paths using the theme's directory structure.
|
||||
{
|
||||
// Use a mutable reference to update the SkinInfo
|
||||
SkinInfo* mutableSkin = nullptr;
|
||||
for (auto& s : skins_) {
|
||||
if (s.id == id) { mutableSkin = &s; break; }
|
||||
}
|
||||
|
||||
fs::path imgDir = resolveImgDir(skin);
|
||||
resolveAndFireCallback(mutableSkin, imgDir);
|
||||
}
|
||||
|
||||
// Force acrylic to re-capture the background with new theme colors/images.
|
||||
// This must happen AFTER images are reloaded so the next frame renders the
|
||||
// updated background before capture.
|
||||
effects::ImGuiAcrylic::InvalidateCapture();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SkinManager::resolveAndReloadImages(const std::string& skinId, const std::string& tomlPath) {
|
||||
// Find the skin and update its image paths from the current UISchema values
|
||||
SkinInfo* skin = nullptr;
|
||||
for (auto& s : skins_) {
|
||||
if (s.id == skinId) {
|
||||
skin = &s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fs::path imgDir = resolveImgDir(skin);
|
||||
resolveAndFireCallback(skin, imgDir);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gradient mode
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void SkinManager::setGradientMode(bool enabled) {
|
||||
if (gradientMode_ == enabled) return;
|
||||
gradientMode_ = enabled;
|
||||
|
||||
// Re-resolve + reload for the currently active skin
|
||||
const SkinInfo* skin = findById(activeSkinId_);
|
||||
if (!skin) return;
|
||||
|
||||
SkinInfo* mutableSkin = nullptr;
|
||||
for (auto& s : skins_) {
|
||||
if (s.id == activeSkinId_) { mutableSkin = &s; break; }
|
||||
}
|
||||
|
||||
fs::path imgDir = resolveImgDir(skin);
|
||||
resolveAndFireCallback(mutableSkin, imgDir);
|
||||
|
||||
effects::ImGuiAcrylic::InvalidateCapture();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fs::path SkinManager::resolveImgDir(const SkinInfo* skin) const {
|
||||
if (!skin) {
|
||||
return fs::path(getBundledSkinsDirectory()).parent_path() / "img";
|
||||
}
|
||||
if (skin->bundled) {
|
||||
fs::path themeDir = skin->path.empty()
|
||||
? fs::path(getBundledSkinsDirectory())
|
||||
: fs::path(skin->path).parent_path();
|
||||
return themeDir.parent_path() / "img";
|
||||
}
|
||||
if (!skin->directory.empty()) {
|
||||
return fs::path(skin->directory) / "img";
|
||||
}
|
||||
if (!skin->path.empty()) {
|
||||
return fs::path(skin->path).parent_path();
|
||||
}
|
||||
return fs::path(getBundledSkinsDirectory()).parent_path() / "img";
|
||||
}
|
||||
|
||||
std::string SkinManager::resolveGradientBg(const std::string& bgFilename,
|
||||
const fs::path& imgDir,
|
||||
bool isDark) const {
|
||||
// Given bgFilename like "backgrounds/texture/drgx_bg.png",
|
||||
// look for "backgrounds/gradient/gradient_drgx_bg.png".
|
||||
fs::path bgRel(bgFilename);
|
||||
std::string stem = bgRel.stem().string(); // "drgx_bg"
|
||||
std::string ext = bgRel.extension().string(); // ".png"
|
||||
|
||||
// Build the gradient candidate: backgrounds/gradient/gradient_<stem><ext>
|
||||
std::string gradientRel = "backgrounds/gradient/gradient_" + stem + ext;
|
||||
fs::path gradientPath = imgDir / gradientRel;
|
||||
|
||||
if (fs::exists(gradientPath)) {
|
||||
return gradientPath.string();
|
||||
}
|
||||
|
||||
// Fallback: dark_gradient.png or light_gradient.png
|
||||
std::string fallbackName = isDark ? "dark_gradient.png" : "light_gradient.png";
|
||||
std::string fallbackRel = "backgrounds/gradient/" + fallbackName;
|
||||
fs::path fallbackPath = imgDir / fallbackRel;
|
||||
|
||||
if (fs::exists(fallbackPath)) {
|
||||
return fallbackPath.string();
|
||||
}
|
||||
|
||||
// Last resort: pass the relative gradient filename for embedded lookup (Windows)
|
||||
return gradientRel;
|
||||
}
|
||||
|
||||
void SkinManager::resolveAndFireCallback(SkinInfo* skin, const fs::path& imgDir) {
|
||||
auto& schema = UISchema::instance();
|
||||
std::string bgFilename = schema.backgroundImagePath();
|
||||
std::string logoFilename = schema.logoImagePath();
|
||||
|
||||
std::string resolvedBg;
|
||||
std::string resolvedLogo;
|
||||
|
||||
if (!bgFilename.empty()) {
|
||||
if (gradientMode_) {
|
||||
resolvedBg = resolveGradientBg(bgFilename, imgDir, schema.isDarkTheme());
|
||||
} else {
|
||||
fs::path bgPath = imgDir / bgFilename;
|
||||
if (fs::exists(bgPath)) {
|
||||
resolvedBg = bgPath.string();
|
||||
} else {
|
||||
resolvedBg = bgFilename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!logoFilename.empty()) {
|
||||
fs::path logoPath = imgDir / logoFilename;
|
||||
if (fs::exists(logoPath)) {
|
||||
resolvedLogo = logoPath.string();
|
||||
} else {
|
||||
resolvedLogo = logoFilename;
|
||||
}
|
||||
}
|
||||
|
||||
if (skin) {
|
||||
skin->backgroundImagePath = resolvedBg;
|
||||
skin->logoPath = resolvedLogo;
|
||||
}
|
||||
|
||||
DEBUG_LOGF("[SkinManager] Resolved images (gradient=%s): bg='%s', logo='%s'\n",
|
||||
gradientMode_ ? "on" : "off", resolvedBg.c_str(), resolvedLogo.c_str());
|
||||
|
||||
if (imageReloadCb_) {
|
||||
imageReloadCb_(resolvedBg, resolvedLogo);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace schema
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user