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

@@ -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: <config>/screenshots-full/<surface>/<skin>.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<std::string> sweep_skins_; // skin ids to cycle
std::vector<ui::NavPage> 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<void(App&)> setup; // reveal the surface (null = plain tab)
std::function<void(App&)> teardown; // clear it (null = nothing to undo)
int settle = 4;// blur overlays override to 8
};
std::vector<SweepTarget> 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<AddressInfo> addresses, z_addresses, t_addresses;
std::vector<TransactionInfo> 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