1846 lines
90 KiB
C++
1846 lines
90 KiB
C++
// DragonX Wallet - ImGui Edition
|
||
// Copyright 2024-2026 The Hush Developers
|
||
// Released under the GPLv3
|
||
|
||
#include "settings_page.h"
|
||
#include "../../app.h"
|
||
#include "../../config/version.h"
|
||
#include "../../config/settings.h"
|
||
#include "../windows/balance_tab.h"
|
||
#include "../windows/console_tab.h"
|
||
#include "../../util/i18n.h"
|
||
#include "../../util/platform.h"
|
||
#include "../../rpc/rpc_client.h"
|
||
#include "../../rpc/rpc_worker.h"
|
||
#include "../theme.h"
|
||
#include "../layout.h"
|
||
#include "../schema/ui_schema.h"
|
||
#include "../schema/skin_manager.h"
|
||
#include "../notifications.h"
|
||
#include "../effects/imgui_acrylic.h"
|
||
#include "../effects/theme_effects.h"
|
||
#include "../effects/low_spec.h"
|
||
#include "../effects/scroll_fade_shader.h"
|
||
#include "../material/draw_helpers.h"
|
||
#include "../material/type.h"
|
||
#include "../material/colors.h"
|
||
#include "../windows/validate_address_dialog.h"
|
||
#include "../windows/address_book_dialog.h"
|
||
#include "../windows/shield_dialog.h"
|
||
#include "../windows/request_payment_dialog.h"
|
||
#include "../windows/block_info_dialog.h"
|
||
#include "../windows/export_all_keys_dialog.h"
|
||
#include "../windows/export_transactions_dialog.h"
|
||
#include "../../embedded/IconsMaterialDesign.h"
|
||
#include "imgui.h"
|
||
#include <nlohmann/json.hpp>
|
||
#include <vector>
|
||
#include <set>
|
||
#include <filesystem>
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
namespace dragonx {
|
||
namespace ui {
|
||
|
||
using namespace material;
|
||
|
||
// ============================================================================
|
||
// Settings state — loaded from config::Settings on first render
|
||
// ============================================================================
|
||
static bool sp_initialized = false;
|
||
static int sp_language_index = 0;
|
||
static bool sp_save_ztxs = true;
|
||
static bool sp_allow_custom_fees = false;
|
||
static bool sp_auto_shield = false;
|
||
static bool sp_fetch_prices = true;
|
||
static bool sp_use_tor = false;
|
||
static char sp_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
|
||
static char sp_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT;
|
||
static char sp_rpc_user[64] = "";
|
||
static char sp_rpc_password[64] = "";
|
||
static char sp_tx_explorer[256] = "https://explorer.dragonx.is/tx/";
|
||
static char sp_addr_explorer[256] = "https://explorer.dragonx.is/address/";
|
||
|
||
// Acrylic settings
|
||
static bool sp_acrylic_enabled = true;
|
||
static float sp_blur_amount = 1.5f; // 0.0=Off, 0.01–4.0 continuous blur multiplier
|
||
static float sp_noise_opacity = 0.5f; // 0.0–1.0 multiplier
|
||
static float sp_ui_opacity = 1.0f; // 0.3–1.0 card/sidebar opacity
|
||
static float sp_window_opacity = 1.0f; // 0.3–1.0 background alpha
|
||
|
||
// Balance layout (string ID)
|
||
static std::string sp_balance_layout = "classic";
|
||
|
||
// Console scanline
|
||
static bool sp_scanline_enabled = true;
|
||
|
||
// Theme effects
|
||
static bool sp_theme_effects_enabled = true;
|
||
|
||
// Gradient background mode
|
||
static bool sp_gradient_background = false;
|
||
|
||
// Low-spec mode
|
||
static bool sp_low_spec_mode = false;
|
||
|
||
// Font scale (user accessibility, 1.0–3.0)
|
||
static float sp_font_scale = 1.0f;
|
||
|
||
// Snapshot of effect settings saved when low-spec is toggled ON,
|
||
// restored when toggled OFF so user state isn't lost.
|
||
struct LowSpecSnapshot {
|
||
bool valid = false;
|
||
bool acrylic_enabled;
|
||
float blur_amount;
|
||
float ui_opacity;
|
||
float window_opacity;
|
||
bool theme_effects_enabled;
|
||
bool scanline_enabled;
|
||
};
|
||
static LowSpecSnapshot s_lowSpecSnap;
|
||
|
||
// Daemon — keep running on close
|
||
static bool sp_keep_daemon_running = false;
|
||
static bool sp_stop_external_daemon = false;
|
||
|
||
// Debug logging categories
|
||
static std::set<std::string> sp_debug_categories;
|
||
static bool sp_debug_cats_dirty = false; // true when changed but daemon not yet restarted
|
||
static bool sp_debug_expanded = false; // collapsible card state
|
||
|
||
// (APPEARANCE card now uses ChannelsSplit like all other cards)
|
||
|
||
// Shader-based scroll-edge fade (per-pixel alpha mask via custom fragment shader)
|
||
static effects::ScrollFadeShader s_fadeShader;
|
||
|
||
|
||
|
||
static void loadSettingsPageState(config::Settings* settings) {
|
||
if (!settings) return;
|
||
|
||
sp_save_ztxs = settings->getSaveZtxs();
|
||
sp_allow_custom_fees = settings->getAllowCustomFees();
|
||
sp_auto_shield = settings->getAutoShield();
|
||
sp_fetch_prices = settings->getFetchPrices();
|
||
sp_use_tor = settings->getUseTor();
|
||
|
||
strncpy(sp_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(sp_tx_explorer) - 1);
|
||
strncpy(sp_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(sp_addr_explorer) - 1);
|
||
|
||
auto& i18n = util::I18n::instance();
|
||
const auto& languages = i18n.getAvailableLanguages();
|
||
std::string current_lang = settings->getLanguage();
|
||
if (current_lang.empty()) current_lang = "en";
|
||
|
||
sp_language_index = 0;
|
||
int idx = 0;
|
||
for (const auto& lang : languages) {
|
||
if (lang.first == current_lang) {
|
||
sp_language_index = idx;
|
||
break;
|
||
}
|
||
idx++;
|
||
}
|
||
|
||
// Load blur amount directly from saved multiplier
|
||
sp_blur_amount = settings->getBlurMultiplier();
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
sp_ui_opacity = settings->getUIOpacity();
|
||
sp_window_opacity = settings->getWindowOpacity();
|
||
sp_noise_opacity = settings->getNoiseOpacity();
|
||
|
||
sp_gradient_background = settings->getGradientBackground();
|
||
|
||
sp_balance_layout = settings->getBalanceLayout();
|
||
sp_scanline_enabled = settings->getScanlineEnabled();
|
||
ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
|
||
sp_theme_effects_enabled = settings->getThemeEffectsEnabled();
|
||
sp_low_spec_mode = settings->getLowSpecMode();
|
||
effects::setLowSpecMode(sp_low_spec_mode);
|
||
sp_font_scale = settings->getFontScale();
|
||
Layout::setUserFontScale(sp_font_scale); // sync with Layout on load
|
||
sp_keep_daemon_running = settings->getKeepDaemonRunning();
|
||
sp_stop_external_daemon = settings->getStopExternalDaemon();
|
||
sp_debug_categories = settings->getDebugCategories();
|
||
sp_debug_cats_dirty = false;
|
||
|
||
// Apply loaded visual effects settings
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
|
||
effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity);
|
||
effects::ImGuiAcrylic::SetNoiseOpacity(sp_noise_opacity);
|
||
effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled);
|
||
|
||
sp_initialized = true;
|
||
}
|
||
|
||
static void saveSettingsPageState(config::Settings* settings) {
|
||
if (!settings) return;
|
||
|
||
settings->setTheme(settings->getSkinId());
|
||
settings->setSaveZtxs(sp_save_ztxs);
|
||
settings->setAllowCustomFees(sp_allow_custom_fees);
|
||
settings->setAutoShield(sp_auto_shield);
|
||
settings->setFetchPrices(sp_fetch_prices);
|
||
settings->setUseTor(sp_use_tor);
|
||
settings->setTxExplorerUrl(sp_tx_explorer);
|
||
settings->setAddressExplorerUrl(sp_addr_explorer);
|
||
|
||
auto& i18n = util::I18n::instance();
|
||
const auto& languages = i18n.getAvailableLanguages();
|
||
auto it = languages.begin();
|
||
std::advance(it, sp_language_index);
|
||
if (it != languages.end()) {
|
||
settings->setLanguage(it->first);
|
||
}
|
||
|
||
// Visual effects settings
|
||
settings->setAcrylicEnabled(sp_acrylic_enabled);
|
||
settings->setAcrylicQuality(sp_blur_amount > 0.001f ? static_cast<int>(effects::AcrylicQuality::Low) : static_cast<int>(effects::AcrylicQuality::Off));
|
||
settings->setBlurMultiplier(sp_blur_amount);
|
||
settings->setUIOpacity(sp_ui_opacity);
|
||
settings->setWindowOpacity(sp_window_opacity);
|
||
settings->setNoiseOpacity(sp_noise_opacity);
|
||
settings->setGradientBackground(sp_gradient_background);
|
||
settings->setScanlineEnabled(sp_scanline_enabled);
|
||
settings->setThemeEffectsEnabled(sp_theme_effects_enabled);
|
||
settings->setLowSpecMode(sp_low_spec_mode);
|
||
settings->setFontScale(sp_font_scale);
|
||
settings->setKeepDaemonRunning(sp_keep_daemon_running);
|
||
settings->setStopExternalDaemon(sp_stop_external_daemon);
|
||
settings->setDebugCategories(sp_debug_categories);
|
||
|
||
settings->save();
|
||
}
|
||
|
||
// ============================================================================
|
||
// Settings Page Renderer
|
||
// ============================================================================
|
||
|
||
void RenderSettingsPage(App* app) {
|
||
// Load settings state on first render
|
||
if (!sp_initialized && app->settings()) {
|
||
loadSettingsPageState(app->settings());
|
||
}
|
||
|
||
// Sync low-spec / theme-effects state from runtime each frame
|
||
// so that hotkey toggles are reflected in the checkboxes.
|
||
{
|
||
bool runtimeLowSpec = effects::isLowSpecMode();
|
||
if (sp_low_spec_mode != runtimeLowSpec) {
|
||
if (runtimeLowSpec) {
|
||
// Hotkey turned low-spec ON — save snapshot, override statics
|
||
s_lowSpecSnap.valid = true;
|
||
s_lowSpecSnap.acrylic_enabled = sp_acrylic_enabled;
|
||
s_lowSpecSnap.blur_amount = sp_blur_amount;
|
||
s_lowSpecSnap.ui_opacity = sp_ui_opacity;
|
||
s_lowSpecSnap.window_opacity = sp_window_opacity;
|
||
s_lowSpecSnap.theme_effects_enabled = sp_theme_effects_enabled;
|
||
s_lowSpecSnap.scanline_enabled = sp_scanline_enabled;
|
||
sp_acrylic_enabled = false;
|
||
sp_blur_amount = 0.0f;
|
||
sp_ui_opacity = 1.0f;
|
||
sp_window_opacity = 1.0f;
|
||
sp_theme_effects_enabled = false;
|
||
sp_scanline_enabled = false;
|
||
} else if (s_lowSpecSnap.valid) {
|
||
// Hotkey turned low-spec OFF — restore snapshot
|
||
sp_blur_amount = s_lowSpecSnap.blur_amount;
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
sp_ui_opacity = s_lowSpecSnap.ui_opacity;
|
||
sp_window_opacity = s_lowSpecSnap.window_opacity;
|
||
sp_theme_effects_enabled = s_lowSpecSnap.theme_effects_enabled;
|
||
sp_scanline_enabled = s_lowSpecSnap.scanline_enabled;
|
||
s_lowSpecSnap.valid = false;
|
||
} else if (app->settings()) {
|
||
// No snapshot — read prefs from settings file
|
||
sp_blur_amount = app->settings()->getBlurMultiplier();
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
sp_ui_opacity = app->settings()->getUIOpacity();
|
||
sp_window_opacity = app->settings()->getWindowOpacity();
|
||
sp_theme_effects_enabled = app->settings()->getThemeEffectsEnabled();
|
||
sp_scanline_enabled = app->settings()->getScanlineEnabled();
|
||
}
|
||
sp_low_spec_mode = runtimeLowSpec;
|
||
}
|
||
bool runtimeThemeEffects = effects::ThemeEffects::instance().isEnabled();
|
||
if (sp_theme_effects_enabled != runtimeThemeEffects) {
|
||
sp_theme_effects_enabled = runtimeThemeEffects;
|
||
}
|
||
}
|
||
|
||
auto& S = schema::UI();
|
||
|
||
// Responsive layout — matches other tabs
|
||
ImVec2 contentAvail = ImGui::GetContentRegionAvail();
|
||
float scrollbarMargin = ImGui::GetStyle().ScrollbarSize + Layout::spacingSm();
|
||
float availWidth = contentAvail.x - scrollbarMargin;
|
||
float hs = Layout::hScale(availWidth);
|
||
float vs = Layout::vScale(contentAvail.y);
|
||
float pad = Layout::cardInnerPadding();
|
||
float bottomPad = std::max(0.0f, pad - ImGui::GetStyle().ItemSpacing.y);
|
||
float gap = Layout::cardGap();
|
||
float glassRound = Layout::glassRounding();
|
||
|
||
char buf[256];
|
||
|
||
// Label column position — adaptive to width
|
||
float labelW = std::max(S.drawElement("components.settings-page", "label-min-width").size, S.drawElement("components.settings-page", "label-width").size * hs);
|
||
// Input field width — fill remaining space in card
|
||
float inputW = std::max(S.drawElement("components.settings-page", "input-min-width").size, availWidth - labelW - pad * 2);
|
||
|
||
// Scrollable content area — NoBackground matches other tabs
|
||
|
||
ImGui::BeginChild("##SettingsPageScroll", ImVec2(0, 0), false,
|
||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
||
ApplySmoothScroll();
|
||
|
||
// Capture the ACTUAL clip boundaries from inside the child window
|
||
ImVec2 childClipMin = ImGui::GetWindowPos();
|
||
ImVec2 childClipMax(childClipMin.x + ImGui::GetWindowSize().x,
|
||
childClipMin.y + ImGui::GetWindowSize().y);
|
||
const float dp = Layout::dpiScale();
|
||
const float fadeH = schema::UI().drawElement("components.settings-page", "edge-fade-zone").size * dp;
|
||
const float fadeOffTop = schema::UI().drawElement("components.settings-page", "edge-fade-offset-top").size * dp;
|
||
const float fadeOffBot = schema::UI().drawElement("components.settings-page", "edge-fade-offset-bottom").size * dp;
|
||
|
||
// Get draw list AFTER BeginChild so we draw on the child window's list
|
||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||
(void)dl; // used by cards below
|
||
|
||
// Capture ForegroundDrawList vertex start — DrawGlassPanel draws
|
||
// theme effects (rainbow border, shimmer, specular glare, edge trace)
|
||
// on the ForegroundDrawList; those bypass the shader fade so we'll
|
||
// apply a vertex-based alpha fade to them after rendering.
|
||
ImDrawList* fgDL = ImGui::GetForegroundDrawList();
|
||
int fgVtxStart = fgDL->VtxBuffer.Size;
|
||
|
||
// --- Shader-based scroll fade: bind custom fragment shader ---
|
||
// The shader multiplies output alpha by a smoothstep gradient based
|
||
// on screen Y, giving a true per-pixel alpha mask at scroll edges.
|
||
float settingsScrollY_pre = ImGui::GetScrollY();
|
||
float settingsScrollMaxY_pre = ImGui::GetScrollMaxY();
|
||
float settingsFadeTopY = childClipMin.y + fadeOffTop;
|
||
float settingsFadeBottomY = childClipMax.y - fadeOffBot;
|
||
float settingsFadeZoneTop = (settingsScrollY_pre > 1.0f) ? fadeH : 0.0f;
|
||
float settingsFadeZoneBot = (settingsScrollMaxY_pre > 0 && settingsScrollY_pre < settingsScrollMaxY_pre - 1.0f) ? fadeH : 0.0f;
|
||
if (fadeH > 0.0f && !sp_low_spec_mode && s_fadeShader.init()) {
|
||
s_fadeShader.fadeTopY = settingsFadeTopY;
|
||
s_fadeShader.fadeBottomY = settingsFadeBottomY;
|
||
s_fadeShader.fadeZoneTop = settingsFadeZoneTop;
|
||
s_fadeShader.fadeZoneBottom = settingsFadeZoneBot;
|
||
s_fadeShader.addBind(dl);
|
||
}
|
||
|
||
// Top margin from schema
|
||
float topMargin = schema::UI().drawElement("components.settings-page", "top-margin").size;
|
||
if (topMargin > 0.0f)
|
||
ImGui::Dummy(ImVec2(0, topMargin));
|
||
|
||
GlassPanelSpec glassSpec;
|
||
glassSpec.rounding = glassRound;
|
||
ImFont* capFont = Type().caption();
|
||
ImFont* body2 = Type().body2();
|
||
ImFont* sub1 = Type().subtitle1();
|
||
|
||
// ====================================================================
|
||
// APPEARANCE — card (draw-first approach; avoids ChannelsSplit which
|
||
// breaks BeginCombo popup rendering in some ImGui versions)
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "APPEARANCE");
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
float contentW = availWidth - pad * 2;
|
||
float comboGap = S.drawElement("components.settings-page", "combo-row-gap").size;
|
||
float compactBP = S.drawElement("components.settings-page", "compact-breakpoint").size;
|
||
bool wideLayout = availWidth >= compactBP;
|
||
float refreshBtnW = S.drawElement("components.settings-page", "refresh-btn-width").size;
|
||
|
||
// --- Skin data ---
|
||
auto& skinMgr = schema::SkinManager::instance();
|
||
const auto& skins = skinMgr.available();
|
||
std::string active_preview = "DragonX";
|
||
bool active_is_custom = false;
|
||
for (const auto& skin : skins) {
|
||
if (skin.id == skinMgr.activeSkinId()) {
|
||
active_preview = skin.name;
|
||
active_is_custom = !skin.bundled;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// --- Language data ---
|
||
auto& i18n = util::I18n::instance();
|
||
const auto& languages = i18n.getAvailableLanguages();
|
||
std::vector<const char*> lang_names;
|
||
lang_names.reserve(languages.size());
|
||
for (const auto& lang : languages) {
|
||
lang_names.push_back(lang.second.c_str());
|
||
}
|
||
|
||
// --- Balance layout data ---
|
||
const auto& layouts = GetBalanceLayouts();
|
||
std::string balPreview = sp_balance_layout;
|
||
for (const auto& l : layouts) {
|
||
if (l.id == sp_balance_layout) { balPreview = l.name; break; }
|
||
}
|
||
|
||
// --- Theme combo popup (shared between wide and narrow paths) ---
|
||
auto renderThemeComboPopup = [&]() {
|
||
ImGui::TextDisabled("Built-in");
|
||
ImGui::Separator();
|
||
for (size_t i = 0; i < skins.size(); i++) {
|
||
const auto& skin = skins[i];
|
||
if (!skin.bundled) continue;
|
||
bool is_selected = (skin.id == skinMgr.activeSkinId());
|
||
if (ImGui::Selectable(skin.name.c_str(), is_selected)) {
|
||
skinMgr.setActiveSkin(skin.id);
|
||
if (app->settings()) {
|
||
app->settings()->setSkinId(skin.id);
|
||
app->settings()->save();
|
||
}
|
||
}
|
||
if (is_selected) ImGui::SetItemDefaultFocus();
|
||
}
|
||
bool has_custom = false;
|
||
for (const auto& skin : skins) {
|
||
if (!skin.bundled) { has_custom = true; break; }
|
||
}
|
||
if (has_custom) {
|
||
ImGui::Spacing();
|
||
ImGui::TextDisabled("Custom");
|
||
ImGui::Separator();
|
||
for (size_t i = 0; i < skins.size(); i++) {
|
||
const auto& skin = skins[i];
|
||
if (skin.bundled) continue;
|
||
bool is_selected = (skin.id == skinMgr.activeSkinId());
|
||
if (!skin.valid) {
|
||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
|
||
ImGui::BeginDisabled(true);
|
||
std::string lbl = skin.name + " (invalid)";
|
||
ImGui::Selectable(lbl.c_str(), false);
|
||
ImGui::EndDisabled();
|
||
ImGui::PopStyleColor();
|
||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||
ImGui::SetTooltip("%s", skin.validationError.c_str());
|
||
} else {
|
||
std::string lbl = skin.name;
|
||
if (!skin.author.empty()) lbl += " (" + skin.author + ")";
|
||
if (ImGui::Selectable(lbl.c_str(), is_selected)) {
|
||
skinMgr.setActiveSkin(skin.id);
|
||
if (app->settings()) {
|
||
app->settings()->setSkinId(skin.id);
|
||
app->settings()->save();
|
||
}
|
||
}
|
||
if (is_selected) ImGui::SetItemDefaultFocus();
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
if (wideLayout) {
|
||
// ============================================================
|
||
// Wide: 3 combos on one row + compact 3-column effects grid
|
||
// ============================================================
|
||
|
||
// --- Combo row: Theme | Layout | Language [Refresh] ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
float lblGap = Layout::spacingXs();
|
||
float lblThemeW = ImGui::CalcTextSize("Theme").x + lblGap;
|
||
float lblLayoutW = ImGui::CalcTextSize("Balance Layout").x + lblGap;
|
||
float lblLangW = ImGui::CalcTextSize("Language").x + lblGap;
|
||
float totalFixed = lblThemeW + lblLayoutW + lblLangW
|
||
+ comboGap * 2 + Layout::spacingSm() + refreshBtnW;
|
||
float comboW = std::max(80.0f, (contentW - totalFixed) / 3.0f);
|
||
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Theme");
|
||
ImGui::SameLine(0, lblGap);
|
||
ImGui::SetNextItemWidth(comboW);
|
||
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
||
renderThemeComboPopup();
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("Hotkey: Ctrl+Left/Right to cycle themes");
|
||
|
||
ImGui::SameLine(0, comboGap);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Balance Layout");
|
||
ImGui::SameLine(0, lblGap);
|
||
ImGui::SetNextItemWidth(comboW);
|
||
if (ImGui::BeginCombo("##BalanceLayout", balPreview.c_str())) {
|
||
for (const auto& l : layouts) {
|
||
if (!l.enabled) continue;
|
||
bool selected = (l.id == sp_balance_layout);
|
||
if (ImGui::Selectable(l.name.c_str(), selected)) {
|
||
sp_balance_layout = l.id;
|
||
if (app->settings()) {
|
||
app->settings()->setBalanceLayout(sp_balance_layout);
|
||
app->settings()->save();
|
||
}
|
||
}
|
||
if (selected) ImGui::SetItemDefaultFocus();
|
||
}
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("Hotkey: Left/Right arrow keys to cycle Balance layouts");
|
||
|
||
ImGui::SameLine(0, comboGap);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Language");
|
||
ImGui::SameLine(0, lblGap);
|
||
ImGui::SetNextItemWidth(comboW);
|
||
if (ImGui::Combo("##Language", &sp_language_index, lang_names.data(),
|
||
static_cast<int>(lang_names.size()))) {
|
||
auto it = languages.begin();
|
||
std::advance(it, sp_language_index);
|
||
i18n.loadLanguage(it->first);
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Interface language for the wallet UI");
|
||
|
||
ImGui::SameLine(0, Layout::spacingSm());
|
||
if (TactileButton("Refresh", ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
||
schema::SkinManager::instance().refresh();
|
||
Notifications::instance().info("Theme list refreshed");
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
|
||
schema::SkinManager::getUserSkinsDirectory().c_str());
|
||
}
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// --- Visual Effects (checkboxes on one row, Quality+Blur paired) ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
|
||
// Checkbox row: Low-spec | Console scanline | Theme effects | Gradient background
|
||
if (ImGui::Checkbox("Low-spec mode", &sp_low_spec_mode)) {
|
||
effects::setLowSpecMode(sp_low_spec_mode);
|
||
if (sp_low_spec_mode) {
|
||
s_lowSpecSnap.valid = true;
|
||
s_lowSpecSnap.acrylic_enabled = sp_acrylic_enabled;
|
||
s_lowSpecSnap.blur_amount = sp_blur_amount;
|
||
s_lowSpecSnap.ui_opacity = sp_ui_opacity;
|
||
s_lowSpecSnap.window_opacity = sp_window_opacity;
|
||
s_lowSpecSnap.theme_effects_enabled = sp_theme_effects_enabled;
|
||
s_lowSpecSnap.scanline_enabled = sp_scanline_enabled;
|
||
sp_acrylic_enabled = false;
|
||
sp_blur_amount = 0.0f;
|
||
sp_ui_opacity = 1.0f;
|
||
sp_window_opacity = 1.0f;
|
||
sp_theme_effects_enabled = false;
|
||
sp_scanline_enabled = false;
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(0.0f);
|
||
effects::ImGuiAcrylic::SetUIOpacity(1.0f);
|
||
effects::ThemeEffects::instance().setEnabled(false);
|
||
ConsoleTab::s_scanline_enabled = false;
|
||
} else if (s_lowSpecSnap.valid) {
|
||
sp_blur_amount = s_lowSpecSnap.blur_amount;
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
sp_ui_opacity = s_lowSpecSnap.ui_opacity;
|
||
sp_window_opacity = s_lowSpecSnap.window_opacity;
|
||
sp_theme_effects_enabled = s_lowSpecSnap.theme_effects_enabled;
|
||
sp_scanline_enabled = s_lowSpecSnap.scanline_enabled;
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
|
||
effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity);
|
||
effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled);
|
||
ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
|
||
s_lowSpecSnap.valid = false;
|
||
}
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Disable all heavy visual effects\nHotkey: Ctrl+Shift+Down");
|
||
|
||
// Simple background is lightweight — always interactive, even in low-spec mode
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
if (ImGui::Checkbox("Simple background", &sp_gradient_background)) {
|
||
schema::SkinManager::instance().setGradientMode(sp_gradient_background);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Use a simple gradient for the background\nHotkey: Ctrl+Up");
|
||
|
||
ImGui::BeginDisabled(sp_low_spec_mode);
|
||
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
if (ImGui::Checkbox("Console scanline", &sp_scanline_enabled)) {
|
||
ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
|
||
app->settings()->setScanlineEnabled(sp_scanline_enabled);
|
||
app->settings()->save();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("CRT scanline effect in console");
|
||
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
if (ImGui::Checkbox("Theme effects", &sp_theme_effects_enabled)) {
|
||
effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Shimmer, glow, hue-cycling per theme");
|
||
|
||
// Row 1: Acrylic preset slider + Noise slider (side by side, labels above)
|
||
float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size;
|
||
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
||
float ctrlW = std::max(effCtrlMinW, halfW);
|
||
float baseX = ImGui::GetCursorScreenPos().x;
|
||
float rightX = baseX + ctrlW + Layout::spacingLg();
|
||
|
||
// Acrylic label + slider (left column)
|
||
ImGui::TextUnformatted("Acrylic");
|
||
float row1Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
// Build display format: "Off" at zero, percentage otherwise
|
||
char blur_fmt[16];
|
||
if (sp_blur_amount < 0.01f)
|
||
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
|
||
else
|
||
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
|
||
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
// Snap to off when dragged near 0%
|
||
if (sp_blur_amount > 0.0f && sp_blur_amount < 0.15f) sp_blur_amount = 0.0f;
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
|
||
}
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Blur amount (0%% = off, 100%% = maximum)");
|
||
float afterRow1Y = ImGui::GetCursorScreenPos().y;
|
||
|
||
// Noise label + slider (right column, same row)
|
||
float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y;
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y - lblH));
|
||
ImGui::TextUnformatted("Noise");
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y));
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char noise_fmt[16];
|
||
if (sp_noise_opacity < 0.01f)
|
||
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
|
||
else
|
||
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
effects::ImGuiAcrylic::SetNoiseOpacity(sp_noise_opacity);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Grain texture intensity (0%% = off, 100%% = maximum)");
|
||
|
||
// Reset cursor to left column, past row 1
|
||
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y));
|
||
|
||
// Row 2: UI Opacity + Window Opacity (labels above)
|
||
ImGui::TextUnformatted("UI Opacity");
|
||
float row2Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char uiop_fmt[16];
|
||
snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", sp_ui_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##UIOpacity", &sp_ui_opacity, 0.3f, 1.0f, uiop_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Card and sidebar opacity (100%% = fully opaque, lower = more see-through)");
|
||
float afterRow2Y = ImGui::GetCursorScreenPos().y;
|
||
|
||
// Window label + slider (right column, same row)
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH));
|
||
ImGui::TextUnformatted("Window");
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y));
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char winop_fmt[16];
|
||
snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", sp_window_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##WindowOpacity", &sp_window_opacity, 0.3f, 1.0f, winop_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Background opacity (lower = desktop visible through window)");
|
||
|
||
// Reset cursor to left column, past row 2
|
||
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y));
|
||
|
||
ImGui::EndDisabled(); // low-spec
|
||
ImGui::PopFont();
|
||
}
|
||
} else {
|
||
// ============================================================
|
||
// Narrow: stacked combos + 2-column effects (original layout)
|
||
// ============================================================
|
||
|
||
// --- Theme row ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Theme");
|
||
ImGui::SameLine(labelW);
|
||
|
||
float themeComboW = std::max(S.drawElement("components.settings-page", "theme-combo-min-width").size,
|
||
availWidth - pad * 2 - labelW - refreshBtnW - Layout::spacingSm());
|
||
ImGui::SetNextItemWidth(themeComboW);
|
||
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
|
||
renderThemeComboPopup();
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("Hotkey: Ctrl+Left/Right to cycle themes");
|
||
if (active_is_custom) {
|
||
ImGui::SameLine();
|
||
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Custom theme active");
|
||
}
|
||
ImGui::SameLine();
|
||
if (TactileButton("Refresh", ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
|
||
schema::SkinManager::instance().refresh();
|
||
Notifications::instance().info("Theme list refreshed");
|
||
}
|
||
if (ImGui::IsItemHovered()) {
|
||
ImGui::SetTooltip("Scan for new themes.\nPlace theme folders in:\n%s",
|
||
schema::SkinManager::getUserSkinsDirectory().c_str());
|
||
}
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// --- Balance Layout row ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Balance Layout");
|
||
ImGui::SameLine(labelW);
|
||
ImGui::SetNextItemWidth(std::max(180.0f, inputW));
|
||
if (ImGui::BeginCombo("##BalanceLayout", balPreview.c_str())) {
|
||
for (const auto& l : layouts) {
|
||
if (!l.enabled) continue;
|
||
bool selected = (l.id == sp_balance_layout);
|
||
if (ImGui::Selectable(l.name.c_str(), selected)) {
|
||
sp_balance_layout = l.id;
|
||
if (app->settings()) {
|
||
app->settings()->setBalanceLayout(sp_balance_layout);
|
||
app->settings()->save();
|
||
}
|
||
}
|
||
if (selected) ImGui::SetItemDefaultFocus();
|
||
}
|
||
ImGui::EndCombo();
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("Hotkey: Left/Right arrow keys to cycle Balance layouts");
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// --- Language row ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Language");
|
||
ImGui::SameLine(labelW);
|
||
ImGui::SetNextItemWidth(inputW);
|
||
if (ImGui::Combo("##Language", &sp_language_index, lang_names.data(),
|
||
static_cast<int>(lang_names.size()))) {
|
||
auto it = languages.begin();
|
||
std::advance(it, sp_language_index);
|
||
i18n.loadLanguage(it->first);
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Interface language for the wallet UI");
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// --- Visual Effects (checkboxes + controls) ---
|
||
{
|
||
ImGui::PushFont(body2);
|
||
|
||
// Checkbox row 1: Low-spec
|
||
if (ImGui::Checkbox("Low-spec mode", &sp_low_spec_mode)) {
|
||
effects::setLowSpecMode(sp_low_spec_mode);
|
||
if (sp_low_spec_mode) {
|
||
s_lowSpecSnap.valid = true;
|
||
s_lowSpecSnap.acrylic_enabled = sp_acrylic_enabled;
|
||
s_lowSpecSnap.blur_amount = sp_blur_amount;
|
||
s_lowSpecSnap.ui_opacity = sp_ui_opacity;
|
||
s_lowSpecSnap.window_opacity = sp_window_opacity;
|
||
s_lowSpecSnap.theme_effects_enabled = sp_theme_effects_enabled;
|
||
s_lowSpecSnap.scanline_enabled = sp_scanline_enabled;
|
||
sp_acrylic_enabled = false;
|
||
sp_blur_amount = 0.0f;
|
||
sp_ui_opacity = 1.0f;
|
||
sp_window_opacity = 1.0f;
|
||
sp_theme_effects_enabled = false;
|
||
sp_scanline_enabled = false;
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(0.0f);
|
||
effects::ImGuiAcrylic::SetUIOpacity(1.0f);
|
||
effects::ThemeEffects::instance().setEnabled(false);
|
||
ConsoleTab::s_scanline_enabled = false;
|
||
} else if (s_lowSpecSnap.valid) {
|
||
sp_blur_amount = s_lowSpecSnap.blur_amount;
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
sp_ui_opacity = s_lowSpecSnap.ui_opacity;
|
||
sp_window_opacity = s_lowSpecSnap.window_opacity;
|
||
sp_theme_effects_enabled = s_lowSpecSnap.theme_effects_enabled;
|
||
sp_scanline_enabled = s_lowSpecSnap.scanline_enabled;
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
|
||
effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity);
|
||
effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled);
|
||
ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
|
||
s_lowSpecSnap.valid = false;
|
||
}
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Disable all heavy visual effects\nHotkey: Ctrl+Shift+Down");
|
||
|
||
// Simple background is lightweight — always interactive, even in low-spec mode
|
||
if (ImGui::Checkbox("Gradient bg", &sp_gradient_background)) {
|
||
schema::SkinManager::instance().setGradientMode(sp_gradient_background);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Use a gradient version of the theme background image\nHotkey: Ctrl+Up");
|
||
|
||
ImGui::BeginDisabled(sp_low_spec_mode);
|
||
|
||
// Checkbox row 2: Console scanline | Theme effects
|
||
if (ImGui::Checkbox("Console scanline", &sp_scanline_enabled)) {
|
||
ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
|
||
app->settings()->setScanlineEnabled(sp_scanline_enabled);
|
||
app->settings()->save();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("CRT scanline effect in console");
|
||
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
if (ImGui::Checkbox("Theme effects", &sp_theme_effects_enabled)) {
|
||
effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Shimmer, glow, hue-cycling per theme");
|
||
|
||
// Acrylic blur slider (label above)
|
||
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
||
contentW);
|
||
ImGui::TextUnformatted("Acrylic");
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char blur_fmt[16];
|
||
if (sp_blur_amount < 0.01f)
|
||
snprintf(blur_fmt, sizeof(blur_fmt), "Off");
|
||
else
|
||
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f);
|
||
if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
if (sp_blur_amount > 0.0f && sp_blur_amount < 0.15f) sp_blur_amount = 0.0f;
|
||
sp_acrylic_enabled = (sp_blur_amount > 0.001f);
|
||
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
|
||
}
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Blur amount (0%% = off, 100%% = maximum)");
|
||
|
||
// Noise opacity slider (label above)
|
||
ImGui::TextUnformatted("Noise");
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char noise_fmt[16];
|
||
if (sp_noise_opacity < 0.01f)
|
||
snprintf(noise_fmt, sizeof(noise_fmt), "Off");
|
||
else
|
||
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
effects::ImGuiAcrylic::SetNoiseOpacity(sp_noise_opacity);
|
||
}
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Grain texture intensity (0%% = off, 100%% = maximum)");
|
||
|
||
// UI Opacity slider (label above)
|
||
ImGui::TextUnformatted("UI Opacity");
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char uiop_fmt[16];
|
||
snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", sp_ui_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##UIOpacity", &sp_ui_opacity, 0.3f, 1.0f, uiop_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity);
|
||
}
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Card and sidebar opacity (100%% = fully opaque, lower = more see-through)");
|
||
|
||
// Window Opacity slider (label above)
|
||
ImGui::TextUnformatted("Window Opacity");
|
||
ImGui::SetNextItemWidth(ctrlW);
|
||
{
|
||
char winop_fmt[16];
|
||
snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", sp_window_opacity * 100.0f);
|
||
if (ImGui::SliderFloat("##WindowOpacity", &sp_window_opacity, 0.3f, 1.0f, winop_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp)) {
|
||
}
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Background opacity (lower = desktop visible through window)");
|
||
|
||
ImGui::EndDisabled(); // low-spec
|
||
ImGui::PopFont();
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// Font Scale slider (always enabled, not affected by low-spec)
|
||
// ============================================================
|
||
{
|
||
ImGui::PushFont(body2);
|
||
ImGui::Spacing();
|
||
ImGui::TextUnformatted("Font Scale");
|
||
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
|
||
availWidth - pad * 2);
|
||
ImGui::SetNextItemWidth(fontSliderW);
|
||
float prev_font_scale = sp_font_scale;
|
||
{
|
||
char fs_fmt[16];
|
||
snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", sp_font_scale);
|
||
ImGui::SliderFloat("##FontScale", &sp_font_scale, 1.0f, 1.5f, fs_fmt,
|
||
ImGuiSliderFlags_AlwaysClamp);
|
||
}
|
||
// Smooth continuous scaling while dragging.
|
||
// Visual scaling uses FontScaleMain (no atlas rebuild),
|
||
// atlas rebuild is deferred to slider release for crisp text.
|
||
sp_font_scale = std::max(1.0f, std::min(1.5f, sp_font_scale));
|
||
if (sp_font_scale != prev_font_scale) {
|
||
// While dragging: update layout scale without atlas rebuild
|
||
Layout::setUserFontScaleVisual(sp_font_scale);
|
||
}
|
||
if (ImGui::IsItemDeactivatedAfterEdit()) {
|
||
// On release: rebuild font atlas at final size
|
||
Layout::setUserFontScale(sp_font_scale);
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Scale all text and UI (1.0x = default, up to 1.5x).");
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
// Bottom padding
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
// Draw glass panel behind content (auto-sized)
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// WALLET — card (Keys, Backup, Tools, Maintenance, Node/RPC)
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "WALLET");
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
float contentW = availWidth - pad * 2;
|
||
bool wideBtns = availWidth >= S.drawElement("components.settings-page", "compact-breakpoint").size;
|
||
|
||
// Content-aware button sizing: uniform per-row width based on widest label
|
||
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(130.0f);
|
||
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
|
||
auto rowBtnW = [&](std::initializer_list<const char*> labels) -> float {
|
||
float maxTextW = 0;
|
||
for (auto* l : labels) maxTextW = std::max(maxTextW, ImGui::CalcTextSize(l).x);
|
||
return std::max(minBtnW, maxTextW + btnPad * 2);
|
||
};
|
||
|
||
// Row 1 — Tools & Actions
|
||
{
|
||
float bw = rowBtnW({"Address Book...", "Validate Address...", "Request Payment...", "Shield Mining...", "Merge to Address...", "Clear Z-Tx History"});
|
||
if (TactileButton("Address Book...", ImVec2(bw, 0), S.resolveFont("button")))
|
||
AddressBookDialog::show();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Manage saved addresses for quick sending");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton("Validate Address...", ImVec2(bw, 0), S.resolveFont("button")))
|
||
ValidateAddressDialog::show();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Check if a DragonX address is valid");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton("Request Payment...", ImVec2(bw, 0), S.resolveFont("button")))
|
||
RequestPaymentDialog::show();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Generate a payment request with QR code");
|
||
if (wideBtns) ImGui::SameLine(0, Layout::spacingMd()); else ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
if (TactileButton("Shield Mining...", ImVec2(bw, 0), S.resolveFont("button")))
|
||
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Move transparent mining rewards to a shielded address");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton("Merge to Address...", ImVec2(bw, 0), S.resolveFont("button")))
|
||
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Consolidate multiple UTXOs into one address");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton("Clear Z-Tx History", ImVec2(bw, 0), S.resolveFont("button"))) {
|
||
std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json";
|
||
if (util::Platform::deleteFile(ztx_file))
|
||
Notifications::instance().success("Z-transaction history cleared");
|
||
else
|
||
Notifications::instance().info("No history file found");
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Delete locally cached z-transaction history");
|
||
}
|
||
|
||
// Thin divider
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
{
|
||
float divAlpha = S.drawElement("components.settings-page", "section-divider-alpha").opacity;
|
||
if (divAlpha <= 0.0f) divAlpha = 0.08f;
|
||
ImU32 baseDivCol = S.resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 20));
|
||
ImU32 divCol = material::ScaleAlpha(baseDivCol, divAlpha / 0.08f);
|
||
ImVec2 p = ImGui::GetCursorScreenPos();
|
||
dl->AddLine(ImVec2(p.x, p.y), ImVec2(p.x + contentW, p.y), divCol);
|
||
}
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit
|
||
{
|
||
float cbSpacing = Layout::spacingMd();
|
||
float fh = ImGui::GetFrameHeight();
|
||
float inner = ImGui::GetStyle().ItemInnerSpacing.x;
|
||
auto cbW = [&](const char* label) { return fh + inner + ImGui::CalcTextSize(label).x; };
|
||
|
||
float totalW = cbW("Save shielded tx history") + cbSpacing
|
||
+ cbW("Auto-shield") + cbSpacing
|
||
+ cbW("Use Tor") + cbSpacing
|
||
+ cbW("Keep daemon running") + cbSpacing
|
||
+ cbW("Stop external daemon");
|
||
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
|
||
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
|
||
float sp = cbSpacing * scale;
|
||
|
||
ImGui::Checkbox("Save shielded tx history", &sp_save_ztxs);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Store z-address transaction history locally for faster loading");
|
||
ImGui::SameLine(0, sp);
|
||
ImGui::Checkbox("Auto-shield", &sp_auto_shield);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Automatically move transparent balance to shielded addresses for privacy");
|
||
ImGui::SameLine(0, sp);
|
||
ImGui::Checkbox("Use Tor", &sp_use_tor);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Route daemon connections through the Tor network for anonymity");
|
||
ImGui::SameLine(0, sp);
|
||
if (ImGui::Checkbox("Keep daemon running", &sp_keep_daemon_running)) {
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("Daemon will still stop when running the setup wizard");
|
||
ImGui::SameLine(0, sp);
|
||
if (ImGui::Checkbox("Stop external daemon", &sp_stop_external_daemon)) {
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered())
|
||
ImGui::SetTooltip("Applies when connecting to a daemon\nyou started outside this wallet");
|
||
|
||
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
||
}
|
||
|
||
// Bottom row — Keys & Data left-aligned, Setup Wizard right-aligned
|
||
{
|
||
const char* r1[] = {"Import Key...", "Export Key...", "Export All...", "Backup...", "Export CSV..."};
|
||
const char* t1[] = {
|
||
"Import a private key (zkey or tkey) into this wallet",
|
||
"Export the private key for the selected address",
|
||
"Export all private keys to a file",
|
||
"Create a backup of your wallet.dat file",
|
||
"Export transaction history as a CSV spreadsheet"
|
||
};
|
||
const char* wizLabel = "Run Setup Wizard...";
|
||
float sp = Layout::spacingSm();
|
||
ImFont* btnFont = S.resolveFont("button");
|
||
|
||
// Measure natural widths
|
||
float btnPadX = btnPad * 2;
|
||
float naturalW = 0;
|
||
for (int i = 0; i < 5; i++)
|
||
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
|
||
float wizW = ImGui::CalcTextSize(wizLabel).x + btnPadX;
|
||
float totalW = naturalW + wizW + sp * 6; // 5 gaps between data btns + 1 gap before wizard
|
||
|
||
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
|
||
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
|
||
|
||
float scaledSp = sp * scale;
|
||
|
||
if (TactileButton(r1[0], ImVec2(0, 0), btnFont))
|
||
app->showImportKeyDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[0]);
|
||
ImGui::SameLine(0, scaledSp);
|
||
if (TactileButton(r1[1], ImVec2(0, 0), btnFont))
|
||
app->showExportKeyDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[1]);
|
||
ImGui::SameLine(0, scaledSp);
|
||
if (TactileButton(r1[2], ImVec2(0, 0), btnFont))
|
||
ExportAllKeysDialog::show();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[2]);
|
||
ImGui::SameLine(0, scaledSp);
|
||
if (TactileButton(r1[3], ImVec2(0, 0), btnFont))
|
||
app->showBackupDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[3]);
|
||
ImGui::SameLine(0, scaledSp);
|
||
if (TactileButton(r1[4], ImVec2(0, 0), btnFont))
|
||
ExportTransactionsDialog::show();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[4]);
|
||
|
||
// Right-align Setup Wizard
|
||
float curX = ImGui::GetCursorScreenPos().x;
|
||
float wizBtnW = ImGui::CalcTextSize(wizLabel).x + btnPadX;
|
||
if (scale < 1.0f) wizBtnW *= scale;
|
||
float rightEdge = cardMin.x + availWidth - pad;
|
||
float wizX = rightEdge - wizBtnW;
|
||
if (wizX > curX) {
|
||
ImGui::SameLine(0, 0);
|
||
ImGui::SetCursorScreenPos(ImVec2(wizX, ImGui::GetCursorScreenPos().y));
|
||
} else {
|
||
ImGui::SameLine(0, scaledSp);
|
||
}
|
||
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
|
||
app->restartWizard();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Re-run the initial setup wizard\nDaemon will be restarted");
|
||
|
||
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// NODE & SECURITY — card
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NODE & SECURITY");
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
float contentW = availWidth - pad * 2;
|
||
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(130.0f);
|
||
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
|
||
auto rowBtnW = [&](std::initializer_list<const char*> labels) -> float {
|
||
float maxTextW = 0;
|
||
for (auto* l : labels) maxTextW = std::max(maxTextW, ImGui::CalcTextSize(l).x);
|
||
return std::max(minBtnW, maxTextW + btnPad * 2);
|
||
};
|
||
|
||
// --- NODE (left) + SECURITY (right) side by side ---
|
||
{
|
||
float colGap = Layout::spacingLg();
|
||
float leftColW = (contentW - colGap) * 0.6f;
|
||
float rightColW = (contentW - colGap) * 0.4f;
|
||
ImVec2 sectionOrigin = ImGui::GetCursorScreenPos();
|
||
float leftX = sectionOrigin.x;
|
||
float rightX = sectionOrigin.x + leftColW + colGap;
|
||
|
||
// ---- LEFT COLUMN: NODE ----
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, sectionOrigin.y));
|
||
ImGui::PushClipRect(ImVec2(leftX, sectionOrigin.y),
|
||
ImVec2(leftX + leftColW, sectionOrigin.y + 9999), true);
|
||
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NODE");
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
{
|
||
ImFont* body2Info = S.resolveFont("body2");
|
||
if (!body2Info) body2Info = Type().body2();
|
||
ImGui::PushFont(body2Info);
|
||
|
||
float rpcLblW = std::max(
|
||
S.drawElement("components.settings-page", "rpc-label-min-width").size,
|
||
std::min(leftColW * 0.35f, S.drawElement("components.settings-page", "rpc-label-width").size * hs));
|
||
|
||
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
|
||
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
|
||
|
||
// Data Dir + Wallet Size — same line with "Label: value" format
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||
ImGui::AlignTextToFramePadding();
|
||
{
|
||
// Measure the wallet size string first
|
||
std::string size_str = (wallet_size > 0) ? util::Platform::formatFileSize(wallet_size) : "Not found";
|
||
std::string dirPath = util::Platform::getDragonXDataDir();
|
||
|
||
// Calculate space taken by "Wallet Size: <size>" on the right
|
||
float walletSizeLabelW = ImGui::CalcTextSize("Wallet Size: ").x;
|
||
float walletSizeValW = ImGui::CalcTextSize(size_str.c_str()).x;
|
||
float walletSizeTotalW = walletSizeLabelW + walletSizeValW + Layout::spacingLg();
|
||
|
||
// Available space for "Data Dir: <path>"
|
||
float dataDirLabelW = ImGui::CalcTextSize("Data Dir: ").x;
|
||
float availForPath = leftColW - walletSizeTotalW - dataDirLabelW;
|
||
|
||
ImGui::TextUnformatted("Data Dir: ");
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str());
|
||
ImVec4 linkCol = ImGui::ColorConvertU32ToFloat4(Primary());
|
||
ImVec4 linkHoverCol = linkCol;
|
||
linkHoverCol.w = std::min(1.0f, linkCol.w + 0.2f);
|
||
bool hovered = false;
|
||
if (pathSize.x > availForPath && availForPath > 0) {
|
||
float scale = availForPath / pathSize.x;
|
||
float condensedSize = body2Info->LegacySize * std::max(scale, 0.65f);
|
||
ImGui::SetWindowFontScale(condensedSize / body2Info->LegacySize);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextColored(linkCol, "%s", dirPath.c_str());
|
||
hovered = ImGui::IsItemHovered();
|
||
if (hovered) {
|
||
ImVec2 tMin = ImGui::GetItemRectMin();
|
||
ImVec2 tMax = ImGui::GetItemRectMax();
|
||
dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol));
|
||
}
|
||
ImGui::SetWindowFontScale(1.0f);
|
||
} else {
|
||
ImGui::TextColored(linkCol, "%s", dirPath.c_str());
|
||
hovered = ImGui::IsItemHovered();
|
||
if (hovered) {
|
||
ImVec2 tMin = ImGui::GetItemRectMin();
|
||
ImVec2 tMax = ImGui::GetItemRectMax();
|
||
dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol));
|
||
}
|
||
}
|
||
if (hovered) {
|
||
ImGui::SetTooltip("Click to open in file explorer");
|
||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||
}
|
||
if (ImGui::IsItemClicked())
|
||
util::Platform::openFolder(dirPath);
|
||
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
ImGui::TextUnformatted("Wallet Size: ");
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
if (wallet_size > 0) {
|
||
ImGui::TextUnformatted(size_str.c_str());
|
||
} else {
|
||
ImGui::TextDisabled("Not found");
|
||
}
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// RPC connection — two columns: (Host | Username) and (Port | Password)
|
||
float rpcHalfW = (leftColW - Layout::spacingMd()) * 0.5f;
|
||
float rpcHalfLblW = std::min(rpcLblW, rpcHalfW * 0.4f);
|
||
float rpcHalfInputW = std::max(40.0f, rpcHalfW - rpcHalfLblW);
|
||
float rpcRightColX = leftX + rpcHalfW + Layout::spacingMd();
|
||
|
||
// Row 1: RPC Host + Username
|
||
float row1Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, row1Y));
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("RPC Host");
|
||
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
|
||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||
ImGui::InputText("##RPCHost", sp_rpc_host, sizeof(sp_rpc_host));
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Hostname of the DragonX daemon");
|
||
|
||
float afterRow1Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row1Y));
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Username");
|
||
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
|
||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||
ImGui::InputText("##RPCUser", sp_rpc_user, sizeof(sp_rpc_user));
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("RPC authentication username");
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y)));
|
||
|
||
// Row 2: Port + Password
|
||
float row2Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, row2Y));
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Port");
|
||
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
|
||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||
ImGui::InputText("##RPCPort", sp_rpc_port, sizeof(sp_rpc_port));
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Port for daemon RPC connections");
|
||
|
||
float afterRow2Y = ImGui::GetCursorScreenPos().y;
|
||
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row2Y));
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Password");
|
||
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
|
||
ImGui::SetNextItemWidth(rpcHalfInputW);
|
||
ImGui::InputText("##RPCPassword", sp_rpc_password, sizeof(sp_rpc_password),
|
||
ImGuiInputTextFlags_Password);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("RPC authentication password");
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y)));
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||
"Auto-detected from DRAGONX.conf");
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
// Node maintenance buttons
|
||
{
|
||
ImFont* btnFont = S.resolveFont("button");
|
||
float nodeBtnW;
|
||
{
|
||
if (btnFont) ImGui::PushFont(btnFont);
|
||
nodeBtnW = rowBtnW({"Test Connection", "Rescan Blockchain"});
|
||
if (btnFont) ImGui::PopFont(/* btnFont */);
|
||
}
|
||
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
|
||
ImGui::BeginDisabled(!app->isConnected());
|
||
if (TactileButton("Test Connection", ImVec2(nodeBtnW, 0), btnFont)) {
|
||
if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
|
||
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
||
try {
|
||
rpc->call("getinfo");
|
||
return []() {
|
||
Notifications::instance().success("RPC connection OK");
|
||
};
|
||
} catch (const std::exception& e) {
|
||
std::string err = e.what();
|
||
return [err]() {
|
||
Notifications::instance().error("RPC error: " + err);
|
||
};
|
||
}
|
||
});
|
||
} else {
|
||
Notifications::instance().warning("Not connected to daemon");
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Verify the RPC connection to the daemon");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton("Rescan Blockchain", ImVec2(nodeBtnW, 0), btnFont)) {
|
||
if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
|
||
Notifications::instance().info("Starting blockchain rescan...");
|
||
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
|
||
try {
|
||
rpc->call("rescanblockchain", {0});
|
||
return []() {
|
||
Notifications::instance().success("Blockchain rescan started");
|
||
};
|
||
} catch (const std::exception& e) {
|
||
std::string err = e.what();
|
||
return [err]() {
|
||
Notifications::instance().error("Failed to start rescan: " + err);
|
||
};
|
||
}
|
||
});
|
||
} else {
|
||
Notifications::instance().warning("Not connected to daemon");
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Rescan the blockchain for missing transactions");
|
||
ImGui::EndDisabled();
|
||
}
|
||
|
||
ImGui::PopFont();
|
||
}
|
||
|
||
float leftBottom = ImGui::GetCursorScreenPos().y;
|
||
ImGui::PopClipRect();
|
||
|
||
// ---- RIGHT COLUMN: SECURITY ----
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, sectionOrigin.y));
|
||
ImGui::PushClipRect(ImVec2(rightX, sectionOrigin.y),
|
||
ImVec2(rightX + rightColW, sectionOrigin.y + 9999), true);
|
||
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SECURITY");
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
{
|
||
// Encrypt / Change passphrase / Lock buttons
|
||
bool isEncrypted = app->state().isEncrypted();
|
||
bool isLocked = app->state().isLocked();
|
||
|
||
float secBtnW = std::min(rowBtnW({"Encrypt Wallet", "Change Passphrase", "Lock Now"}), (rightColW - Layout::spacingMd()) * 0.5f);
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
|
||
if (!isEncrypted) {
|
||
if (TactileButton("Encrypt Wallet", ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||
app->showEncryptDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Encrypt wallet.dat with a passphrase");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "Not encrypted");
|
||
} else {
|
||
if (TactileButton("Change Passphrase", ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||
app->showChangePassphraseDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Change the wallet encryption passphrase");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (isLocked) {
|
||
ImGui::PushFont(Type().iconSmall());
|
||
ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), ICON_MD_LOCK);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), "Locked");
|
||
} else {
|
||
if (TactileButton("Lock Now", ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||
app->lockWallet();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Lock the wallet immediately");
|
||
ImGui::SameLine(0, Layout::spacingSm());
|
||
ImGui::PushFont(Type().iconSmall());
|
||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "Unlocked");
|
||
}
|
||
// Remove Encryption button
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y + Layout::spacingXs()));
|
||
if (TactileButton("Remove Encryption", ImVec2(secBtnW, 0), S.resolveFont("button")))
|
||
app->showDecryptDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Remove encryption and store wallet unprotected");
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// Auto-Lock timeout
|
||
{
|
||
float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f);
|
||
|
||
int timeout = app->settings()->getAutoLockTimeout();
|
||
const char* timeoutLabels[] = { "Off", "1 min", "5 min", "15 min", "30 min", "1 hour" };
|
||
int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 };
|
||
int selTimeout = 0;
|
||
for (int i = 0; i < 6; i++) {
|
||
if (timeoutValues[i] == timeout) { selTimeout = i; break; }
|
||
}
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
|
||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), "AUTO-LOCK");
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
|
||
ImGui::PushItemWidth(comboW);
|
||
if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) {
|
||
app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]);
|
||
app->settings()->save();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Lock wallet after this much inactivity");
|
||
ImGui::PopItemWidth();
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// PIN unlock section
|
||
bool isEncryptedPIN = app->state().isEncrypted();
|
||
if (isEncryptedPIN) {
|
||
bool hasPIN = app->hasPinVault();
|
||
float pinBtnW = std::min(rowBtnW({"Set PIN", "Change PIN", "Remove PIN"}), (rightColW - Layout::spacingSm()) * 0.5f);
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
|
||
if (!hasPIN) {
|
||
if (TactileButton("Set PIN", ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
||
app->showPinSetupDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Set a 4-8 digit PIN for quick unlock");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "Quick-unlock PIN");
|
||
} else {
|
||
if (TactileButton("Change PIN", ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
||
app->showPinChangeDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Change your unlock PIN");
|
||
ImGui::SameLine(0, Layout::spacingSm());
|
||
if (TactileButton("Remove PIN", ImVec2(pinBtnW, 0), S.resolveFont("button")))
|
||
app->showPinRemoveDialog();
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Remove PIN and require passphrase to unlock");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
ImGui::PushFont(Type().iconSmall());
|
||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "PIN");
|
||
}
|
||
} else {
|
||
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
|
||
ImGui::TextColored(ImVec4(1,1,1,0.3f), "Encrypt wallet first to enable PIN");
|
||
}
|
||
}
|
||
|
||
float rightBottom = ImGui::GetCursorScreenPos().y;
|
||
ImGui::PopClipRect();
|
||
|
||
// Advance cursor past both columns
|
||
float maxBottom = std::max(leftBottom, rightBottom);
|
||
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// EXPLORER & OPTIONS — full-width card
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "EXPLORER");
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
float contentW = availWidth - pad * 2;
|
||
ImGui::PushFont(body2);
|
||
|
||
// Row 1: Transaction URL | Address URL (side-by-side)
|
||
float halfW = (contentW - Layout::spacingLg()) * 0.5f;
|
||
float lblTxW = ImGui::CalcTextSize("Transaction URL").x + Layout::spacingXs();
|
||
float lblAddrW = ImGui::CalcTextSize("Address URL").x + Layout::spacingXs();
|
||
float inputTxW = std::max(80.0f, halfW - lblTxW);
|
||
float inputAddrW = std::max(80.0f, halfW - lblAddrW);
|
||
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Transaction URL");
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImGui::SetNextItemWidth(inputTxW);
|
||
ImGui::InputText("##TxExplorer", sp_tx_explorer, sizeof(sp_tx_explorer));
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Base URL for viewing transactions in a block explorer");
|
||
ImGui::SameLine(pad + halfW + Layout::spacingLg());
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Address URL");
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
ImGui::SetNextItemWidth(inputAddrW);
|
||
ImGui::InputText("##AddrExplorer", sp_addr_explorer, sizeof(sp_addr_explorer));
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Base URL for viewing addresses in a block explorer");
|
||
|
||
ImGui::PopFont();
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
// Row 2: Checkboxes + Block Explorer button (on one line)
|
||
ImGui::Checkbox("Allow custom transaction fees", &sp_allow_custom_fees);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Enable manual fee entry when sending transactions");
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
ImGui::Checkbox("Fetch price data from CoinGecko", &sp_fetch_prices);
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Retrieve DRGX market prices from CoinGecko API");
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
if (TactileButton("Block Explorer", ImVec2(0, 0), S.resolveFont("button"))) {
|
||
util::Platform::openUrl("https://explorer.dragonx.is");
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Open the DragonX block explorer in your browser");
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// ABOUT — card
|
||
// ====================================================================
|
||
{
|
||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ABOUT");
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
float contentW = availWidth - pad * 2;
|
||
|
||
// App name + version on same line
|
||
ImGui::PushFont(sub1);
|
||
ImGui::TextUnformatted(DRAGONX_APP_NAME);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
ImGui::PushFont(body2);
|
||
snprintf(buf, sizeof(buf), "v%s", DRAGONX_VERSION);
|
||
ImGui::TextUnformatted(buf);
|
||
ImGui::SameLine(0, Layout::spacingLg());
|
||
snprintf(buf, sizeof(buf), "ImGui %s", IMGUI_VERSION);
|
||
ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf);
|
||
ImGui::PopFont();
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
ImGui::PushFont(body2);
|
||
ImGui::PushTextWrapPos(cardMin.x + availWidth - pad);
|
||
ImGui::TextUnformatted(
|
||
"A shielded cryptocurrency wallet for DragonX (DRGX), "
|
||
"built with Dear ImGui for a lightweight, portable experience.");
|
||
ImGui::PopTextWrapPos();
|
||
ImGui::PopFont();
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
ImGui::PushFont(capFont);
|
||
ImGui::TextColored(ImVec4(1,1,1,0.5f), "Copyright 2024-2026 The Hush Developers | GPLv3 License");
|
||
ImGui::PopFont();
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||
|
||
// Buttons — consistent equal-width row
|
||
{
|
||
float aboutBtnW = (contentW - Layout::spacingMd() * 3) / 4.0f;
|
||
|
||
if (TactileButton("Website", ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||
util::Platform::openUrl("https://dragonx.is");
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Open the DragonX website");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton("Report Bug", ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||
util::Platform::openUrl("https://git.hush.is/dragonx/ObsidianDragon/issues");
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Report an issue on the project tracker");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton("Save Settings", ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||
saveSettingsPageState(app->settings());
|
||
Notifications::instance().success("Settings saved");
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Save all settings to disk");
|
||
ImGui::SameLine(0, Layout::spacingMd());
|
||
if (TactileButton("Reset to Defaults", ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
|
||
if (app->settings()) {
|
||
loadSettingsPageState(app->settings());
|
||
Notifications::instance().info("Settings reloaded from disk");
|
||
}
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Reload settings from disk (undo unsaved changes)");
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, gap));
|
||
|
||
// ====================================================================
|
||
// DEBUG LOGGING — collapsible card with glass styling
|
||
// ====================================================================
|
||
{
|
||
// Clickable header row
|
||
ImVec2 headerPos = ImGui::GetCursorScreenPos();
|
||
const char* arrow = sp_debug_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
|
||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f));
|
||
if (ImGui::Button("##DebugToggle", ImVec2(availWidth, ImGui::GetFrameHeight()))) {
|
||
sp_debug_expanded = !sp_debug_expanded;
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip(sp_debug_expanded ? "Collapse debug logging options" : "Expand debug logging options");
|
||
ImGui::PopStyleColor(3);
|
||
|
||
// Draw overline label + arrow on top of the invisible button
|
||
{
|
||
ImFont* ovFont = Type().overline();
|
||
float textY = headerPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f;
|
||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(headerPos.x, textY), OnSurfaceMedium(), "DEBUG LOGGING");
|
||
// Use the icon font for the expand/collapse arrow
|
||
ImFont* iconFont = Type().iconSmall();
|
||
if (!iconFont) iconFont = ovFont;
|
||
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
|
||
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(headerPos.x + availWidth - arrowW - pad, textY), OnSurfaceMedium(), arrow);
|
||
}
|
||
|
||
if (sp_debug_expanded) {
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||
|
||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||
dl->ChannelsSplit(2);
|
||
dl->ChannelsSetCurrent(1);
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad));
|
||
ImGui::Indent(pad);
|
||
|
||
ImGui::TextColored(ImVec4(1,1,1,0.5f),
|
||
"Select categories to enable daemon debug logging (-debug= flags).");
|
||
ImGui::TextColored(ImVec4(1,1,1,0.35f),
|
||
"Changes take effect after restarting the daemon.");
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// The 22 hushd debug categories
|
||
static const char* debugCats[] = {
|
||
"addrman", "alert", "bench", "coindb", "db", "estimatefee",
|
||
"http", "libevent", "lock", "mempool", "net",
|
||
"paymentdisclosure", "pow", "proxy", "prune", "rand",
|
||
"reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc"
|
||
};
|
||
static const char* debugTips[] = {
|
||
"Peer address tracking and management",
|
||
"Alert system messages",
|
||
"Benchmark timings for operations",
|
||
"Coin database read/write operations",
|
||
"Berkeley DB operations",
|
||
"Fee estimation algorithm",
|
||
"HTTP RPC server activity",
|
||
"Libevent networking library",
|
||
"Lock contention debugging",
|
||
"Transaction memory pool activity",
|
||
"Network connections and messages",
|
||
"Payment disclosure protocol",
|
||
"Proof-of-work mining activity",
|
||
"SOCKS5 proxy connections",
|
||
"Block pruning operations",
|
||
"Random number generation",
|
||
"Blockchain reindexing progress",
|
||
"RPC command processing",
|
||
"Coin selection for transactions",
|
||
"Tor integration and circuit info",
|
||
"ZeroMQ notification system",
|
||
"Shielded (z-addr) RPC operations"
|
||
};
|
||
constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]);
|
||
|
||
// Render as a 4-column grid of checkboxes
|
||
int columns = 4;
|
||
float dbgContentW = availWidth - pad * 2.0f;
|
||
float colW = dbgContentW / columns;
|
||
|
||
for (int i = 0; i < numCats; i++) {
|
||
if (i > 0 && (i % columns) != 0) {
|
||
ImGui::SameLine(pad + colW * (i % columns));
|
||
}
|
||
bool enabled = sp_debug_categories.count(debugCats[i]) > 0;
|
||
if (ImGui::Checkbox(debugCats[i], &enabled)) {
|
||
if (enabled) {
|
||
sp_debug_categories.insert(debugCats[i]);
|
||
} else {
|
||
sp_debug_categories.erase(debugCats[i]);
|
||
}
|
||
sp_debug_cats_dirty = true;
|
||
saveSettingsPageState(app->settings());
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", debugTips[i]);
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||
|
||
// "Restart daemon" button — only active when categories changed
|
||
if (sp_debug_cats_dirty) {
|
||
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 218, 0, 255));
|
||
ImFont* iconFont = Type().iconSmall();
|
||
if (iconFont) {
|
||
ImGui::PushFont(iconFont);
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted(ICON_MD_INFO);
|
||
ImGui::PopFont();
|
||
ImGui::SameLine(0, Layout::spacingXs());
|
||
}
|
||
ImGui::AlignTextToFramePadding();
|
||
ImGui::TextUnformatted("Debug categories changed — restart daemon to apply");
|
||
ImGui::PopStyleColor();
|
||
ImGui::SameLine();
|
||
if (TactileButton("Restart daemon", ImVec2(0, 0), S.resolveFont("button"))) {
|
||
sp_debug_cats_dirty = false;
|
||
app->restartDaemon();
|
||
}
|
||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Restart the daemon to apply debug logging changes");
|
||
}
|
||
|
||
ImGui::Dummy(ImVec2(0, bottomPad));
|
||
ImGui::Unindent(pad);
|
||
|
||
ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y);
|
||
dl->ChannelsSetCurrent(0);
|
||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||
dl->ChannelsMerge();
|
||
|
||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||
}
|
||
}
|
||
|
||
// --- Shader-based scroll fade: unbind (restore ImGui's default shader) ---
|
||
if (fadeH > 0.0f && !sp_low_spec_mode && s_fadeShader.ready) {
|
||
effects::ScrollFadeShader::addUnbind(dl);
|
||
}
|
||
|
||
// --- Vertex-based alpha fade for ForegroundDrawList theme effects ---
|
||
// DrawGlassPanel draws rainbow borders, shimmer, specular glare, and
|
||
// edge traces on the ForegroundDrawList which bypasses the shader.
|
||
// Apply the same fade boundaries via vertex alpha manipulation.
|
||
if (fadeH > 0.0f) {
|
||
int fgVtxEnd = fgDL->VtxBuffer.Size;
|
||
float safeTop = settingsFadeTopY + settingsFadeZoneTop;
|
||
float safeBot = settingsFadeBottomY - settingsFadeZoneBot;
|
||
for (int vi = fgVtxStart; vi < fgVtxEnd; vi++) {
|
||
ImDrawVert& v = fgDL->VtxBuffer[vi];
|
||
if (v.pos.y >= safeTop && v.pos.y <= safeBot)
|
||
continue;
|
||
float alpha = 1.0f;
|
||
if (settingsFadeZoneTop > 0.0f) {
|
||
float dTop = v.pos.y - settingsFadeTopY;
|
||
if (dTop < settingsFadeZoneTop)
|
||
alpha = std::min(alpha, std::max(0.0f, dTop / settingsFadeZoneTop));
|
||
}
|
||
if (settingsFadeZoneBot > 0.0f) {
|
||
float dBot = settingsFadeBottomY - v.pos.y;
|
||
if (dBot < settingsFadeZoneBot)
|
||
alpha = std::min(alpha, std::max(0.0f, dBot / settingsFadeZoneBot));
|
||
}
|
||
if (alpha < 1.0f) {
|
||
int a = (v.col >> IM_COL32_A_SHIFT) & 0xFF;
|
||
a = static_cast<int>(a * alpha);
|
||
v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast<ImU32>(a) << IM_COL32_A_SHIFT);
|
||
}
|
||
}
|
||
}
|
||
|
||
ImGui::EndChild(); // ##SettingsPageScroll
|
||
|
||
}
|
||
|
||
} // namespace ui
|
||
} // namespace dragonx
|