diff --git a/src/app.cpp b/src/app.cpp index a7a08b2..caac6f4 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1243,6 +1243,10 @@ void App::render() // Process deferred encryption from wizard (runs in background) 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 // 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 diff --git a/src/app.h b/src/app.h index 2803c02..9e52897 100644 --- a/src/app.h +++ b/src/app.h @@ -308,7 +308,16 @@ public: // UI navigation void setCurrentPage(ui::NavPage 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) void showImportKeyDialog() { show_import_key_ = true; } void showExportKeyDialog() { show_export_key_ = true; } @@ -638,6 +647,21 @@ private: ui::NavPage prev_page_ = ui::NavPage::Overview; float page_alpha_ = 1.0f; // 0→1 fade on page switch 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 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 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 96f5902..5bac3f1 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -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 #include #include +#include #include #include @@ -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(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; + + // 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(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(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(); +} + bool App::shouldRefreshTransactions() const { // NOTE: this is block-height / dirty driven, NOT interval-gated. It returns true only when a new diff --git a/src/main.cpp b/src/main.cpp index 1c25af3..e0872a9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -71,11 +71,70 @@ extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid, #include #include #include +#include #include +#include #include +#include #include +#include #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& 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(png), static_cast(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 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 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 // Windows crash handler — writes a minidump and log entry before exit static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep) @@ -1632,6 +1691,12 @@ int main(int argc, char* argv[]) } 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) if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { @@ -1655,6 +1720,12 @@ int main(int argc, char* argv[]) glClear(GL_COLOR_BUFFER_BIT); 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) if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index bb2f2fb..39c2e2c 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -150,6 +150,7 @@ struct SettingsPageState { bool confirm_clear_ztx = false; bool confirm_delete_blockchain = 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 // runtime rescan from a snapshot-available height instead of the (failing) -rescan-from-genesis. bool rescan_height_detecting = false; @@ -1186,7 +1187,7 @@ void RenderSettingsPage(App* app) { totalW += cbW(TR("keep_daemon")) + 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; if (scale < 1.0f) ImGui::SetWindowFontScale(scale); float sp = cbSpacing * scale; @@ -1213,19 +1214,18 @@ void RenderSettingsPage(App* app) { if (ImGui::IsItemHovered()) 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); } 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... --- material::CollapsibleHeader(dl, "##ToolsToggle", TR("tools_actions"), s_settingsState.tools_expanded, contentW, @@ -2580,6 +2580,26 @@ void RenderSettingsPage(App* app) { 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 if (s_settingsState.confirm_clear_ztx) { if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_settingsState.confirm_clear_ztx, 480.0f, 0.94f)) { diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index c8f7f19..f480c59 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -253,6 +253,10 @@ void I18n::loadBuiltinEnglish() strings_["keep_daemon"] = "Keep daemon running"; strings_["stop_external"] = "Stop external daemon"; 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_["setup_wizard"] = "Run Setup Wizard...";