feat(debug): full UI screenshot sweep — captures modals/dialogs/flows/states

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:
  <config>/screenshots-full/<surface>/<skin>.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) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 16:57:54 -05:00
parent 5228da707f
commit a86cb8f0f3
11 changed files with 500 additions and 112 deletions

View File

@@ -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<int>(ui::NavPage::Count_); ++p) {
ui::NavPage pg = static_cast<ui::NavPage>(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 <screenshots>/<tab>/<skin>.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<int>(sweep_pages_.size())) {
sweep_page_idx_ = 0;
if (++sweep_skin_idx_ >= static_cast<int>(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::function<void(bool, bool, const std::string&, co
// learns whether a mnemonic exists). Legacy (non-mnemonic) wallets are not nagged.
void App::maybeRemindSeedBackup()
{
if (capture_mode_) return; // no live ops during a UI sweep
if (lite_wallet_) return; // lite has its own seed UX
if (!settings_ || settings_->getSeedBackupReminded()) 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;