feat(debug): theme x tab screenshot sweep + Debug options dialog

Add a debug tool that cycles every skin across every build-enabled tab and saves a
PNG of each to a timestamped folder under the config dir (screenshots/sweep_<ts>/)
— for gathering visual context on theme-specific UI work.

- App state machine (app_network.cpp): startScreenshotSweep() builds the
  skin+enabled-page lists, saves the current skin/page, and steps through them;
  updateScreenshotSweep() (top of App::render) pins the page + settles ~4 frames
  after each skin/page change (skin switches reload TOML + reset the acrylic
  capture, so they need to settle); wantsScreenshotThisFrame()/screenshotSweepPath()
  /onScreenshotCaptured() coordinate with main.cpp. Restores the original skin/page
  when done; nothing persisted.
- Framebuffer capture (main.cpp): read the finished frame and encode PNG via the
  bundled miniz (tdefl_write_image_to_png_file_in_memory_ex). GL reads FBO 0 (RGBA,
  bottom-up -> flip); DX11 copies the backbuffer to a staging texture + maps it
  (BGRA, top-down -> channel-swap). Alpha forced opaque.
- Settings UI: move the Verbose logging checkbox off the wallet row into a new
  "Debug options" button that opens a dialog housing the verbose toggle + a "Run
  screenshot sweep" button. New i18n keys.

Both platforms build; no-crash smoke verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 17:41:49 -05:00
parent a523611779
commit dddf1b6ec7
6 changed files with 236 additions and 9 deletions

View File

@@ -40,6 +40,7 @@
#include "daemon/xmrig_manager.h"
#include "util/pool_registry.h"
#include "ui/notifications.h"
#include "ui/schema/skin_manager.h"
#include "default_banlist_embedded.h"
#include "util/amount_format.h"
#include "util/http_download.h"
@@ -53,6 +54,7 @@
#include <algorithm>
#include <cmath>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <utility>
@@ -741,6 +743,108 @@ 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::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";
}
}
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;
// Timestamped output folder under the config dir.
std::time_t now = std::time(nullptr);
char stamp[32] = "sweep";
if (std::tm* tmv = std::localtime(&now)) std::strftime(stamp, sizeof(stamp), "%Y%m%d_%H%M%S", tmv);
namespace fs = std::filesystem;
fs::path dir = fs::path(util::Platform::getObsidianDragonDir()) / "screenshots" / (std::string("sweep_") + stamp);
std::error_code ec; fs::create_directories(dir, ec);
sweep_dir_ = dir.string();
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
int idx = sweep_skin_idx_ * static_cast<int>(sweep_pages_.size()) + sweep_page_idx_;
char name[160];
snprintf(name, sizeof(name), "%03d_%s_%s.png", idx,
sweep_skins_[sweep_skin_idx_].c_str(), sweepPageName(current_page_));
sweep_current_path_ = (std::filesystem::path(sweep_dir_) / name).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();
}
bool App::shouldRefreshTransactions() const
{
// NOTE: this is block-height / dirty driven, NOT interval-gated. It returns true only when a new