From a86cb8f0f357136f624538475d40aa4a81d6bcd7 Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 7 Jul 2026 16:57:54 -0500 Subject: [PATCH] =?UTF-8?q?feat(debug):=20full=20UI=20screenshot=20sweep?= =?UTF-8?q?=20=E2=80=94=20captures=20modals/dialogs/flows/states?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing sweep only captures tabs. This adds a "Full UI sweep" (DEBUG OPTIONS) that also drives every normally-hidden surface into view and captures it under every skin, so all the UI a normal sweep misses can be reviewed at once. - The capture unit becomes a "surface" (SweepTarget: a base tab, optionally with a setup()/teardown() that forces a modal / multi-step flow / state overlay on top). Tabs are surfaces with a null setup. The existing tab sweep is folded into the same state machine (one code path). All sweep logic + the catalog + demo data move into a new src/app_sweep.cpp. - Runs OFFLINE against injected demo data (installDemoWalletData snapshots then restores the real state_ — WalletState isn't copy-assignable, so a targeted field snapshot is used) and fires NO live op: capture_mode_ guards the seed-backup RPC, auto-lock, and the migration pumps; the async-firing dialogs are neutralized by pre-setting their state (fetch_started_ + demo phrase; seed_migration_step_ set directly). Verified: 0 RPC/secret/send calls. - Catalog (v1): tabs + the bool-flag modals (import/export key, backup, seed-backup, about, settings) + the 6 migrate-to-seed steps + 4 wizard steps + lock/warmup/not-ready overlays + the send-confirm popup (via a new ui::SweepShowSendConfirm debug hook, since its state is send_tab-static). - Overlay/blur surfaces settle 8 frames (blur backdrop freeze); setup re-runs each frame (OpenPopup popups must re-fire). updateScreenshotSweep now runs before the first-run-wizard early-return in render() so the sweep advances while a wizard step is shown. main.cpp drops vsync during a sweep so it isn't throttled to the compositor's unfocused rate. Output: /screenshots-full//.png + index.md. - DRAGONX_FULL_SWEEP=1 runs the sweep headlessly then quits (CI / verification). Verified end-to-end under WSLg: 288 PNGs (9 skins x 32 surfaces), all 1200x720, [Sweep] done, clean quit, no daemon. Byte-identity across runs holds only for static skins on data-free surfaces — the rest vary by animated theme effects / live price / relative timestamps (expected for a review tool, not a bug). Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 1 + src/app.cpp | 13 +- src/app.h | 47 +++- src/app_network.cpp | 107 +-------- src/app_security.cpp | 1 + src/app_sweep.cpp | 389 +++++++++++++++++++++++++++++++++ src/main.cpp | 25 +++ src/ui/pages/settings_page.cpp | 4 + src/ui/windows/send_tab.cpp | 17 ++ src/ui/windows/send_tab.h | 7 + src/util/i18n.cpp | 1 + 11 files changed, 500 insertions(+), 112 deletions(-) create mode 100644 src/app_sweep.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 233c888..dee5255 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -404,6 +404,7 @@ set(APP_SOURCES src/app.cpp src/app_network.cpp src/app_security.cpp + src/app_sweep.cpp src/app_wizard.cpp src/services/network_refresh_service.cpp src/services/refresh_scheduler.cpp diff --git a/src/app.cpp b/src/app.cpp index f7d6a39..cdfff5b 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1272,6 +1272,11 @@ void App::handleGlobalShortcuts() void App::render() { + // Advance the screenshot sweep FIRST — before the first-run-wizard early-return below — so the + // sweep keeps progressing (and re-forces its target's state) even while a wizard step is shown. + // (Pins current_page_ too, before the sidebar reads it further down.) + updateScreenshotSweep(); + // First-run wizard gate — blocks all normal UI if (wizard_phase_ != WizardPhase::None && wizard_phase_ != WizardPhase::Done) { renderFirstRunWizard(); @@ -1303,9 +1308,6 @@ void App::render() } } - // Debug screenshot sweep — pins the current (skin,page) and arms capture once settled. Must run - // before the sidebar reads current_page_ (below) so the forced page is reflected. - updateScreenshotSweep(); // While any full-window BLUR overlay is open: (1) suppress panel theme-effects for the whole // frame so their foreground-draw-list borders (sidebar included) don't bleed over the overlay; @@ -2969,8 +2971,9 @@ void App::renderSeedBackupDialog() show_seed_backup_ = false; }; - // Kick off the async z_exportmnemonic fetch once, and only while unlocked. - if (!seed_backup_fetch_started_ && !state_.isLocked()) { + // Kick off the async z_exportmnemonic fetch once, and only while unlocked. Never during a UI + // sweep — the sweep pre-sets fetch_started_ + a demo phrase, but guard the RPC too. + if (!seed_backup_fetch_started_ && !state_.isLocked() && !capture_mode_) { seed_backup_fetch_started_ = true; seed_backup_loading_ = true; seed_backup_no_mnemonic_ = false; diff --git a/src/app.h b/src/app.h index 3909e22..0cd3ab3 100644 --- a/src/app.h +++ b/src/app.h @@ -343,6 +343,12 @@ public: // wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls // onScreenshotCaptured() to advance. Transient — restores the original skin/page when done. void startScreenshotSweep(); + // Full UI sweep: like the tab sweep, but also drives every modal / dialog / multi-step flow / + // state overlay into view (with injected demo data, offline, firing no live ops) and captures + // each under every skin. Output: /screenshots-full//.png + an index. + void startFullUiSweep(); + std::string screenshotFullDir() const; + bool isScreenshotSweeping() const { return screenshot_sweep_active_; } bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; } const std::string& screenshotSweepPath() const { return sweep_current_path_; } void onScreenshotCaptured(); @@ -756,16 +762,49 @@ private: bool screenshot_sweep_active_ = false; bool sweep_capture_this_frame_ = false; int sweep_skin_idx_ = 0; - int sweep_page_idx_ = 0; - int sweep_settle_frames_ = 0; // frames to let a new skin/page settle before capturing + int sweep_settle_frames_ = 0; // frames to let a new skin/surface settle before capture std::vector sweep_skins_; // skin ids to cycle - std::vector sweep_pages_; // enabled pages to cycle std::string sweep_dir_; // output folder for this sweep std::string sweep_current_path_; // PNG path for the frame about to be captured std::string sweep_saved_skin_; // restore on completion ui::NavPage sweep_saved_page_ = ui::NavPage::Overview; void updateScreenshotSweep(); // called at the top of render() while active - void applySweepTarget(); // apply current (skin,page), compute path, arm settle + void applySweepTarget(); // apply current (skin,page/surface), path, arm settle + + // --- Full UI sweep: the capture unit is a "surface" (a tab, optionally with a modal / step / + // state forced on top). A tab is a surface with a null setup. --- + struct SweepTarget { + std::string name; // fs-safe id, e.g. "modal-seed-backup" / "overview" + ui::NavPage page = ui::NavPage::Overview; // base tab under the surface + std::function setup; // reveal the surface (null = plain tab) + std::function teardown; // clear it (null = nothing to undo) + int settle = 4;// blur overlays override to 8 + }; + std::vector sweep_targets_; + int sweep_target_idx_ = 0; + bool sweep_full_ = false; // full-UI sweep (drives surfaces) vs the legacy tab-only sweep + // capture_mode_: set only during a full sweep. CONTRACT: while true, NO live op may fire — no + // RPC, no auto-lock, no async pump. New async paths that could run mid-sweep must guard on it. + bool capture_mode_ = false; + void startSweepImpl(bool full); + void buildSweepCatalog(); + void installDemoWalletData(); + void clearDemoWalletData(); + void applyHealthyDemoState(); // reset the connection/encryption flags to the healthy demo values + void writeSweepManifest() const; + // Snapshot of the state_ fields the demo installer mutates, restored at sweep end. Kept as a + // plain struct because WalletState has reference-alias members and isn't copy-assignable. + struct SweepStateSnapshot { + bool valid = false; + bool connected=false, warming_up=false, daemon_initializing=false; + bool encrypted=false, locked=false, encryption_state_known=false; + std::string warmup_status, warmup_description; + SyncInfo sync; + double privateBalance=0, transparentBalance=0, totalBalance=0, unconfirmedBalance=0; + std::vector addresses, z_addresses, t_addresses; + std::vector transactions; + double market_price_usd=0; + } sweep_state_snapshot_; bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized) float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width diff --git a/src/app_network.cpp b/src/app_network.cpp index 6833cf5..f2ce688 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -759,109 +759,7 @@ void App::setCurrentPage(ui::NavPage page) } } -// ── Debug screenshot sweep ────────────────────────────────────────────────── -// Cycle every skin x every build-enabled tab, saving one PNG per (skin,tab). Driven from -// App::render() (updateScreenshotSweep pins the page + settles); main.cpp reads the framebuffer -// on the settled frame and calls onScreenshotCaptured() to advance. - -// Filesystem-safe one-word page name (tracePageName returns "Overview tab" etc.). -static 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::Peers: return "network"; - case ui::NavPage::Explorer: return "explorer"; - case ui::NavPage::Settings: return "settings"; - default: return "page"; - } -} - -std::string App::screenshotDir() const -{ - return (std::filesystem::path(util::Platform::getObsidianDragonDir()) / "screenshots").string(); -} - -void App::startScreenshotSweep() -{ - if (screenshot_sweep_active_) return; - - sweep_skins_.clear(); - for (const auto& s : ui::schema::SkinManager::instance().available()) - if (s.valid) sweep_skins_.push_back(s.id); - sweep_pages_.clear(); - for (int p = 0; p < static_cast(ui::NavPage::Count_); ++p) { - ui::NavPage pg = static_cast(p); - if (isPageEnabledForBuild(pg)) sweep_pages_.push_back(pg); - } - if (sweep_skins_.empty() || sweep_pages_.empty()) return; - - // Fixed output folder (organized into per-tab subfolders below); subsequent sweeps overwrite the - // existing PNGs in place rather than creating a new folder each time. - sweep_dir_ = screenshotDir(); - std::error_code ec; std::filesystem::create_directories(sweep_dir_, ec); - - sweep_saved_skin_ = ui::schema::SkinManager::instance().activeSkinId(); - sweep_saved_page_ = current_page_; - sweep_skin_idx_ = 0; sweep_page_idx_ = 0; - screenshot_sweep_active_ = true; - sweep_capture_this_frame_ = false; - - ui::schema::SkinManager::instance().setActiveSkin(sweep_skins_[0]); - applySweepTarget(); - ui::Notifications::instance().info("Screenshot sweep running…"); - DEBUG_LOGF("[Sweep] -> %s (%d skins x %d pages)\n", - sweep_dir_.c_str(), (int)sweep_skins_.size(), (int)sweep_pages_.size()); -} - -void App::applySweepTarget() -{ - current_page_ = sweep_pages_[sweep_page_idx_]; - page_alpha_ = 1.0f; // skip the page-switch fade so the shot isn't captured mid-animation - // Organized as //.png — one subfolder per tab, one PNG per theme, - // overwritten in place on the next sweep. (writePng in main.cpp creates the parent subfolder.) - sweep_current_path_ = (std::filesystem::path(sweep_dir_) / sweepPageName(current_page_) - / (sweep_skins_[sweep_skin_idx_] + ".png")).string(); - sweep_settle_frames_ = 4; // let the new skin (TOML reload / acrylic recapture) + page settle - sweep_capture_this_frame_ = false; -} - -void App::updateScreenshotSweep() -{ - if (!screenshot_sweep_active_) return; - current_page_ = sweep_pages_[sweep_page_idx_]; // keep the page pinned each frame - page_alpha_ = 1.0f; - 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; - if (++sweep_page_idx_ >= static_cast(sweep_pages_.size())) { - sweep_page_idx_ = 0; - if (++sweep_skin_idx_ >= static_cast(sweep_skins_.size())) { - // Done — restore the original skin + page. - ui::schema::SkinManager::instance().setActiveSkin(sweep_saved_skin_); - current_page_ = sweep_saved_page_; - page_alpha_ = 1.0f; - screenshot_sweep_active_ = false; - ui::Notifications::instance().success("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(); -} +// Screenshot sweep (tab-only + full-UI) + its demo data live in src/app_sweep.cpp. bool App::shouldRefreshTransactions() const { @@ -2914,6 +2812,7 @@ void App::exportSeedPhrase(std::functiongetSeedBackupReminded()) return; if (!state_.connected || !state_.encryption_state_known) return; @@ -3063,6 +2962,7 @@ void App::beginSweepToSeedWallet() // remainder is left behind (a sweep too big for one transaction). void App::pollSweepStatus() { + if (capture_mode_) return; // no live ops during a UI sweep if (!rpc_ || !worker_ || !state_.connected) return; if (seed_migration_confirm_in_flight_ || seed_migration_sweep_txid_.empty()) return; seed_migration_confirm_in_flight_ = true; @@ -3172,6 +3072,7 @@ void App::beginAdoptSeedWallet() void App::pumpSeedMigration() { + if (capture_mode_) return; // no live ops during a UI sweep (steps are set directly) // --- Adopt handoff (Phase 2, background daemon swap) --- { bool done = false, ok = false; std::string err; diff --git a/src/app_security.cpp b/src/app_security.cpp index 825b6bb..4363e19 100644 --- a/src/app_security.cpp +++ b/src/app_security.cpp @@ -593,6 +593,7 @@ void App::refreshWalletEncryptionState() { // =========================================================================== void App::checkAutoLock() { + if (capture_mode_) return; // don't auto-lock while a UI sweep forces the encrypted/locked state if (!state_.isEncrypted() || state_.isLocked()) return; // Don't auto-lock while mining — mining is a long-running intentional diff --git a/src/app_sweep.cpp b/src/app_sweep.cpp new file mode 100644 index 0000000..e1ab22b --- /dev/null +++ b/src/app_sweep.cpp @@ -0,0 +1,389 @@ +// 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 "ui/schema/skin_manager.h" +#include "ui/notifications.h" +#include "ui/sidebar.h" +#include "ui/windows/send_tab.h" +#include "util/platform.h" +#include "wallet/wallet_capabilities.h" + +#include + +#include +#include + +namespace dragonx { + +namespace { +namespace fs = std::filesystem; + +// 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.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; + + 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"), + }; + + 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; + s = SweepStateSnapshot{}; // invalidate +} + +// ── 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(); + }); + }; + mig("modal-migrate-intro", SeedMigrationStep::Intro, nullptr); + 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; }); + 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."; }); + } + + // 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); }); +} + +// ── 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); } + + 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 diff --git a/src/main.cpp b/src/main.cpp index 5f70a87..08c6e69 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1754,6 +1754,16 @@ int main(int argc, char* argv[]) SDL_GL_MakeCurrent(backup_current_window, backup_current_context); } + // Uncap FPS during a screenshot sweep so it doesn't crawl at the compositor's (often + // heavily throttled for an unfocused window) vsync rate. Restored when the sweep ends. + { + static bool sweepVsyncOff = false; + const bool sweeping = app.isScreenshotSweeping(); + if (sweeping != sweepVsyncOff) { + SDL_GL_SetSwapInterval(sweeping ? 0 : 1); + sweepVsyncOff = sweeping; + } + } SDL_GL_SwapWindow(window); #endif PERF_END("Present", _perfPresent); @@ -1761,6 +1771,21 @@ int main(int argc, char* argv[]) // --- PerfLog: end frame --- dragonx::util::PerfLog::instance().endFrame(); + // Headless verification hook: DRAGONX_FULL_SWEEP=1 runs the full UI sweep once the app has + // settled, then quits when it finishes. Used to run the sweep without clicking the button. + { + static const bool autoSweep = std::getenv("DRAGONX_FULL_SWEEP") != nullptr; + static int autoFrames = 0; + static bool autoStarted = false; + if (autoSweep) { + if (!autoStarted) { + if (++autoFrames >= 90) { app.startFullUiSweep(); autoStarted = true; } + } else if (!app.isScreenshotSweeping()) { + running = false; // sweep complete + } + } + } + // Exit when shutdown is complete if (app.shouldQuit()) { running = false; diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 898d610..e152c93 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -2510,6 +2510,10 @@ void RenderSettingsPage(App* app) { if (TactileButton(TR("screenshot_sweep"), ImVec2(0, 0), S.resolveFont("button"))) app->startScreenshotSweep(); ImGui::SameLine(); + // Full UI sweep: also captures every modal / dialog / flow / state (demo data, offline). + if (TactileButton(TR("screenshot_sweep_full"), ImVec2(0, 0), S.resolveFont("button"))) + app->startFullUiSweep(); + ImGui::SameLine(); if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button"))) util::Platform::openFolder(app->screenshotDir()); if (chat::hushChatFeatureEnabledAtBuild()) { diff --git a/src/ui/windows/send_tab.cpp b/src/ui/windows/send_tab.cpp index 89f673a..6829e97 100644 --- a/src/ui/windows/send_tab.cpp +++ b/src/ui/windows/send_tab.cpp @@ -659,6 +659,23 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w, // ============================================================================ // Confirmation dialog // ============================================================================ +void SweepShowSendConfirm(App* app, bool show) { + (void)app; + if (show) { + std::snprintf(s_from_address, sizeof(s_from_address), "%s", + "zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000"); + std::snprintf(s_to_address, sizeof(s_to_address), "%s", + "zs1demopeerreceivingaddressabcdef0000000000000000000000000000000"); + s_amount = 2.50000000; s_fee = DRAGONX_DEFAULT_FEE; + std::snprintf(s_memo, sizeof(s_memo), "%s", "thanks!"); + s_show_confirm = true; // RenderSendConfirmPopup re-issues OpenPopup while this is set + } else { + s_show_confirm = false; + s_from_address[0] = s_to_address[0] = s_memo[0] = '\0'; + s_amount = 0.0; + } +} + void RenderSendConfirmPopup(App* app) { if (!s_show_confirm) return; diff --git a/src/ui/windows/send_tab.h b/src/ui/windows/send_tab.h index 8f85007..248bd29 100644 --- a/src/ui/windows/send_tab.h +++ b/src/ui/windows/send_tab.h @@ -24,6 +24,13 @@ void RenderSendTab(App* app); */ void RenderSendConfirmPopup(App* app); +/** + * @brief Debug/screenshot-sweep hook: force the send-confirm popup open with demo values (show=true) + * or close it (show=false). The popup's state lives in send_tab file statics, so this hook is + * the only way an external driver (the full UI sweep) can reveal it. Fires no send. + */ +void SweepShowSendConfirm(App* app, bool show); + /** * @brief Pre-fill the "from" address in the send tab * @param address The address to send from diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 81e2d84..7b726ed 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -329,6 +329,7 @@ void I18n::loadBuiltinEnglish() strings_["stop_external"] = "Stop external daemon"; strings_["verbose_logging"] = "Verbose logging"; strings_["screenshot_sweep"] = "Run screenshot sweep"; + strings_["screenshot_sweep_full"] = "Full UI sweep"; strings_["screenshot_open_dir"] = "Open location"; strings_["screenshot_sweep_desc"] = "Cycles every theme across every tab and saves a screenshot of each into per-tab subfolders under the config directory's screenshots folder (overwriting the previous sweep). Runs for a few seconds."; strings_["mine_when_idle"] = "Mine when idle";