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

@@ -1243,6 +1243,10 @@ void App::render()
// Process deferred encryption from wizard (runs in background) // Process deferred encryption from wizard (runs in background)
processDeferredEncryption(); processDeferredEncryption();
// 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 // 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; // frame so their foreground-draw-list borders (sidebar included) don't bleed over the overlay;
// (2) mark a full-window blur overlay active so glass panels use their opaque fallback (they're // (2) mark a full-window blur overlay active so glass panels use their opaque fallback (they're

View File

@@ -308,7 +308,16 @@ public:
// UI navigation // UI navigation
void setCurrentPage(ui::NavPage page); void setCurrentPage(ui::NavPage page);
ui::NavPage getCurrentPage() const { return current_page_; } ui::NavPage getCurrentPage() const { return current_page_; }
// Debug: screenshot sweep — cycles every skin x every enabled tab, one PNG each, into a
// timestamped folder under the config dir. Driven from App::render(); main.cpp polls
// wantsScreenshotThisFrame() after drawing the frame, saves screenshotSweepPath(), then calls
// onScreenshotCaptured() to advance. Transient — restores the original skin/page when done.
void startScreenshotSweep();
bool wantsScreenshotThisFrame() const { return sweep_capture_this_frame_; }
const std::string& screenshotSweepPath() const { return sweep_current_path_; }
void onScreenshotCaptured();
// Dialog triggers (used by settings page to open modal dialogs) // Dialog triggers (used by settings page to open modal dialogs)
void showImportKeyDialog() { show_import_key_ = true; } void showImportKeyDialog() { show_import_key_ = true; }
void showExportKeyDialog() { show_export_key_ = true; } void showExportKeyDialog() { show_export_key_ = true; }
@@ -638,6 +647,21 @@ private:
ui::NavPage prev_page_ = ui::NavPage::Overview; ui::NavPage prev_page_ = ui::NavPage::Overview;
float page_alpha_ = 1.0f; // 0→1 fade on page switch float page_alpha_ = 1.0f; // 0→1 fade on page switch
bool sidebar_collapsed_ = false; // true = icon-only mode bool sidebar_collapsed_ = false; // true = icon-only mode
// Debug screenshot sweep state.
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
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
bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse bool sidebar_user_toggled_ = false; // user manually toggled — suppress auto-collapse
float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized) float sidebar_width_anim_ = 0.0f; // animated width (0 = uninitialized)
float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width float prev_dpi_scale_ = 0.0f; // detect DPI changes to snap sidebar width

View File

@@ -40,6 +40,7 @@
#include "daemon/xmrig_manager.h" #include "daemon/xmrig_manager.h"
#include "util/pool_registry.h" #include "util/pool_registry.h"
#include "ui/notifications.h" #include "ui/notifications.h"
#include "ui/schema/skin_manager.h"
#include "default_banlist_embedded.h" #include "default_banlist_embedded.h"
#include "util/amount_format.h" #include "util/amount_format.h"
#include "util/http_download.h" #include "util/http_download.h"
@@ -53,6 +54,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <ctime> #include <ctime>
#include <filesystem>
#include <fstream> #include <fstream>
#include <utility> #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 bool App::shouldRefreshTransactions() const
{ {
// NOTE: this is block-height / dirty driven, NOT interval-gated. It returns true only when a new // NOTE: this is block-height / dirty driven, NOT interval-gated. It returns true only when a new

View File

@@ -71,11 +71,70 @@ extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid,
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cmath> #include <cmath>
#include <cstdint>
#include <filesystem> #include <filesystem>
#include <fstream>
#include <string> #include <string>
#include <vector>
#include <chrono> #include <chrono>
#include <miniz.h>
#include "util/logger.h" #include "util/logger.h"
// Read the just-rendered framebuffer and write it to a PNG (used by the debug screenshot sweep).
// GL: read FBO 0 (RGBA, bottom-up -> miniz flip). DX11: copy the backbuffer to a staging texture,
// map it (BGRA, top-down -> channel-swap, no flip). Alpha forced opaque (the window is transparent).
namespace {
inline void writePng(const std::string& path, const std::vector<uint8_t>& rgba, int w, int h, bool flip) {
size_t len = 0;
void* png = tdefl_write_image_to_png_file_in_memory_ex(rgba.data(), w, h, 4, &len, 6, flip ? MZ_TRUE : MZ_FALSE);
if (!png) return;
std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (out) out.write(reinterpret_cast<const char*>(png), static_cast<std::streamsize>(len));
mz_free(png);
}
#ifdef DRAGONX_USE_DX11
inline void captureFrameToPng(dragonx::platform::DX11Context& dx, const std::string& path) {
IDXGISwapChain1* sc = dx.swapChain(); ID3D11Device* dev = dx.device(); ID3D11DeviceContext* ctx = dx.deviceContext();
if (!sc || !dev || !ctx) return;
ID3D11Texture2D* back = nullptr;
if (FAILED(sc->GetBuffer(0, IID_PPV_ARGS(&back))) || !back) return;
D3D11_TEXTURE2D_DESC bd; back->GetDesc(&bd);
D3D11_TEXTURE2D_DESC sd = bd; sd.Usage = D3D11_USAGE_STAGING; sd.BindFlags = 0;
sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; sd.MiscFlags = 0;
ID3D11Texture2D* staging = nullptr;
if (SUCCEEDED(dev->CreateTexture2D(&sd, nullptr, &staging)) && staging) {
ctx->CopyResource(staging, back);
D3D11_MAPPED_SUBRESOURCE m;
if (SUCCEEDED(ctx->Map(staging, 0, D3D11_MAP_READ, 0, &m))) {
int w = (int)bd.Width, h = (int)bd.Height;
std::vector<uint8_t> px((size_t)w * h * 4);
for (int y = 0; y < h; ++y) {
const uint8_t* s = (const uint8_t*)m.pData + (size_t)y * m.RowPitch;
uint8_t* d = px.data() + (size_t)y * w * 4;
for (int x = 0; x < w; ++x) { d[x*4+0]=s[x*4+2]; d[x*4+1]=s[x*4+1]; d[x*4+2]=s[x*4+0]; d[x*4+3]=255; }
}
ctx->Unmap(staging, 0);
writePng(path, px, w, h, /*flip*/false); // DX11 textures are top-down
}
staging->Release();
}
back->Release();
}
#else
inline void captureFrameToPng(const std::string& path, int w, int h) {
if (w <= 0 || h <= 0) return;
std::vector<uint8_t> px((size_t)w * h * 4);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glReadBuffer(GL_BACK);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px.data());
for (size_t i = 3; i < px.size(); i += 4) px[i] = 255; // opaque
writePng(path, px, w, h, /*flip*/true); // GL default framebuffer is bottom-up
}
#endif
} // namespace
#ifdef _WIN32 #ifdef _WIN32
// Windows crash handler — writes a minidump and log entry before exit // Windows crash handler — writes a minidump and log entry before exit
static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep) static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
@@ -1632,6 +1691,12 @@ int main(int argc, char* argv[])
} }
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
// Debug screenshot sweep — grab the finished frame before present.
if (app.wantsScreenshotThisFrame()) {
captureFrameToPng(dx, app.screenshotSweepPath());
app.onScreenshotCaptured();
}
// Update and Render additional Platform Windows (multi-viewport) // Update and Render additional Platform Windows (multi-viewport)
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{ {
@@ -1655,6 +1720,12 @@ int main(int argc, char* argv[])
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
// Debug screenshot sweep — grab the finished frame before swap.
if (app.wantsScreenshotThisFrame()) {
captureFrameToPng(app.screenshotSweepPath(), (int)io.DisplaySize.x, (int)io.DisplaySize.y);
app.onScreenshotCaptured();
}
// Update and Render additional Platform Windows (multi-viewport) // Update and Render additional Platform Windows (multi-viewport)
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{ {

View File

@@ -150,6 +150,7 @@ struct SettingsPageState {
bool confirm_clear_ztx = false; bool confirm_clear_ztx = false;
bool confirm_delete_blockchain = false; bool confirm_delete_blockchain = false;
bool confirm_rescan = false; bool confirm_rescan = false;
bool debug_options_open = false;
// Rescan dialog: probe the node's available block range so a bootstrapped/pruned node gets a // Rescan dialog: probe the node's available block range so a bootstrapped/pruned node gets a
// runtime rescan from a snapshot-available height instead of the (failing) -rescan-from-genesis. // runtime rescan from a snapshot-available height instead of the (failing) -rescan-from-genesis.
bool rescan_height_detecting = false; bool rescan_height_detecting = false;
@@ -1186,7 +1187,7 @@ void RenderSettingsPage(App* app) {
totalW += cbW(TR("keep_daemon")) + cbSpacing totalW += cbW(TR("keep_daemon")) + cbSpacing
+ cbW(TR("stop_external")) + cbSpacing; + cbW(TR("stop_external")) + cbSpacing;
} }
totalW += cbW(TR("verbose_logging")); // (Verbose logging moved into the Debug options dialog below.)
float scale = (totalW > contentW) ? contentW / totalW : 1.0f; float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
if (scale < 1.0f) ImGui::SetWindowFontScale(scale); if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
float sp = cbSpacing * scale; float sp = cbSpacing * scale;
@@ -1213,19 +1214,18 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemHovered()) if (ImGui::IsItemHovered())
material::Tooltip("%s", TR("tt_stop_external")); material::Tooltip("%s", TR("tt_stop_external"));
} }
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &s_settingsState.verbose_logging)) {
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
material::Tooltip("%s", TR("tt_verbose"));
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f); if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
} }
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Debug options — a button that opens a small dialog (verbose logging + screenshot sweep).
if (TactileButton(TR("debug_options"), ImVec2(0, 0), S.resolveFont("button")))
s_settingsState.debug_options_open = true;
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Collapsible: Tools & Actions... --- // --- Collapsible: Tools & Actions... ---
material::CollapsibleHeader(dl, "##ToolsToggle", TR("tools_actions"), material::CollapsibleHeader(dl, "##ToolsToggle", TR("tools_actions"),
s_settingsState.tools_expanded, contentW, s_settingsState.tools_expanded, contentW,
@@ -2580,6 +2580,26 @@ void RenderSettingsPage(App* app) {
ImGui::EndChild(); // ##SettingsPageScroll ImGui::EndChild(); // ##SettingsPageScroll
// Debug options dialog — verbose logging toggle + the theme/tab screenshot sweep.
if (s_settingsState.debug_options_open) {
if (BeginOverlayDialog(TR("debug_options_title"), &s_settingsState.debug_options_open, 460.0f, 0.94f)) {
if (ImGui::Checkbox(TrId("verbose_logging", "dbg_verbose").c_str(), &s_settingsState.verbose_logging)) {
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_verbose"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::TextWrapped("%s", TR("screenshot_sweep_desc"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton(TR("screenshot_sweep"), ImVec2(0, 0), S.resolveFont("button"))) {
app->startScreenshotSweep();
s_settingsState.debug_options_open = false;
}
EndOverlayDialog();
}
}
// Confirmation dialog for clearing z-tx history // Confirmation dialog for clearing z-tx history
if (s_settingsState.confirm_clear_ztx) { if (s_settingsState.confirm_clear_ztx) {
if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_settingsState.confirm_clear_ztx, 480.0f, 0.94f)) { if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_settingsState.confirm_clear_ztx, 480.0f, 0.94f)) {

View File

@@ -253,6 +253,10 @@ void I18n::loadBuiltinEnglish()
strings_["keep_daemon"] = "Keep daemon running"; strings_["keep_daemon"] = "Keep daemon running";
strings_["stop_external"] = "Stop external daemon"; strings_["stop_external"] = "Stop external daemon";
strings_["verbose_logging"] = "Verbose logging"; strings_["verbose_logging"] = "Verbose logging";
strings_["debug_options"] = "Debug options\xE2\x80\xA6";
strings_["debug_options_title"] = "Debug options";
strings_["screenshot_sweep"] = "Run screenshot sweep";
strings_["screenshot_sweep_desc"] = "Cycles every theme across every tab and saves a screenshot of each to a timestamped folder in the config directory. Runs for a few seconds; the path is shown when it finishes.";
strings_["mine_when_idle"] = "Mine when idle"; strings_["mine_when_idle"] = "Mine when idle";
strings_["setup_wizard"] = "Run Setup Wizard..."; strings_["setup_wizard"] = "Run Setup Wizard...";