Files
ObsidianDragon/src/ui/pages/settings_page.cpp
2026-02-28 00:58:43 -06:00

1846 lines
90 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.014.0 continuous blur multiplier
static float sp_noise_opacity = 0.5f; // 0.01.0 multiplier
static float sp_ui_opacity = 1.0f; // 0.31.0 card/sidebar opacity
static float sp_window_opacity = 1.0f; // 0.31.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.03.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