Files
ObsidianDragon/src/ui/pages/settings_page.cpp
DanS 732d892d4d feat(lite): ObsidianDragonLite Network tab — server browser
A lite-wallet-only "Network" tab (full-node keeps the Peers tab; exactly one shows per variant)
to manage lightwalletd servers, replacing the basic selector that was in Settings.

- Card list of servers with per-server latency + status dot, DNS host + resolved IP, and an
  Official/Custom pill. Official DragonX servers get a glowing outline.
- Pick a server (Sticky) by clicking its card, or toggle "use a random server" (Random mode);
  selection applies immediately (App::rebuildLiteWallet(force=true) tears down + rebuilds the
  controller against the new server and resyncs — its dtor detaches the uninterruptible sync
  thread, so this doesn't block).
- Add custom servers; hide/unhide servers (persisted set, revealed by a "Show hidden" toggle).
- Latency/IP come from a new background probe (util/LiteServerProbe): libcurl CONNECT_ONLY does
  the TCP+TLS handshake (works for gRPC lightwalletd, no HTTP response needed), recording
  APPCONNECT_TIME as latency and CURLINFO_PRIMARY_IP. Auto-runs on tab open + a Refresh button.

Wiring: WalletUiSurface::LiteNetwork (gated !fullNodePagesAvailable) + NavPage::LiteNetwork in
the sidebar + app.cpp dispatch; settings gains a hidden-servers set; isOfficialLiteServer() added
to lite_connection_service. The Settings page lite-server selector + its plumbing are removed
(single source of truth = the tab).

Reuses the existing server model (LiteServerPreference, Sticky/Random, selectLiteServer) and UI
primitives (DrawGlassPanel, ThemeEffects glow, peers-tab ping-dot idiom). Unit-tested
(liteServerHost, isOfficialLiteServer) + an env-gated live probe (verified vs lite.dragonx.is:
online, latency, IP). Both variants + lite-backend build; suite passes; hygiene clean; GUI
smoke-launched without crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:09:27 -05:00

2488 lines
132 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 "../../wallet/lite_wallet_lifecycle_ui_adapter.h"
#include "../../wallet/lite_wallet_server_selection_adapter.h"
#include "../../wallet/lite_wallet_controller.h"
#include <sodium.h>
#include "../../util/logger.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/connection.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 "../windows/bootstrap_download_dialog.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <nlohmann/json.hpp>
#include <vector>
#include <set>
#include <filesystem>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstring>
namespace dragonx {
namespace ui {
using namespace material;
// Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID
static std::string TrId(const char* tr_key, const char* id) {
std::string s = TR(tr_key);
s += "##";
s += id;
return s;
}
// ============================================================================
// Settings state loaded from config::Settings on first render
// ============================================================================
struct LowSpecSnapshot {
bool valid = false;
bool acrylic_enabled;
float blur_amount;
float ui_opacity;
float window_opacity;
bool theme_effects_enabled;
bool scanline_enabled;
};
struct SettingsPageState {
bool initialized = false;
int language_index = 0;
bool save_ztxs = true;
bool allow_custom_fees = false;
bool auto_shield = false;
bool fetch_prices = true;
bool use_tor = false;
char rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST;
char rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT;
char rpc_user[64] = "";
char rpc_password[64] = "";
bool rpc_plaintext_remote = false;
char tx_explorer[256] = "https://explorer.dragonx.is/tx/";
char addr_explorer[256] = "https://explorer.dragonx.is/address/";
bool acrylic_enabled = true;
float blur_amount = 1.5f;
float noise_opacity = 0.5f;
float ui_opacity = 1.0f;
float window_opacity = 1.0f;
std::string balance_layout = "classic";
bool scanline_enabled = true;
bool theme_effects_enabled = true;
bool gradient_background = false;
bool low_spec_mode = false;
bool reduce_motion = false;
float font_scale = 1.0f;
LowSpecSnapshot low_spec_snapshot;
bool keep_daemon_running = false;
bool stop_external_daemon = false;
bool lite_lifecycle_expanded = false;
int lite_lifecycle_operation = 0;
char lite_wallet_path[256] = "";
char lite_lifecycle_passphrase[128] = "";
char lite_restore_seed[512] = "";
int lite_restore_birthday = 0;
int lite_restore_account = 0;
bool lite_restore_overwrite = false;
std::string lite_lifecycle_status;
std::string lite_lifecycle_summary;
// Backup & keys (only populated for an open lite wallet). lite_export_secret holds the
// revealed seed/private-keys backup and is SECRET: securely wiped on hide / new export /
// import. lite_import_key is the import input buffer (wiped right after submission).
std::string lite_export_secret;
std::string lite_export_label;
char lite_import_key[512] = "";
std::string lite_backup_status;
// Encryption passphrase inputs (SECRET: zeroed right after each action). lite_enc_pass is
// reused for Encrypt (unencrypted wallet) and Unlock (locked wallet); lite_dec_pass for Decrypt.
char lite_enc_pass[128] = "";
char lite_dec_pass[128] = "";
std::string lite_encryption_status;
bool mine_when_idle = false;
int mine_idle_delay = 120;
bool idle_thread_scaling = false;
int idle_threads_active = 0;
int idle_threads_idle = 0;
bool verbose_logging = false;
std::set<std::string> debug_categories;
bool debug_cats_dirty = false;
bool debug_expanded = false;
bool effects_expanded = false;
bool tools_expanded = false;
bool confirm_clear_ztx = false;
bool confirm_delete_blockchain = false;
effects::ScrollFadeShader fade_shader;
};
static SettingsPageState s_settingsState;
static wallet::LiteWalletLifecycleOperation liteLifecycleOperationFromPageState() {
switch (s_settingsState.lite_lifecycle_operation) {
case 1: return wallet::LiteWalletLifecycleOperation::OpenExisting;
case 2: return wallet::LiteWalletLifecycleOperation::RestoreFromSeed;
default: return wallet::LiteWalletLifecycleOperation::CreateNew;
}
}
static void evaluateLiteLifecycleRequestFromPageState(App* app) {
if (!app || !app->settings()) return;
wallet::LiteWalletLifecycleUiExecutionInput input;
input.capabilities = app->walletCapabilities();
input.settingsLoaded = true;
input.requirePersistedServerSelectionIntent = true;
input.ui.selectedServerDisplayReady = true;
input.ui.lifecycleUiOwnerReady = true;
input.ui.operationConfirmed = true;
input.ui.privateDataRedactionReady = true;
input.ui.syncPlannerFeedReady = true;
input.request.requestProvided = true;
input.request.operation = liteLifecycleOperationFromPageState();
switch (input.request.operation) {
case wallet::LiteWalletLifecycleOperation::CreateNew:
input.request.createRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
break;
case wallet::LiteWalletLifecycleOperation::OpenExisting:
input.request.openRequest.walletPath = s_settingsState.lite_wallet_path;
input.request.openRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
break;
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
input.request.restoreRequest.walletPath = s_settingsState.lite_wallet_path;
input.request.restoreRequest.seedPhrase = s_settingsState.lite_restore_seed;
input.request.restoreRequest.passphrase = s_settingsState.lite_lifecycle_passphrase;
input.request.restoreRequest.birthday = static_cast<unsigned long long>(std::max(0, s_settingsState.lite_restore_birthday));
input.request.restoreRequest.account = static_cast<unsigned long long>(std::max(0, s_settingsState.lite_restore_account));
input.request.restoreRequest.overwrite = s_settingsState.lite_restore_overwrite;
break;
}
// Wipe ALL secret material when leaving this function, on every path (real execution,
// validation-only fallback, or early return): the UI char buffers AND the std::string
// copies inside `input.request.*Request`. The controller wipes its own by-value request
// copy, but these page-local copies are separate; leaving them would defeat the wipe.
struct LiteSecretScrubber {
wallet::LiteWalletLifecycleUiExecutionInput& in;
~LiteSecretScrubber() {
sodium_memzero(s_settingsState.lite_lifecycle_passphrase,
sizeof(s_settingsState.lite_lifecycle_passphrase));
sodium_memzero(s_settingsState.lite_restore_seed,
sizeof(s_settingsState.lite_restore_seed));
wallet::secureWipeLiteSecret(in.request.createRequest.passphrase);
wallet::secureWipeLiteSecret(in.request.openRequest.passphrase);
wallet::secureWipeLiteSecret(in.request.restoreRequest.seedPhrase);
wallet::secureWipeLiteSecret(in.request.restoreRequest.passphrase);
}
} liteSecretScrubber{input};
// When a linked lite backend is present, execute the operation for real through the
// App-owned controller. Otherwise fall back to the validation-only adapter.
if (auto* lite = app->liteWallet()) {
wallet::LiteWalletLifecycleResult result;
switch (input.request.operation) {
case wallet::LiteWalletLifecycleOperation::CreateNew:
result = lite->createWallet(input.request.createRequest);
break;
case wallet::LiteWalletLifecycleOperation::OpenExisting:
result = lite->openWallet(input.request.openRequest);
break;
case wallet::LiteWalletLifecycleOperation::RestoreFromSeed:
result = lite->restoreWallet(input.request.restoreRequest);
break;
}
// (Secret wiping is handled unconditionally by liteSecretScrubber at function exit.)
s_settingsState.lite_lifecycle_summary = result.bridgeResponseRedacted;
if (result.walletReady) {
s_settingsState.lite_lifecycle_status = "Wallet ready";
} else {
s_settingsState.lite_lifecycle_status = result.error.empty()
? result.status.message
: result.error;
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
}
return;
}
const auto result = wallet::executeLiteWalletLifecycleUiRequest(*app->settings(), input);
s_settingsState.lite_lifecycle_summary = result.requestSummaryRedacted;
if (result.ok) {
s_settingsState.lite_lifecycle_status = "Ready";
} else {
s_settingsState.lite_lifecycle_status = result.error.empty()
? wallet::liteWalletLifecycleUiExecutionStatusName(result.status)
: result.error;
Notifications::instance().warning(s_settingsState.lite_lifecycle_status);
}
}
// (APPEARANCE card now uses ChannelsSplit like all other cards)
static void loadSettingsPageState(config::Settings* settings) {
if (!settings) return;
s_settingsState.save_ztxs = settings->getSaveZtxs();
s_settingsState.allow_custom_fees = settings->getAllowCustomFees();
s_settingsState.auto_shield = settings->getAutoShield();
s_settingsState.fetch_prices = settings->getFetchPrices();
s_settingsState.use_tor = settings->getUseTor();
strncpy(s_settingsState.tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(s_settingsState.tx_explorer) - 1);
strncpy(s_settingsState.addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(s_settingsState.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";
s_settingsState.language_index = 0;
int idx = 0;
for (const auto& lang : languages) {
if (lang.first == current_lang) {
s_settingsState.language_index = idx;
break;
}
idx++;
}
// Load blur amount directly from saved multiplier
s_settingsState.blur_amount = settings->getBlurMultiplier();
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
s_settingsState.ui_opacity = settings->getUIOpacity();
s_settingsState.window_opacity = settings->getWindowOpacity();
s_settingsState.noise_opacity = settings->getNoiseOpacity();
s_settingsState.gradient_background = settings->getGradientBackground();
s_settingsState.balance_layout = settings->getBalanceLayout();
s_settingsState.scanline_enabled = settings->getScanlineEnabled();
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.theme_effects_enabled = settings->getThemeEffectsEnabled();
s_settingsState.low_spec_mode = settings->getLowSpecMode();
effects::setLowSpecMode(s_settingsState.low_spec_mode);
s_settingsState.reduce_motion = settings->getReduceMotion();
s_settingsState.font_scale = settings->getFontScale();
Layout::setUserFontScale(s_settingsState.font_scale); // sync with Layout on load
s_settingsState.keep_daemon_running = settings->getKeepDaemonRunning();
s_settingsState.stop_external_daemon = settings->getStopExternalDaemon();
// Lite-server selection is managed entirely by the Network tab (not the Settings page).
s_settingsState.mine_when_idle = settings->getMineWhenIdle();
s_settingsState.mine_idle_delay = settings->getMineIdleDelay();
s_settingsState.idle_thread_scaling = settings->getIdleThreadScaling();
s_settingsState.idle_threads_active = settings->getIdleThreadsActive();
s_settingsState.idle_threads_idle = settings->getIdleThreadsIdle();
s_settingsState.verbose_logging = settings->getVerboseLogging();
s_settingsState.debug_categories = settings->getDebugCategories();
s_settingsState.debug_cats_dirty = false;
s_settingsState.rpc_plaintext_remote = rpc::Connection::usesPlaintextRemote(rpc::Connection::autoDetectConfig());
// Apply loaded visual effects settings
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
effects::ImGuiAcrylic::SetNoiseOpacity(s_settingsState.noise_opacity);
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
s_settingsState.initialized = true;
}
static void saveSettingsPageState(config::Settings* settings) {
if (!settings) return;
settings->setTheme(settings->getSkinId());
settings->setSaveZtxs(s_settingsState.save_ztxs);
settings->setAllowCustomFees(s_settingsState.allow_custom_fees);
settings->setAutoShield(s_settingsState.auto_shield);
settings->setFetchPrices(s_settingsState.fetch_prices);
settings->setUseTor(s_settingsState.use_tor);
settings->setTxExplorerUrl(s_settingsState.tx_explorer);
settings->setAddressExplorerUrl(s_settingsState.addr_explorer);
auto& i18n = util::I18n::instance();
const auto& languages = i18n.getAvailableLanguages();
auto it = languages.begin();
std::advance(it, s_settingsState.language_index);
if (it != languages.end()) {
settings->setLanguage(it->first);
}
// Visual effects settings
settings->setAcrylicEnabled(s_settingsState.acrylic_enabled);
settings->setAcrylicQuality(s_settingsState.blur_amount > 0.001f ? static_cast<int>(effects::AcrylicQuality::Low) : static_cast<int>(effects::AcrylicQuality::Off));
settings->setBlurMultiplier(s_settingsState.blur_amount);
settings->setUIOpacity(s_settingsState.ui_opacity);
settings->setWindowOpacity(s_settingsState.window_opacity);
settings->setNoiseOpacity(s_settingsState.noise_opacity);
settings->setGradientBackground(s_settingsState.gradient_background);
settings->setScanlineEnabled(s_settingsState.scanline_enabled);
settings->setThemeEffectsEnabled(s_settingsState.theme_effects_enabled);
settings->setLowSpecMode(s_settingsState.low_spec_mode);
settings->setReduceMotion(s_settingsState.reduce_motion);
settings->setFontScale(s_settingsState.font_scale);
settings->setKeepDaemonRunning(s_settingsState.keep_daemon_running);
settings->setStopExternalDaemon(s_settingsState.stop_external_daemon);
// Lite-server selection is owned by the Network tab; the Settings page no longer writes it.
settings->setMineWhenIdle(s_settingsState.mine_when_idle);
settings->setMineIdleDelay(s_settingsState.mine_idle_delay);
settings->setIdleThreadScaling(s_settingsState.idle_thread_scaling);
settings->setIdleThreadsActive(s_settingsState.idle_threads_active);
settings->setIdleThreadsIdle(s_settingsState.idle_threads_idle);
settings->setVerboseLogging(s_settingsState.verbose_logging);
settings->setDebugCategories(s_settingsState.debug_categories);
settings->save();
}
// ============================================================================
// Settings Page Renderer
// ============================================================================
void RenderSettingsPage(App* app) {
// Load settings state on first render
if (!s_settingsState.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 (s_settingsState.low_spec_mode != runtimeLowSpec) {
if (runtimeLowSpec) {
// Hotkey turned low-spec ON — save snapshot, override statics
s_settingsState.low_spec_snapshot.valid = true;
s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled;
s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount;
s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity;
s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity;
s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled;
s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.acrylic_enabled = false;
s_settingsState.blur_amount = 0.0f;
s_settingsState.ui_opacity = 1.0f;
s_settingsState.window_opacity = 1.0f;
s_settingsState.theme_effects_enabled = false;
s_settingsState.scanline_enabled = false;
} else if (s_settingsState.low_spec_snapshot.valid) {
// Hotkey turned low-spec OFF — restore snapshot
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity;
s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity;
s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled;
s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled;
s_settingsState.low_spec_snapshot.valid = false;
} else if (app->settings()) {
// No snapshot — read prefs from settings file
s_settingsState.blur_amount = app->settings()->getBlurMultiplier();
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
s_settingsState.ui_opacity = app->settings()->getUIOpacity();
s_settingsState.window_opacity = app->settings()->getWindowOpacity();
s_settingsState.theme_effects_enabled = app->settings()->getThemeEffectsEnabled();
s_settingsState.scanline_enabled = app->settings()->getScanlineEnabled();
}
s_settingsState.low_spec_mode = runtimeLowSpec;
}
bool runtimeThemeEffects = effects::ThemeEffects::instance().isEnabled();
if (s_settingsState.theme_effects_enabled != runtimeThemeEffects) {
s_settingsState.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);
// Widen labelW if any translated label is wider (prevents overflow)
{
ImFont* mf = Type().body2();
const char* labelKeys[] = {"theme", "balance_layout", "language", "acrylic", "noise",
"ui_opacity", "window_opacity", "font_scale",
"rpc_host", "rpc_port", "rpc_user", "rpc_pass",
"transaction_url", "address_url"};
for (const char* k : labelKeys) {
float tw = mf->CalcTextSizeA(mf->LegacySize, FLT_MAX, 0, TR(k)).x + Layout::spacingMd();
if (tw > labelW) labelW = tw;
}
}
// 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 && !s_settingsState.low_spec_mode && s_settingsState.fade_shader.init()) {
s_settingsState.fade_shader.fadeTopY = settingsFadeTopY;
s_settingsState.fade_shader.fadeBottomY = settingsFadeBottomY;
s_settingsState.fade_shader.fadeZoneTop = settingsFadeZoneTop;
s_settingsState.fade_shader.fadeZoneBottom = settingsFadeZoneBot;
s_settingsState.fade_shader.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();
// ====================================================================
// THEME & LANGUAGE — card (draw-first approach; avoids ChannelsSplit
// which breaks BeginCombo popup rendering in some ImGui versions)
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language"));
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 = s_settingsState.balance_layout;
for (const auto& l : layouts) {
if (l.id == s_settingsState.balance_layout) { balPreview = l.name; break; }
}
// --- Theme combo popup (shared between wide and narrow paths) ---
auto renderThemeComboPopup = [&]() {
ImGui::TextDisabled("%s", TR("settings_builtin"));
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("%s", TR("settings_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(TR("theme")).x + lblGap;
float lblLayoutW = ImGui::CalcTextSize(TR("balance_layout")).x + lblGap;
float lblLangW = ImGui::CalcTextSize(TR("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(TR("theme"));
ImGui::SameLine(0, lblGap);
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##Theme", active_preview.c_str())) {
renderThemeComboPopup();
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_theme_hotkey"));
ImGui::SameLine(0, comboGap);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("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 == s_settingsState.balance_layout);
if (ImGui::Selectable(l.name.c_str(), selected)) {
s_settingsState.balance_layout = l.id;
if (app->settings()) {
app->settings()->setBalanceLayout(s_settingsState.balance_layout);
app->settings()->save();
}
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_layout_hotkey"));
ImGui::SameLine(0, comboGap);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("language"));
ImGui::SameLine(0, lblGap);
ImGui::SetNextItemWidth(comboW);
if (ImGui::Combo("##Language", &s_settingsState.language_index, lang_names.data(),
static_cast<int>(lang_names.size()))) {
auto it = languages.begin();
std::advance(it, s_settingsState.language_index);
i18n.loadLanguage(it->first);
if (app->settings()) {
app->settings()->setLanguage(it->first);
app->settings()->save();
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_language"));
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
schema::SkinManager::instance().refresh();
Notifications::instance().info("Theme list refreshed");
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(TR("tt_scan_themes"),
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Font Scale slider (always visible) ---
{
ImGui::PushFont(body2);
ImGui::TextUnformatted(TR("font_scale"));
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW);
ImGui::SetNextItemWidth(fontSliderW);
s_settingsState.font_scale = Layout::userFontScale();
float prev_font_scale = s_settingsState.font_scale;
{
char fs_fmt[16];
snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", s_settingsState.font_scale);
ImGui::SliderFloat("##FontScale", &s_settingsState.font_scale, 1.0f, 1.5f, fs_fmt,
ImGuiSliderFlags_AlwaysClamp);
}
s_settingsState.font_scale = std::max(1.0f, std::min(1.5f,
std::round(s_settingsState.font_scale * 20.0f) / 20.0f));
if (s_settingsState.font_scale != prev_font_scale)
Layout::setUserFontScaleVisual(s_settingsState.font_scale);
if (ImGui::IsItemDeactivatedAfterEdit()) {
Layout::setUserFontScale(s_settingsState.font_scale);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Collapsible: Advanced Effects... ---
{
const char* arrow = s_settingsState.effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
ImGui::PushFont(body2);
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));
{
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button("##EffectsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) {
s_settingsState.effects_expanded = !s_settingsState.effects_expanded;
}
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = body2;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow);
}
ImGui::PopStyleColor(3);
ImGui::PopFont();
}
if (s_settingsState.effects_expanded) {
ImGui::PushFont(body2);
// Checkbox row: Low-spec | Console scanline | Theme effects | Gradient background
if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &s_settingsState.low_spec_mode)) {
effects::setLowSpecMode(s_settingsState.low_spec_mode);
if (s_settingsState.low_spec_mode) {
s_settingsState.low_spec_snapshot.valid = true;
s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled;
s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount;
s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity;
s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity;
s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled;
s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.acrylic_enabled = false;
s_settingsState.blur_amount = 0.0f;
s_settingsState.ui_opacity = 1.0f;
s_settingsState.window_opacity = 1.0f;
s_settingsState.theme_effects_enabled = false;
s_settingsState.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_settingsState.low_spec_snapshot.valid) {
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity;
s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity;
s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled;
s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled;
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.low_spec_snapshot.valid = false;
}
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_settingsState.gradient_background)) {
schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg"));
ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &s_settingsState.scanline_enabled)) {
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled);
app->settings()->save();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_scanline"));
ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("theme_effects", "effects").c_str(), &s_settingsState.theme_effects_enabled)) {
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects"));
// 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();
ImGui::TextUnformatted(TR("acrylic"));
float row1Y = ImGui::GetCursorScreenPos().y;
ImGui::SetNextItemWidth(ctrlW);
{
char blur_fmt[16];
if (s_settingsState.blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", s_settingsState.blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &s_settingsState.blur_amount, 0.0f, 4.0f, blur_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
if (s_settingsState.blur_amount > 0.0f && s_settingsState.blur_amount < 0.15f) s_settingsState.blur_amount = 0.0f;
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
float afterRow1Y = ImGui::GetCursorScreenPos().y;
float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y;
ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y - lblH));
ImGui::TextUnformatted(TR("noise"));
ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y));
ImGui::SetNextItemWidth(ctrlW);
{
char noise_fmt[16];
if (s_settingsState.noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_settingsState.noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &s_settingsState.noise_opacity, 0.0f, 1.0f, noise_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
effects::ImGuiAcrylic::SetNoiseOpacity(s_settingsState.noise_opacity);
saveSettingsPageState(app->settings());
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y));
ImGui::TextUnformatted(TR("ui_opacity"));
float row2Y = ImGui::GetCursorScreenPos().y;
ImGui::SetNextItemWidth(ctrlW);
{
char uiop_fmt[16];
snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", s_settingsState.ui_opacity * 100.0f);
if (ImGui::SliderFloat("##UIOpacity", &s_settingsState.ui_opacity, 0.3f, 1.0f, uiop_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
saveSettingsPageState(app->settings());
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
float afterRow2Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH));
ImGui::TextUnformatted(TR("window_opacity"));
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y));
ImGui::SetNextItemWidth(ctrlW);
{
char winop_fmt[16];
snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", s_settingsState.window_opacity * 100.0f);
if (ImGui::SliderFloat("##WindowOpacity", &s_settingsState.window_opacity, 0.3f, 1.0f, winop_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
saveSettingsPageState(app->settings());
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity"));
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y));
ImGui::EndDisabled(); // low-spec
ImGui::PopFont();
} // s_settingsState.effects_expanded
} else {
// ============================================================
// Narrow: stacked combos + 2-column effects (original layout)
// ============================================================
// --- Theme row ---
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("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("%s", TR("tt_theme_hotkey"));
if (active_is_custom) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_custom_theme"));
}
ImGui::SameLine();
if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) {
schema::SkinManager::instance().refresh();
Notifications::instance().info("Theme list refreshed");
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(TR("tt_scan_themes"),
schema::SkinManager::getUserSkinsDirectory().c_str());
}
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// --- Balance Layout row ---
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("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 == s_settingsState.balance_layout);
if (ImGui::Selectable(l.name.c_str(), selected)) {
s_settingsState.balance_layout = l.id;
if (app->settings()) {
app->settings()->setBalanceLayout(s_settingsState.balance_layout);
app->settings()->save();
}
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_layout_hotkey"));
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// --- Language row ---
{
ImGui::PushFont(body2);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("language"));
ImGui::SameLine(labelW);
ImGui::SetNextItemWidth(inputW);
if (ImGui::Combo("##Language", &s_settingsState.language_index, lang_names.data(),
static_cast<int>(lang_names.size()))) {
auto it = languages.begin();
std::advance(it, s_settingsState.language_index);
i18n.loadLanguage(it->first);
if (app->settings()) {
app->settings()->setLanguage(it->first);
app->settings()->save();
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_language"));
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Font Scale slider (always visible) ---
{
ImGui::PushFont(body2);
ImGui::TextUnformatted(TR("font_scale"));
float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
availWidth - pad * 2);
ImGui::SetNextItemWidth(fontSliderW);
s_settingsState.font_scale = Layout::userFontScale();
float prev_font_scale = s_settingsState.font_scale;
{
char fs_fmt[16];
snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", s_settingsState.font_scale);
ImGui::SliderFloat("##FontScale", &s_settingsState.font_scale, 1.0f, 1.5f, fs_fmt,
ImGuiSliderFlags_AlwaysClamp);
}
s_settingsState.font_scale = std::max(1.0f, std::min(1.5f,
std::round(s_settingsState.font_scale * 20.0f) / 20.0f));
if (s_settingsState.font_scale != prev_font_scale)
Layout::setUserFontScaleVisual(s_settingsState.font_scale);
if (ImGui::IsItemDeactivatedAfterEdit()) {
Layout::setUserFontScale(s_settingsState.font_scale);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
ImGui::PopFont();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Collapsible: Advanced Effects... ---
{
const char* arrow = s_settingsState.effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
ImGui::PushFont(body2);
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));
{
float narrowContentW = availWidth - pad * 2;
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button("##EffectsToggleN", ImVec2(narrowContentW, ImGui::GetFrameHeight()))) {
s_settingsState.effects_expanded = !s_settingsState.effects_expanded;
}
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = body2;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + narrowContentW - arrowW, textY), OnSurfaceMedium(), arrow);
}
ImGui::PopStyleColor(3);
ImGui::PopFont();
}
if (s_settingsState.effects_expanded) {
ImGui::PushFont(body2);
if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &s_settingsState.low_spec_mode)) {
effects::setLowSpecMode(s_settingsState.low_spec_mode);
if (s_settingsState.low_spec_mode) {
s_settingsState.low_spec_snapshot.valid = true;
s_settingsState.low_spec_snapshot.acrylic_enabled = s_settingsState.acrylic_enabled;
s_settingsState.low_spec_snapshot.blur_amount = s_settingsState.blur_amount;
s_settingsState.low_spec_snapshot.ui_opacity = s_settingsState.ui_opacity;
s_settingsState.low_spec_snapshot.window_opacity = s_settingsState.window_opacity;
s_settingsState.low_spec_snapshot.theme_effects_enabled = s_settingsState.theme_effects_enabled;
s_settingsState.low_spec_snapshot.scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.acrylic_enabled = false;
s_settingsState.blur_amount = 0.0f;
s_settingsState.ui_opacity = 1.0f;
s_settingsState.window_opacity = 1.0f;
s_settingsState.theme_effects_enabled = false;
s_settingsState.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_settingsState.low_spec_snapshot.valid) {
s_settingsState.blur_amount = s_settingsState.low_spec_snapshot.blur_amount;
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
s_settingsState.ui_opacity = s_settingsState.low_spec_snapshot.ui_opacity;
s_settingsState.window_opacity = s_settingsState.low_spec_snapshot.window_opacity;
s_settingsState.theme_effects_enabled = s_settingsState.low_spec_snapshot.theme_effects_enabled;
s_settingsState.scanline_enabled = s_settingsState.low_spec_snapshot.scanline_enabled;
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
s_settingsState.low_spec_snapshot.valid = false;
}
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &s_settingsState.gradient_background)) {
schema::SkinManager::instance().setGradientMode(s_settingsState.gradient_background);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg_alt"));
if (ImGui::Checkbox(TrId("reduce_motion", "reduce_motion").c_str(), &s_settingsState.reduce_motion)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reduce_motion"));
ImGui::BeginDisabled(s_settingsState.low_spec_mode);
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &s_settingsState.scanline_enabled)) {
ConsoleTab::s_scanline_enabled = s_settingsState.scanline_enabled;
app->settings()->setScanlineEnabled(s_settingsState.scanline_enabled);
app->settings()->save();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_scanline"));
ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("theme_effects", "theme_fx").c_str(), &s_settingsState.theme_effects_enabled)) {
effects::ThemeEffects::instance().setEnabled(s_settingsState.theme_effects_enabled);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects"));
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
availWidth - pad * 2.0f);
ImGui::TextUnformatted(TR("acrylic"));
ImGui::SetNextItemWidth(ctrlW);
{
char blur_fmt[16];
if (s_settingsState.blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "%s", TR("slider_off"));
else
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", s_settingsState.blur_amount * 25.0f);
if (ImGui::SliderFloat("##AcrylicBlur", &s_settingsState.blur_amount, 0.0f, 4.0f, blur_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
if (s_settingsState.blur_amount > 0.0f && s_settingsState.blur_amount < 0.15f) s_settingsState.blur_amount = 0.0f;
s_settingsState.acrylic_enabled = (s_settingsState.blur_amount > 0.001f);
effects::ImGuiAcrylic::ApplyBlurAmount(s_settingsState.blur_amount);
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
ImGui::TextUnformatted(TR("noise"));
ImGui::SetNextItemWidth(ctrlW);
{
char noise_fmt[16];
if (s_settingsState.noise_opacity < 0.01f)
snprintf(noise_fmt, sizeof(noise_fmt), "%s", TR("slider_off"));
else
snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_settingsState.noise_opacity * 100.0f);
if (ImGui::SliderFloat("##NoiseOpacity", &s_settingsState.noise_opacity, 0.0f, 1.0f, noise_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
effects::ImGuiAcrylic::SetNoiseOpacity(s_settingsState.noise_opacity);
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
ImGui::TextUnformatted(TR("ui_opacity"));
ImGui::SetNextItemWidth(ctrlW);
{
char uiop_fmt[16];
snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", s_settingsState.ui_opacity * 100.0f);
if (ImGui::SliderFloat("##UIOpacity", &s_settingsState.ui_opacity, 0.3f, 1.0f, uiop_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
effects::ImGuiAcrylic::SetUIOpacity(s_settingsState.ui_opacity);
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
ImGui::TextUnformatted(TR("window_opacity"));
ImGui::SetNextItemWidth(ctrlW);
{
char winop_fmt[16];
snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", s_settingsState.window_opacity * 100.0f);
if (ImGui::SliderFloat("##WindowOpacity", &s_settingsState.window_opacity, 0.3f, 1.0f, winop_fmt,
ImGuiSliderFlags_AlwaysClamp)) {
}
}
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity"));
ImGui::EndDisabled(); // low-spec
ImGui::PopFont();
} // s_settingsState.effects_expanded
}
// 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 (privacy/daemon toggles + collapsible tools)
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("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;
// Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit
{
const bool showDaemonOptions = app->supportsFullNodeLifecycleActions();
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(TR("save_z_transactions")) + cbSpacing
+ cbW(TR("auto_shield")) + cbSpacing
+ cbW(TR("use_tor")) + cbSpacing;
if (showDaemonOptions) {
totalW += cbW(TR("keep_daemon")) + cbSpacing
+ cbW(TR("stop_external")) + cbSpacing;
}
totalW += cbW(TR("verbose_logging"));
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
float sp = cbSpacing * scale;
ImGui::Checkbox(TrId("save_z_transactions", "save_ztx").c_str(), &s_settingsState.save_ztxs);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_save_ztx"));
ImGui::SameLine(0, sp);
ImGui::Checkbox(TrId("auto_shield", "auto_shld").c_str(), &s_settingsState.auto_shield);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_auto_shield"));
ImGui::SameLine(0, sp);
ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tor"));
if (showDaemonOptions) {
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_keep_daemon"));
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &s_settingsState.stop_external_daemon)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_stop_external"));
}
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &s_settingsState.verbose_logging)) {
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_verbose"));
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Collapsible: Tools & Actions... ---
{
const char* arrow = s_settingsState.tools_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
ImGui::PushFont(body2);
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));
{
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button("##ToolsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) {
s_settingsState.tools_expanded = !s_settingsState.tools_expanded;
}
float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f;
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("tools_actions"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = body2;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow);
}
ImGui::PopStyleColor(3);
ImGui::PopFont();
}
if (s_settingsState.tools_expanded) {
float btnSpacing = Layout::spacingMd();
int btnsPerRow = (contentW >= 600.0f) ? 3 : 2;
float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow;
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f);
bw = std::max(minBtnW, bw);
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
AddressBookDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_address_book"));
ImGui::SameLine(0, btnSpacing);
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
ValidateAddressDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_validate"));
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton(TR("settings_request_payment"), ImVec2(bw, 0), S.resolveFont("button")))
RequestPaymentDialog::show();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_request_payment"));
if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); }
if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_shield_mining"));
ImGui::SameLine(0, btnSpacing);
if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button")))
ShieldDialog::show(ShieldDialog::Mode::MergeToAddress);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_merge"));
if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); }
if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) {
s_settingsState.confirm_clear_ztx = true;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_clear_ztx"));
}
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));
// ====================================================================
// BACKUP & DATA — card
// ====================================================================
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("backup_data"));
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 btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
{
const char* r1[] = {TR("settings_import_key"), TR("settings_export_key"), TR("settings_export_all"), TR("settings_backup"), TR("settings_export_csv")};
const char* t1[] = {
TR("tt_import_key"),
TR("tt_export_key"),
TR("tt_export_all"),
TR("tt_backup"),
TR("tt_export_csv")
};
const bool showFullNodeLifecycleActions = app->supportsFullNodeLifecycleActions();
const char* wizLabel = TR("setup_wizard");
const char* bsLabel = TR("download_bootstrap");
float sp = Layout::spacingSm();
ImFont* btnFont = S.resolveFont("button");
float btnPadX = btnPad * 2;
float naturalW = 0;
for (int i = 0; i < 5; i++)
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
float wizW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(wizLabel).x + btnPadX : 0.0f;
float bsW = showFullNodeLifecycleActions ? ImGui::CalcTextSize(bsLabel).x + btnPadX : 0.0f;
float totalW = naturalW + sp * 5;
if (showFullNodeLifecycleActions) totalW += wizW + bsW + sp * 2;
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]);
if (showFullNodeLifecycleActions) {
// Right-align Setup Wizard + Download Bootstrap
float framePadX2 = ImGui::GetStyle().FramePadding.x * 2.0f;
float curX = ImGui::GetCursorScreenPos().x;
float wizBtnW = ImGui::CalcTextSize(wizLabel).x + framePadX2;
float bsBtnW = ImGui::CalcTextSize(bsLabel).x + framePadX2;
float rightEdge = cardMin.x + availWidth - pad;
float rightGroupW = bsBtnW + scaledSp + wizBtnW;
float groupX = rightEdge - rightGroupW;
if (groupX > curX) {
ImGui::SameLine(0, 0);
ImGui::SetCursorScreenPos(ImVec2(groupX, ImGui::GetCursorScreenPos().y));
} else {
ImGui::SameLine(0, scaledSp);
}
if (TactileButton(bsLabel, ImVec2(0, 0), btnFont))
BootstrapDownloadDialog::show(app);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_download_bootstrap"));
ImGui::SameLine(0, scaledSp);
if (TactileButton(wizLabel, ImVec2(0, 0), btnFont))
app->restartWizard();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_wizard"));
}
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(), TR("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(), TR("node"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{
ImFont* body2Info = S.resolveFont("body2");
if (!body2Info) body2Info = Type().body2();
ImGui::PushFont(body2Info);
if (app->isLiteBuild()) {
float liteLabelW = std::min(leftColW * 0.35f, 132.0f);
float liteInputW = std::max(80.0f, leftColW - liteLabelW - Layout::spacingSm());
// Lite-server selection lives in the dedicated Network tab now.
Type().textColored(TypeStyle::Body2, OnSurfaceMedium(),
"Lite servers are managed in the Network tab.");
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (ImGui::Button("Lite wallet request##LiteLifecycleToggle", ImVec2(liteInputW, 0))) {
s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded;
}
if (s_settingsState.lite_lifecycle_expanded) {
const char* lifecycleLabels[] = {"Create", "Open", "Restore"};
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Action");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
if (ImGui::BeginCombo("##LiteLifecycleOperation",
lifecycleLabels[std::max(0, std::min(2, s_settingsState.lite_lifecycle_operation))])) {
for (int operationIndex = 0; operationIndex < 3; ++operationIndex) {
const bool selected = s_settingsState.lite_lifecycle_operation == operationIndex;
if (ImGui::Selectable(lifecycleLabels[operationIndex], selected)) {
s_settingsState.lite_lifecycle_operation = operationIndex;
s_settingsState.lite_lifecycle_status.clear();
s_settingsState.lite_lifecycle_summary.clear();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (s_settingsState.lite_lifecycle_operation == 1 ||
s_settingsState.lite_lifecycle_operation == 2) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Wallet");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path,
sizeof(s_settingsState.lite_wallet_path));
}
if (s_settingsState.lite_lifecycle_operation == 2) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Seed");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed,
sizeof(s_settingsState.lite_restore_seed),
ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Birthday");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Account");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::Checkbox("Overwrite##LiteRestoreOverwrite",
&s_settingsState.lite_restore_overwrite);
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Passphrase");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteLifecyclePassphrase",
s_settingsState.lite_lifecycle_passphrase,
sizeof(s_settingsState.lite_lifecycle_passphrase),
ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Validate##LiteLifecycleValidate", ImVec2(0, 0), S.resolveFont("button"))) {
evaluateLiteLifecycleRequestFromPageState(app);
}
if (!s_settingsState.lite_lifecycle_status.empty()) {
ImGui::SameLine(0, Layout::spacingSm());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
s_settingsState.lite_lifecycle_status.c_str());
}
if (!s_settingsState.lite_lifecycle_summary.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
s_settingsState.lite_lifecycle_summary.c_str());
}
}
// ---- Backup & keys (open wallet only) ----------------------------------
if (app->liteWallet() && app->liteWallet()->walletOpen()) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().text(TypeStyle::Body2, "Backup & keys");
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Show seed##LiteExportSeed", ImVec2(0, 0), S.resolveFont("button"))) {
auto r = app->liteWallet()->exportSeed();
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
if (r.ok) {
s_settingsState.lite_export_secret = r.seedPhrase;
s_settingsState.lite_export_label = "Seed phrase — write it down, never share it";
s_settingsState.lite_backup_status.clear();
} else {
s_settingsState.lite_export_label.clear();
s_settingsState.lite_backup_status = r.error;
}
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Show private keys##LiteExportKeys", ImVec2(0, 0), S.resolveFont("button"))) {
auto r = app->liteWallet()->exportPrivateKeys();
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
if (r.ok) {
s_settingsState.lite_export_secret = r.privateKeysJson;
s_settingsState.lite_export_label = "Private keys — anyone with these can spend your funds";
s_settingsState.lite_backup_status.clear();
} else {
s_settingsState.lite_export_label.clear();
s_settingsState.lite_backup_status = r.error;
}
wallet::secureWipeLiteSecret(r.privateKeysJson);
}
// Revealed secret: shown read-only (no extra copies), with copy + wipe.
if (!s_settingsState.lite_export_secret.empty()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
s_settingsState.lite_export_label.c_str());
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::PushTextWrapPos(0.0f);
ImGui::TextWrapped("%s", s_settingsState.lite_export_secret.c_str());
ImGui::PopTextWrapPos();
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Copy##LiteExportCopy", ImVec2(0, 0), S.resolveFont("button"))) {
ImGui::SetClipboardText(s_settingsState.lite_export_secret.c_str());
}
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton("Hide & wipe##LiteExportHide", ImVec2(0, 0), S.resolveFont("button"))) {
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
s_settingsState.lite_export_label.clear();
}
}
// Import a spending/viewing key (history appears after the next sync).
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Import key");
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
sizeof(s_settingsState.lite_import_key),
ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Import##LiteImportKeyBtn", ImVec2(0, 0), S.resolveFont("button"))) {
const auto r = app->liteWallet()->importKey(s_settingsState.lite_import_key);
sodium_memzero(s_settingsState.lite_import_key, sizeof(s_settingsState.lite_import_key));
s_settingsState.lite_backup_status =
r.ok ? "Key imported — run a sync to scan its history" : r.error;
}
if (!s_settingsState.lite_backup_status.empty()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
s_settingsState.lite_backup_status.c_str());
}
// ---- Security: passphrase encryption (encrypt / unlock / lock / decrypt) ----
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().text(TypeStyle::Body2, "Security");
const auto& wstate = app->getWalletState();
const float encLabelX = leftX - sectionOrigin.x + liteLabelW;
if (!wstate.isEncrypted()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Passphrase");
ImGui::SameLine(encLabelX);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass,
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Encrypt wallet##LiteEncrypt", ImVec2(0, 0), S.resolveFont("button"))) {
const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass);
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
s_settingsState.lite_encryption_status = r.ok ? "Wallet encrypted" : r.error;
}
} else {
if (wstate.isLocked()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Unlock");
ImGui::SameLine(encLabelX);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass,
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Unlock##LiteUnlock", ImVec2(0, 0), S.resolveFont("button"))) {
const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass);
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
s_settingsState.lite_encryption_status = ok ? "Wallet unlocked" : "Unlock failed";
}
} else {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Lock now##LiteLock", ImVec2(0, 0), S.resolveFont("button"))) {
s_settingsState.lite_encryption_status =
app->liteWallet()->lockWallet() ? "Wallet locked" : "Lock failed";
}
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted("Passphrase");
ImGui::SameLine(encLabelX);
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass,
sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password);
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton("Remove encryption##LiteDecrypt", ImVec2(0, 0), S.resolveFont("button"))) {
const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass);
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
s_settingsState.lite_encryption_status = r.ok ? "Encryption removed" : r.error;
}
}
if (!s_settingsState.lite_encryption_status.empty()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
s_settingsState.lite_encryption_status.c_str());
}
} else if (!s_settingsState.lite_export_secret.empty()) {
// Wallet closed while a backup/secret was revealed — don't leave it in memory.
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
s_settingsState.lite_export_label.clear();
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
}
} else {
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) : TR("settings_not_found");
std::string dirPath = util::Platform::getDragonXDataDir();
// Calculate space taken by "Wallet Size: <size>" on the right
float walletSizeLabelW = ImGui::CalcTextSize(TR("settings_wallet_size_label")).x + Layout::spacingXs();
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(TR("settings_data_dir")).x + Layout::spacingXs();
float availForPath = leftColW - walletSizeTotalW - dataDirLabelW;
ImGui::TextUnformatted(TR("settings_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("%s", TR("tt_open_dir"));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
if (ImGui::IsItemClicked())
util::Platform::openFolder(dirPath);
ImGui::SameLine(0, Layout::spacingLg());
ImGui::TextUnformatted(TR("settings_wallet_size_label"));
ImGui::SameLine(0, Layout::spacingXs());
if (wallet_size > 0) {
ImGui::TextUnformatted(size_str.c_str());
} else {
ImGui::TextDisabled("%s", TR("settings_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(TR("rpc_host"));
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_host"));
float afterRow1Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row1Y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("rpc_user"));
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_user"));
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(TR("rpc_port"));
ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_port"));
float afterRow2Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row2Y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("rpc_pass"));
ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password),
ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_pass"));
ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y)));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
TR("settings_auto_detected"));
if (s_settingsState.rpc_plaintext_remote) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::PushTextWrapPos(leftX + leftColW);
ImGui::TextWrapped("Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS.");
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Node maintenance buttons (full-node build only)
if (app->supportsFullNodeLifecycleActions()) {
ImFont* btnFont = S.resolveFont("button");
float nodeBtnW;
{
if (btnFont) ImGui::PushFont(btnFont);
nodeBtnW = rowBtnW({TR("test_connection"), TR("rescan")});
if (btnFont) ImGui::PopFont(/* btnFont */);
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TR("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::RPCClient::TraceScope trace("Settings / Test connection");
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("%s", TR("tt_test_conn"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("rescan"), ImVec2(nodeBtnW, 0), btnFont)) {
app->rescanBlockchain();
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan"));
ImGui::EndDisabled();
// Delete blockchain button (always available when using embedded daemon)
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y + Layout::spacingSm()));
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), btnFont)) {
s_settingsState.confirm_delete_blockchain = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_delete_blockchain"));
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(), TR("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({TR("settings_encrypt_wallet"), TR("settings_change_passphrase"), TR("settings_lock_now")}), (rightColW - Layout::spacingMd()) * 0.5f);
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
if (!isEncrypted) {
if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showEncryptDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_encrypt"));
ImGui::SameLine(0, Layout::spacingMd());
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_not_encrypted"));
} else {
if (TactileButton(TR("settings_change_passphrase"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showChangePassphraseDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_change_pass"));
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), "%s", TR("settings_locked"));
} else {
if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->lockWallet();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_lock"));
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), "%s", TR("settings_unlocked"));
}
// Remove Encryption button
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y + Layout::spacingXs()));
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showDecryptDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_encrypt"));
}
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[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") };
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(), TR("settings_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("%s", TR("tt_auto_lock"));
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({TR("settings_set_pin"), TR("settings_change_pin"), TR("settings_remove_pin")}), (rightColW - Layout::spacingSm()) * 0.5f);
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
if (!hasPIN) {
if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
app->showPinSetupDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_set_pin"));
ImGui::SameLine(0, Layout::spacingMd());
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_quick_unlock_pin"));
} else {
if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
app->showPinChangeDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_change_pin"));
ImGui::SameLine(0, Layout::spacingSm());
if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
app->showPinRemoveDialog();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_pin"));
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), "%s", TR("settings_pin_active"));
}
} else {
ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y));
ImGui::TextColored(ImVec4(1,1,1,0.3f), "%s", TR("settings_encrypt_first_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(), TR("explorer_section"));
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(TR("transaction_url"));
ImGui::SameLine(0, Layout::spacingXs());
ImGui::SetNextItemWidth(inputTxW);
ImGui::InputText("##TxExplorer", s_settingsState.tx_explorer, sizeof(s_settingsState.tx_explorer));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tx_url"));
ImGui::SameLine(pad + halfW + Layout::spacingLg());
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("address_url"));
ImGui::SameLine(0, Layout::spacingXs());
ImGui::SetNextItemWidth(inputAddrW);
ImGui::InputText("##AddrExplorer", s_settingsState.addr_explorer, sizeof(s_settingsState.addr_explorer));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_addr_url"));
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Row 2: Checkboxes + Block Explorer button (on one line)
ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &s_settingsState.allow_custom_fees);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_custom_fees"));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &s_settingsState.fetch_prices);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_fetch_prices"));
ImGui::SameLine(0, Layout::spacingLg());
if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://explorer.dragonx.is");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_block_explorer"));
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(), TR("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);
// Logo on the left side of the about card
ImTextureID logoTex = app->getLogoTexture();
float logoAreaW = 0;
if (logoTex != 0) {
float logoMaxH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(64.0f);
float logoH = logoMaxH;
float aspect = (app->getLogoHeight() > 0) ? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f;
float logoW = logoH * aspect;
ImVec2 logoPos = ImGui::GetCursorScreenPos();
dl->AddImage(logoTex,
ImVec2(logoPos.x, logoPos.y),
ImVec2(logoPos.x + logoW, logoPos.y + logoH));
logoAreaW = logoW + Layout::spacingLg();
ImGui::Indent(logoAreaW);
}
float contentW = availWidth - pad * 2 - logoAreaW;
// 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();
// Daemon version
{
const auto& st = app->state();
if (st.daemon_version > 0) {
int dmaj = st.daemon_version / 1000000;
int dmin = (st.daemon_version / 10000) % 100;
int dpat = (st.daemon_version / 100) % 100;
ImGui::PushFont(body2);
snprintf(buf, sizeof(buf), "%s: %d.%d.%d", TR("daemon_version"), dmaj, dmin, dpat);
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", buf);
ImGui::PopFont();
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushFont(body2);
ImGui::PushTextWrapPos(cardMin.x + availWidth - pad - logoAreaW);
ImGui::TextUnformatted(TR("settings_about_text"));
ImGui::PopTextWrapPos();
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::PushFont(capFont);
ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_copyright"));
ImGui::PopFont();
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// Buttons — consistent equal-width row (full card width)
if (logoAreaW > 0) {
ImGui::Unindent(logoAreaW);
}
{
float fullContentW = availWidth - pad * 2;
float aboutBtnW = (fullContentW - Layout::spacingMd() * 3) / 4.0f;
if (TactileButton(TrId("website", "about_website").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://dragonx.is");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_website"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TrId("report_bug", "about_bug").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_report_bug"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
saveSettingsPageState(app->settings());
Notifications::instance().success("Settings saved");
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_save_settings"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TrId("reset_to_defaults", "about_reset").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) {
if (app->settings()) {
loadSettingsPageState(app->settings());
Notifications::instance().info("Settings reloaded from disk");
}
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reset_settings"));
}
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 = s_settingsState.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()))) {
s_settingsState.debug_expanded = !s_settingsState.debug_expanded;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", s_settingsState.debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand"));
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(), TR("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 (s_settingsState.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), "%s", TR("settings_debug_select"));
ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// The 22 dragonxd 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 = s_settingsState.debug_categories.count(debugCats[i]) > 0;
if (ImGui::Checkbox(debugCats[i], &enabled)) {
if (enabled) {
s_settingsState.debug_categories.insert(debugCats[i]);
} else {
s_settingsState.debug_categories.erase(debugCats[i]);
}
s_settingsState.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 (s_settingsState.debug_cats_dirty && app->supportsFullNodeLifecycleActions()) {
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(TR("settings_debug_changed"));
ImGui::PopStyleColor();
ImGui::SameLine();
if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) {
s_settingsState.debug_cats_dirty = false;
app->restartDaemon();
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_restart_daemon"));
}
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 && !s_settingsState.low_spec_mode && s_settingsState.fade_shader.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
// Confirmation dialog for clearing z-tx history
if (s_settingsState.confirm_clear_ztx) {
if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_settingsState.confirm_clear_ztx, 480.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning"));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1"));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning2"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
s_settingsState.confirm_clear_ztx = false;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button(TrId("clear_anyway", "clear_ztx_btn").c_str(), ImVec2(btnW, 40))) {
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");
}
s_settingsState.confirm_clear_ztx = false;
}
ImGui::PopStyleColor(2);
EndOverlayDialog();
}
}
// Confirmation dialog for deleting blockchain data
if (s_settingsState.confirm_delete_blockchain) {
if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &s_settingsState.confirm_delete_blockchain, 500.0f, 0.94f)) {
ImGui::PushFont(Type().iconLarge());
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ICON_MD_WARNING);
ImGui::PopFont();
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("warning"));
ImGui::Spacing();
ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg"));
ImGui::Spacing();
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_delete_blockchain_safe"));
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) {
s_settingsState.confirm_delete_blockchain = false;
}
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f));
if (ImGui::Button(TrId("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) {
if (app->supportsFullNodeLifecycleActions())
app->deleteBlockchainData();
s_settingsState.confirm_delete_blockchain = false;
}
ImGui::PopStyleColor(2);
EndOverlayDialog();
}
}
}
} // namespace ui
} // namespace dragonx