// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Screenshot sweep: capture the UI under every skin. Two modes share one state machine: // - startScreenshotSweep() — the legacy tab-only sweep (/screenshots). // - startFullUiSweep() — ALSO drives every modal / dialog / multi-step flow / state overlay // into view (offline, with injected demo data, firing no live ops) and captures each under // every skin (/screenshots-full//.png + index.md). // The capture unit is a "surface" (SweepTarget): a base tab, optionally with a setup() that forces // a modal/step/state on top and a teardown() that clears it. main.cpp reads the framebuffer on the // settled frame and calls onScreenshotCaptured() to advance. #include "app.h" #include "config/settings.h" #include "ui/schema/skin_manager.h" #include "ui/notifications.h" #include "ui/sidebar.h" #include "ui/windows/send_tab.h" #include "ui/windows/wallets_dialog.h" #include "util/platform.h" #include "wallet/wallet_capabilities.h" #include "imgui.h" #include "imgui_internal.h" // ClosePopupToLevel / OpenPopupStack — flush stuck popups after teardown #include #include #include #include namespace dragonx { namespace { namespace fs = std::filesystem; // The real portfolio, snapshotted while demo groups are shown during a full sweep. Settings are // forward-declared in app.h, so this can't live in the app.h SweepStateSnapshot struct. std::vector g_pfSnapshot; int g_pfStyleSnapshot = 0; bool g_pfSnapshotValid = false; std::vector g_marketHistorySnapshot; // real price history, restored at sweep end // Filesystem-safe one-word tab name. const char* sweepPageName(ui::NavPage page) { switch (page) { case ui::NavPage::Overview: return "overview"; case ui::NavPage::Send: return "send"; case ui::NavPage::Receive: return "receive"; case ui::NavPage::History: return "history"; case ui::NavPage::Contacts: return "contacts"; case ui::NavPage::Chat: return "chat"; case ui::NavPage::Mining: return "mining"; case ui::NavPage::Market: return "market"; case ui::NavPage::Console: return "console"; case ui::NavPage::LiteConsole: return "console"; case ui::NavPage::Peers: return "network"; case ui::NavPage::LiteNetwork: return "network"; case ui::NavPage::Explorer: return "explorer"; case ui::NavPage::Settings: return "settings"; default: return "page"; } } bool sweepPageEnabled(ui::NavPage page) { return wallet::isUiSurfaceAvailable(wallet::currentWalletCapabilities(), ui::NavPageSurface(page)); } // Deterministic demo secrets/addresses (NON-secret — safe to hold in memory + display). const char* kDemoMnemonic = "select milk exit banana type alcohol comic moral drama federal just green " "elevator render stumble lesson convince organ category caution panther misery pelican immune"; const char* kDemoZAddr = "zs1demoseedwalletreceiveaddressforuisweepcaptures00000000000000000000000000"; const char* kDemoTxid = "b647077c471fdd2877d35c9d8b70e3c785547fae618458b4068d913ee3d1dc4f"; } // namespace std::string App::screenshotDir() const { return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots").string(); } std::string App::screenshotFullDir() const { return (fs::path(util::Platform::getObsidianDragonDir()) / "screenshots-full").string(); } // ── Demo data (full sweep only): make every data-dependent screen render offline ───────────── void App::installDemoWalletData() { // Snapshot the state_ fields we mutate (WalletState isn't copy-assignable — reference aliases). auto& s = sweep_state_snapshot_; s.connected = state_.connected; s.warming_up = state_.warming_up; s.daemon_initializing = state_.daemon_initializing; s.encrypted = state_.encrypted; s.locked = state_.locked; s.encryption_state_known = state_.encryption_state_known; s.warmup_status = state_.warmup_status; s.warmup_description = state_.warmup_description; s.sync = state_.sync; s.privateBalance = state_.privateBalance; s.transparentBalance = state_.transparentBalance; s.totalBalance = state_.totalBalance; s.unconfirmedBalance = state_.unconfirmedBalance; s.addresses = state_.addresses; s.z_addresses = state_.z_addresses; s.t_addresses = state_.t_addresses; s.transactions = state_.transactions; s.market_price_usd = state_.market.price_usd; s.market_change_24h = state_.market.change_24h; s.valid = true; applyHealthyDemoState(); state_.sync.blocks = state_.sync.headers = 3124322; state_.sync.verification_progress = 1.0; state_.sync.syncing = false; state_.privateBalance = 12.50000000; state_.transparentBalance = 3.25000000; state_.totalBalance = 15.75000000; state_.unconfirmedBalance = 0.50000000; state_.market.price_usd = 0.01336200; state_.market.change_24h = 5.24000000; // Wavy, gently-rising price history so the row sparklines render (restored at sweep end). g_marketHistorySnapshot = state_.market.price_history; { std::vector hist; for (int i = 0; i < 48; i++) { double t = static_cast(i); hist.push_back(0.01250 + 0.0000180 * t + 0.00060 * std::sin(t * 0.55) + 0.00025 * std::sin(t * 1.7)); } state_.market.price_history = hist; } auto zaddr = [](const char* a, double bal, const char* label) { AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i; }; auto taddr = [](const char* a, double bal, const char* label) { AddressInfo i; i.address = a; i.balance = bal; i.type = "transparent"; i.label = label; return i; }; state_.z_addresses = { zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"), zaddr("zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", 0.5, ""), }; state_.t_addresses = { taddr("t1DemoTransparentAddressForUiSweep00000", 3.25, "Mining payouts"), taddr("t1DemoSecondTransparentAddressForUiSw00", 0.0, ""), }; state_.rebuildAddressList(); auto tx = [](const char* id, const char* type, double amt, int64_t ts, int conf, const char* addr, const char* memo) { TransactionInfo t; t.txid = id; t.type = type; t.amount = amt; t.timestamp = ts; t.confirmations = conf; t.address = addr; t.memo = memo; return t; }; state_.transactions = { tx(kDemoTxid, "receive", 15.75000000, 1751286000, 42, "zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", "welcome to DragonX"), tx("a1b2c3d4e5f600000000000000000000000000000000000000000000000000000000", "send", 2.50000000, 1751200000, 120, "zs1demopeerreceivingaddressabcdef0000000000000000000000000000000", ""), tx("c0ffee00000000000000000000000000000000000000000000000000000000000000", "mined", 0.30000000, 1751100000, 300, "t1DemoTransparentAddressForUiSweep00000", ""), tx("deadbeef000000000000000000000000000000000000000000000000000000000000", "receive", 0.50000000, 1751290000, 0, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", "pending"), }; // Demo portfolio groups so the Market tab's row styles render with real-looking data. Snapshot // the user's real portfolio first; setPortfolio* only mutate memory (save() is never called // here), and clearDemoWalletData() restores them at sweep end. if (settings_) { g_pfSnapshot = settings_->getPortfolioEntries(); g_pfStyleSnapshot = settings_->getPortfolioStyle(); g_pfSnapshotValid = true; auto grp = [](const char* label, const char* icon, uint32_t color, const char* addr, bool drgx, bool value, bool ch, bool spark) { config::Settings::PortfolioEntry e; e.label = label; e.icon = icon; e.color = color; e.outlineOpacity = 30; e.addresses = { addr }; e.priceBasis = 0; e.showDrgx = drgx; e.showValue = value; e.show24h = ch; e.showSparkline = spark; return e; }; settings_->setPortfolioEntries({ grp("Savings", "savings", 0xFFFF9D4Fu, "zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", true, true, true, true), grp("Mining rewards", "pickaxe", 0xFF3DB0FFu, "t1DemoTransparentAddressForUiSweep00000", true, true, true, true), grp("Cold storage", "diamond", 0xFFFF7BB0u, "zs1demosecondaryshieldedaddressforuisweep0000000000000000000000000", true, true, false, false), }); settings_->setPortfolioStyle(0); } seedChatDemoData(); } void App::applyHealthyDemoState() { state_.connected = true; state_.warming_up = false; state_.daemon_initializing = false; state_.encryption_state_known = true; state_.encrypted = false; state_.locked = false; state_.warmup_status.clear(); state_.warmup_description.clear(); // Dismiss any first-run wizard so the base tabs/modals aren't occluded (the wizard targets set // wizard_phase_ themselves and restore None on teardown). wizard_phase_ = WizardPhase::None; } void App::clearDemoWalletData() { auto& s = sweep_state_snapshot_; if (!s.valid) return; state_.connected = s.connected; state_.warming_up = s.warming_up; state_.daemon_initializing = s.daemon_initializing; state_.encrypted = s.encrypted; state_.locked = s.locked; state_.encryption_state_known = s.encryption_state_known; state_.warmup_status = s.warmup_status; state_.warmup_description = s.warmup_description; state_.sync = s.sync; state_.privateBalance = s.privateBalance; state_.transparentBalance = s.transparentBalance; state_.totalBalance = s.totalBalance; state_.unconfirmedBalance = s.unconfirmedBalance; state_.addresses = s.addresses; state_.z_addresses = s.z_addresses; state_.t_addresses = s.t_addresses; state_.transactions = s.transactions; state_.market.price_usd = s.market_price_usd; state_.market.change_24h = s.market_change_24h; state_.market.price_history = g_marketHistorySnapshot; g_marketHistorySnapshot.clear(); s = SweepStateSnapshot{}; // invalidate // Restore the user's real portfolio (demo groups were in-memory only). if (g_pfSnapshotValid && settings_) { settings_->setPortfolioEntries(g_pfSnapshot); settings_->setPortfolioStyle(g_pfStyleSnapshot); } g_pfSnapshot.clear(); g_pfSnapshotValid = false; } // ── Catalog ────────────────────────────────────────────────────────────────────────────────── // Lambdas defined in this member function may touch App's private members through the App& arg. void App::buildSweepCatalog() { sweep_targets_.clear(); // Tabs (both sweeps). for (int p = 0; p < static_cast(ui::NavPage::Count_); ++p) { ui::NavPage pg = static_cast(p); if (sweepPageEnabled(pg)) sweep_targets_.push_back({ sweepPageName(pg), pg, nullptr, nullptr, 4 }); } if (!sweep_full_) return; // Full sweep: modals / flows / states. Blur overlays need more settle frames. const int kOverlaySettle = 8; auto add = [&](const char* name, ui::NavPage pg, std::function setup, std::function teardown) { sweep_targets_.push_back({ name, pg, std::move(setup), std::move(teardown), kOverlaySettle }); }; // Simple bool-flag modals. add("modal-import-key", ui::NavPage::Overview, [](App& a) { a.show_import_key_ = true; }, [](App& a) { a.show_import_key_ = false; }); add("modal-export-key", ui::NavPage::Overview, [](App& a) { a.show_export_key_ = true; }, [](App& a) { a.show_export_key_ = false; }); add("modal-backup", ui::NavPage::Overview, [](App& a) { a.show_backup_ = true; }, [](App& a) { a.show_backup_ = false; a.backup_status_.clear(); }); add("modal-about", ui::NavPage::Overview, [](App& a) { a.show_about_ = true; }, [](App& a) { a.show_about_ = false; }); add("modal-settings", ui::NavPage::Settings, [](App& a) { a.show_settings_ = true; }, [](App& a) { a.show_settings_ = false; }); // Seed-backup: pre-set fetch + a demo phrase so it renders the word grid without any RPC. add("modal-seed-backup", ui::NavPage::Overview, [](App& a) { a.show_seed_backup_ = true; a.seed_backup_fetch_started_ = true; a.seed_backup_loading_ = false; a.seed_backup_no_mnemonic_ = false; a.seed_backup_status_.clear(); if (a.seed_backup_phrase_.empty()) a.seed_backup_phrase_ = kDemoMnemonic; }, [](App& a) { a.show_seed_backup_ = false; a.seed_backup_fetch_started_ = false; if (!a.seed_backup_phrase_.empty()) sodium_memzero(&a.seed_backup_phrase_[0], a.seed_backup_phrase_.size()); a.seed_backup_phrase_.clear(); }); // Migrate-to-seed — one surface per step (full-node only). Setting the step directly never // fires the async create/sweep/adopt (those are button-triggered). if (supportsFullNodeLifecycleActions()) { auto mig = [&](const char* name, SeedMigrationStep step, std::function extra) { add(name, ui::NavPage::Settings, [step, extra](App& a) { a.show_seed_migration_ = true; a.seed_migration_step_ = step; a.seed_migration_dest_ = kDemoZAddr; if (extra) extra(a); }, [](App& a) { a.show_seed_migration_ = false; if (!a.seed_migration_seed_.empty()) sodium_memzero(&a.seed_migration_seed_[0], a.seed_migration_seed_.size()); a.seed_migration_seed_.clear(); a.seed_migration_status_.clear(); }); }; // Intro pre-flight branches: pre-set the probe result so the sweep captures each variant // (the probe itself is guarded off during capture_mode_). mig("modal-migrate-intro", SeedMigrationStep::Intro, [](App& a) { a.seed_migration_precheck_ = SeedMigrationPrecheck::Legacy; a.seed_migration_precheck_started_ = true; }); mig("modal-migrate-intro-seeded", SeedMigrationStep::Intro, [](App& a) { a.seed_migration_precheck_ = SeedMigrationPrecheck::AlreadyMnemonic; a.seed_migration_precheck_started_ = true; }); mig("modal-migrate-intro-oldnode", SeedMigrationStep::Intro, [](App& a) { a.seed_migration_precheck_ = SeedMigrationPrecheck::DaemonTooOld; a.seed_migration_precheck_started_ = true; }); mig("modal-migrate-showseed", SeedMigrationStep::ShowSeed, [](App& a) { a.seed_migration_seed_ = kDemoMnemonic; a.seed_migration_backed_up_ = false; }); mig("modal-migrate-sweep", SeedMigrationStep::Sweep, [](App& a) { a.seed_migration_balance_ = 15.75000000; a.seed_migration_balance_loaded_ = true; }); mig("modal-migrate-nofunds", SeedMigrationStep::Sweep, [](App& a) { a.seed_migration_balance_ = 0.0; a.seed_migration_balance_loaded_ = true; }); mig("modal-migrate-confirming", SeedMigrationStep::Confirming, [](App& a) { a.seed_migration_sweep_confs_ = 1; a.seed_migration_sweep_txid_ = kDemoTxid; a.seed_migration_legacy_remaining_ = 0.0; }); mig("modal-migrate-done", SeedMigrationStep::Done, nullptr); mig("modal-migrate-error", SeedMigrationStep::Error, [](App& a) { a.seed_migration_status_ = "Example error: the daemon did not respond."; }); // Multi-wallet: the wallet-files list. Drop a couple of demo wallet files in the (throwaway) // datadir + cache their metadata so the list renders populated. add("modal-wallets", ui::NavPage::Settings, [](App& a) { std::error_code ec; const std::string dd = util::Platform::getDragonXDataDir(); fs::create_directories(dd, ec); if (!fs::exists(dd + "/wallet.dat", ec)) std::ofstream(dd + "/wallet.dat", std::ios::binary) << std::string(122880, '\0'); if (!fs::exists(dd + "/wallet-savings.dat", ec)) std::ofstream(dd + "/wallet-savings.dat", std::ios::binary) << std::string(65536, '\0'); data::WalletIndexEntry e1; e1.fileName = "wallet.dat"; e1.displayName = "wallet.dat"; e1.cachedBalance = 15.7526; e1.cachedAddressCount = 4; e1.lastOpenedEpoch = 1720000000; e1.syncedHere = true; data::WalletIndexEntry e2; e2.fileName = "wallet-savings.dat"; e2.displayName = "wallet-savings.dat"; e2.cachedBalance = 250.0; e2.cachedAddressCount = 2; e2.lastOpenedEpoch = 1719400000; e2.syncedHere = true; a.walletIndex().upsert(e1); a.walletIndex().upsert(e2); // An out-of-datadir wallet (in an extra folder) so the Import action shows too. const std::string extra = util::Platform::getConfigDir() + "extra-wallets"; fs::create_directories(extra, ec); if (!fs::exists(extra + "/my-wallet.dat", ec)) std::ofstream(extra + "/my-wallet.dat", std::ios::binary) << std::string(80000, '\0'); a.walletIndex().addExtraFolder(extra); if (a.settings()) a.settings()->setActiveWalletFile("wallet.dat"); ui::WalletsDialog::show(&a); }, [](App&) { ui::WalletsDialog::hide(); }); } // First-run wizard — one per meaningful phase (full-node only; blocks all other UI while shown). if (!isLiteBuild()) { auto wiz = [&](const char* name, WizardPhase phase) { add(name, ui::NavPage::Overview, [phase](App& a) { a.wizard_phase_ = phase; }, [](App& a) { a.wizard_phase_ = WizardPhase::None; }); }; wiz("wizard-appearance", WizardPhase::Appearance); wiz("wizard-bootstrap", WizardPhase::BootstrapOffer); wiz("wizard-encrypt", WizardPhase::EncryptOffer); wiz("wizard-pin", WizardPhase::PinSetup); } // App-state overlays. Teardown restores the healthy demo flags (full restore at sweep end). add("overlay-lock", ui::NavPage::Overview, [](App& a) { a.state_.encrypted = true; a.state_.locked = true; }, [](App& a) { a.applyHealthyDemoState(); }); add("overlay-warmup", ui::NavPage::Overview, [](App& a) { a.state_.warming_up = true; a.state_.warmup_status = "Processing blocks…"; a.state_.warmup_description = "The node is loading the block index."; }, [](App& a) { a.applyHealthyDemoState(); }); add("overlay-not-ready", ui::NavPage::Overview, [](App& a) { a.state_.connected = false; a.state_.encryption_state_known = false; }, [](App& a) { a.applyHealthyDemoState(); }); // Send-confirm popup (state lives in send_tab statics → driven via a debug hook). add("popup-send-confirm", ui::NavPage::Send, [](App& a) { ui::SweepShowSendConfirm(&a, true); }, [](App& a) { ui::SweepShowSendConfirm(&a, false); }); // Market portfolio row styles — capture all three so the redesign is reviewable per skin. add("market-rows-compact", ui::NavPage::Market, [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); }, [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); }); add("market-rows-detailed", ui::NavPage::Market, [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(1); }, [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); }); add("market-rows-featured", ui::NavPage::Market, [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(2); }, [](App& a) { if (a.settings_) a.settings_->setPortfolioStyle(0); }); // Console RPC command-reference popup (fixed-height dialog with a fill-height command list). add("modal-console-commands", ui::NavPage::Console, [](App& a) { a.console_tab_.sweepSetCommandsPopup(true); }, [](App& a) { a.console_tab_.sweepSetCommandsPopup(false); }); } // ── State machine ─────────────────────────────────────────────────────────────────────────── void App::startSweepImpl(bool full) { if (screenshot_sweep_active_) return; sweep_skins_.clear(); for (const auto& sk : ui::schema::SkinManager::instance().available()) if (sk.valid) sweep_skins_.push_back(sk.id); if (sweep_skins_.empty()) return; sweep_full_ = full; if (full) { capture_mode_ = true; installDemoWalletData(); } buildSweepCatalog(); if (sweep_targets_.empty()) { if (full) { clearDemoWalletData(); capture_mode_ = false; sweep_full_ = false; } return; } sweep_dir_ = full ? screenshotFullDir() : screenshotDir(); std::error_code ec; fs::create_directories(sweep_dir_, ec); sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId(); sweep_saved_page_ = current_page_; sweep_skin_idx_ = 0; sweep_target_idx_ = 0; screenshot_sweep_active_ = true; sweep_capture_this_frame_ = false; ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]); applySweepTarget(); ui::Notifications::instance().info(full ? "Full UI sweep running…" : "Screenshot sweep running…"); DEBUG_LOGF("[Sweep] %s -> %s (%d skins x %d surfaces)\n", full ? "FULL" : "tabs", sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_targets_.size()); } void App::startScreenshotSweep() { startSweepImpl(false); } void App::startFullUiSweep() { startSweepImpl(true); } void App::applySweepTarget() { const SweepTarget& t = sweep_targets_[sweep_target_idx_]; current_page_ = t.page; page_alpha_ = 1.0f; // skip the page-switch fade so the shot isn't captured mid-animation if (t.setup) t.setup(*this); // //.png — one subfolder per surface, one PNG per theme, overwritten in // place next sweep. (writePng in main.cpp creates the parent subfolder.) sweep_current_path_ = (fs::path(sweep_dir_) / t.name / (sweep_skins_[sweep_skin_idx_] + ".png")).string(); sweep_settle_frames_ = t.settle; sweep_capture_this_frame_ = false; } void App::updateScreenshotSweep() { if (!screenshot_sweep_active_) return; const SweepTarget& t = sweep_targets_[sweep_target_idx_]; current_page_ = t.page; // keep the surface pinned each frame page_alpha_ = 1.0f; // Re-run setup every frame: idempotent for flags/enums, and required for OpenPopup-based popups // (must re-fire while open) + to keep the surface state fixed against any refresh. if (t.setup) t.setup(*this); if (sweep_settle_frames_ > 0) { sweep_settle_frames_--; sweep_capture_this_frame_ = false; } else sweep_capture_this_frame_ = true; // settled — main.cpp captures this frame } void App::onScreenshotCaptured() { sweep_capture_this_frame_ = false; if (!screenshot_sweep_active_) return; { const SweepTarget& t = sweep_targets_[sweep_target_idx_]; if (t.teardown) t.teardown(*this); } // Some surfaces leave an ImGui popup open (e.g. the send-confirm dialog calls OpenPopup but is // torn down by just clearing its flag). The headless sweep never clicks, so ImGui's normal // click-to-dismiss cleanup never runs and the popup lingers on the stack — which keeps // IsPopupOpen(AnyPopup) true forever and disables the smooth-scroll wheel capture on // Settings/Explorer/Console (draw_helpers::ApplySmoothScroll). Flush any lingering popup here so // it can't bleed into the next surface's capture or survive past the sweep. if (ImGuiContext* g = ImGui::GetCurrentContext(); g && g->OpenPopupStack.Size > 0) ImGui::ClosePopupToLevel(0, false); if (++sweep_target_idx_ >= static_cast(sweep_targets_.size())) { sweep_target_idx_ = 0; if (++sweep_skin_idx_ >= static_cast(sweep_skins_.size())) { // Done — restore the original skin + page, and (full) the real state. const bool wasFull = sweep_full_; if (wasFull) writeSweepManifest(); ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_); current_page_ = sweep_saved_page_; page_alpha_ = 1.0f; if (wasFull) { clearDemoWalletData(); capture_mode_ = false; } sweep_full_ = false; screenshot_sweep_active_ = false; ui::Notifications::instance().success( (wasFull ? std::string("Full UI screenshots saved to ") : std::string("Screenshots saved to ")) + sweep_dir_); DEBUG_LOGF("[Sweep] done -> %s\n", sweep_dir_.c_str()); return; } ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[sweep_skin_idx_]); } applySweepTarget(); } void App::writeSweepManifest() const { std::error_code ec; fs::create_directories(sweep_dir_, ec); std::ofstream out((fs::path(sweep_dir_) / "index.md").string(), std::ios::trunc); if (!out) return; out << "# Full UI sweep\n\n"; out << sweep_targets_.size() << " surfaces x " << sweep_skins_.size() << " skins\n\n"; for (const auto& t : sweep_targets_) { out << "## " << t.name << " (base: " << sweepPageName(t.page) << ")\n\n"; for (const auto& skin : sweep_skins_) out << "- " << skin << ": `" << t.name << "/" << skin << ".png`\n"; out << "\n"; } } } // namespace dragonx