From 587f8baebffce5416e99516d5ff0db4f61e103ff Mon Sep 17 00:00:00 2001 From: Pixelplanet Date: Thu, 15 Oct 2026 15:09:10 +0200 Subject: [PATCH] Non-blocking navigation (no main-thread scan join); downloads dialog buttons + modal confirm with folder size; darker model; bigger badge --- shaders/model.frag | 4 +- src/app/app.cpp | 55 ++++++++++++++++------- src/gfx/vulkan_renderer.cpp | 37 ++++++++++----- src/gfx/vulkan_renderer.hpp | 8 +++- src/ui/browser.cpp | 90 +++++++++++++++++++++++++++++++++---- src/ui/browser.hpp | 17 ++++++- 6 files changed, 172 insertions(+), 39 deletions(-) diff --git a/shaders/model.frag b/shaders/model.frag index 6027f43..a5c7732 100644 --- a/shaders/model.frag +++ b/shaders/model.frag @@ -28,7 +28,7 @@ void main() { // with plain diffuse only (no specular/rim) so there is no shimmer, while the // printed button labels still read. float diff = max(dot(N, L), 0.0); - float amb = 0.05 + 0.05 * (N.y * 0.5 + 0.5); - outColor = vec4(tex.rgb * (amb + 0.40 * diff) * pc.color.rgb, + float amb = 0.03 + 0.03 * (N.y * 0.5 + 0.5); + outColor = vec4(tex.rgb * (amb + 0.30 * diff) * pc.color.rgb, tex.a * pc.color.a); } diff --git a/src/app/app.cpp b/src/app/app.cpp index bf7fcd1..f6f3bba 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -542,12 +542,15 @@ int App::run() { // Downloads window: job list + Pause/Stop/Clear actions. Rows are // clickable (completed files play, actions run). std::vector drows; - std::vector dact; // 1 pause/resume, 2 stop, 3 clear, 100+i play + std::vector dact; // 1 pause/resume, 2 stop, 3 clear, 100+i play + std::vector dkind; // 0 text, 1 button if (browser_.downloadActive()) { - drows.push_back(browser_.downloadPaused() ? "[ Resume ]" : "[ Pause ]"); + drows.push_back(browser_.downloadPaused() ? "Resume" : "Pause"); dact.push_back(1); - drows.push_back("[ Stop ]"); + dkind.push_back(1); + drows.push_back("Stop"); dact.push_back(2); + dkind.push_back(1); } { const auto& jobs = browser_.downloadJobs(); @@ -563,8 +566,7 @@ int App::run() { nm.c_str(), pct, j.bytes / 1048576.0, j.total / 1048576.0); } else if (j.state == 1) { - std::snprintf(buf, sizeof(buf), "%s done (trigger to play)", - nm.c_str()); + std::snprintf(buf, sizeof(buf), "%s done", nm.c_str()); } else if (j.state == 3) { std::snprintf(buf, sizeof(buf), "%s stopped", nm.c_str()); } else { @@ -572,17 +574,21 @@ int App::run() { } drows.push_back(buf); dact.push_back(j.state == 1 ? (100 + static_cast(i)) : 0); + dkind.push_back(0); } } - drows.push_back("[ Clear finished ]"); + drows.push_back("Clear finished"); dact.push_back(3); + dkind.push_back(1); const bool dlWin = downloadsWindow_ && !browser_.isOpen() && - !settings_.isOpen(); + !settings_.isOpen() && pendingDl_.empty(); + renderer_.setDownloadsModal(false); renderer_.setDownloads(dlWin, "Downloads", drows, browser_.downloadActive() ? browser_.downloadText() - : "Trigger: select B: close"); + : "Point + Trigger to select B: close", + dkind); if (dlWin && in.select) { const int hov = renderer_.downloadsHovered(); if (hov >= 0 && hov < static_cast(dact.size())) { @@ -607,23 +613,41 @@ int App::run() { } } - // Folder-download confirmation overlay (overrides the downloads panel). + // Folder-download confirmation: a modal with the size and two buttons. if (!pendingDl_.empty()) { + bool confirmNow = false; if (pendingDlFresh_) { pendingDlFresh_ = false; // ignore the press that opened it } else if (in.togglePlay) { - browser_.startDownload(pendingDl_, pendingDlName_, true); - pendingDl_.clear(); + confirmNow = true; } else if (in.browserToggle) { pendingDl_.clear(); } if (!pendingDl_.empty()) { - std::vector crows{ - "Folder: " + pendingDlName_, - "Download this folder and everything in it?"}; + char sz[96]; + std::snprintf(sz, sizeof(sz), "Size: %.2f GB%s", + browser_.measuredSize() / 1073741824.0, + browser_.measuring() ? " (scanning...)" : ""); + std::vector crows{"Download this folder?", + pendingDlName_, sz, "Download", + "Cancel"}; + std::vector ckind{0, 0, 0, 2, 1}; + renderer_.setDownloadsModal(true); renderer_.setDownloads(true, "Confirm Download", crows, - "A: Download B: Cancel"); + "A: Download B: Cancel", ckind); + const int hov = renderer_.downloadsHovered(); + if (in.select && hov == 3) { + confirmNow = true; + } else if (in.select && hov == 4) { + pendingDl_.clear(); + } } + if (confirmNow && !pendingDl_.empty()) { + browser_.startDownload(pendingDl_, pendingDlName_, true); + pendingDl_.clear(); + } + } else { + renderer_.setDownloadsModal(false); } // Publish the browser panel first so the pointer can raycast it. if (browser_.isOpen()) { @@ -793,6 +817,7 @@ int App::run() { pendingDl_ = fe->path; pendingDlName_ = fe->label; pendingDlFresh_ = true; + browser_.measureFolder(fe->path); // size for the dialog } else { browser_.downloadFocused(); } diff --git a/src/gfx/vulkan_renderer.cpp b/src/gfx/vulkan_renderer.cpp index 797806f..2f6b150 100644 --- a/src/gfx/vulkan_renderer.cpp +++ b/src/gfx/vulkan_renderer.cpp @@ -1601,16 +1601,16 @@ void VulkanRenderer::renderView(uint32_t viewIndex, uint32_t imageIndex, const float badgeY = kBarY + kBtnHalfH * 0.55f; if (downloadsAllDone_) { const float tickBg[4] = {0.10f, 0.65f, 0.25f, 1.0f}; - drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.62f, - kBtnHalfH * 0.62f, UiShape::RoundedBox, tickBg, + drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.85f, + kBtnHalfH * 0.85f, UiShape::RoundedBox, tickBg, 0.50f, 0.0f, nullptr); - drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.44f, - kBtnHalfH * 0.44f, UiShape::Checkmark, + drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.62f, + kBtnHalfH * 0.62f, UiShape::Checkmark, iconCol); } else { const float dotCol[4] = {0.30f, 0.75f, 1.0f, 1.0f}; - drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.34f, - kBtnHalfH * 0.34f, UiShape::Cursor, dotCol); + drawUiElement(vpPanel, badgeX, badgeY, kBtnHalfH * 0.45f, + kBtnHalfH * 0.45f, UiShape::Cursor, dotCol); } } } @@ -1860,15 +1860,32 @@ void VulkanRenderer::renderView(uint32_t viewIndex, uint32_t imageIndex, const float rowH = kRowH; const float panelBg[4] = {0.02f, 0.03f, 0.05f, 0.94f}; const float panelBorder[4] = {0.25f, 0.45f, 0.65f, 0.85f}; + if (downloadsModal_) { // dim backdrop so a confirmation reads as a modal + const float backdrop[4] = {0.0f, 0.0f, 0.0f, 0.55f}; + drawUiElement(vpPanel, 0.0f, 0.0f, panelW * 1.35f, panelTop * 1.25f, + UiShape::RoundedBox, backdrop, 0.06f, 0.0f, nullptr); + } drawUiElement(vpPanel, 0.0f, 0.0f, panelW, panelTop, UiShape::RoundedBox, panelBg, 0.05f, 0.015f, panelBorder); const float hlCol[4] = {0.18f, 0.45f, 0.85f, 0.85f}; + const float btnBg[4] = {0.10f, 0.14f, 0.22f, 0.95f}; + const float btnBgHover[4] = {0.18f, 0.42f, 0.82f, 0.95f}; + const float btnBorder[4] = {0.30f, 0.45f, 0.65f, 0.80f}; + const float primBg[4] = {0.12f, 0.50f, 0.22f, 0.95f}; for (std::size_t i = 0; i < downloadsRows_.size(); ++i) { - if (static_cast(i) == downloadsHovered_) { - const float ry = panelTop - 0.2f - static_cast(i) * rowH; + const float ry = panelTop - 0.2f - static_cast(i) * rowH; + const uint8_t kind = + (i < downloadsKinds_.size()) ? downloadsKinds_[i] : 0; + const bool hovered = (static_cast(i) == downloadsHovered_); + if (kind != 0) { + const float* bg = hovered ? btnBgHover : (kind == 2 ? primBg : btnBg); + drawUiElement(vpPanel, 0.0f, ry + rowH * 0.30f, panelW - 0.06f, + rowH * 0.50f, UiShape::RoundedBox, bg, 0.25f, 0.03f, + btnBorder); + } else if (hovered) { drawUiElement(vpPanel, 0.0f, ry + rowH * 0.28f, panelW - 0.04f, - rowH * 0.44f, UiShape::RoundedBox, hlCol, 0.20f, - 0.0f, nullptr); + rowH * 0.44f, UiShape::RoundedBox, hlCol, 0.20f, 0.0f, + nullptr); } } vkCmdBindPipeline(commandBuffer_, VK_PIPELINE_BIND_POINT_GRAPHICS, textPipeline_); diff --git a/src/gfx/vulkan_renderer.hpp b/src/gfx/vulkan_renderer.hpp index 306434a..d92cb04 100644 --- a/src/gfx/vulkan_renderer.hpp +++ b/src/gfx/vulkan_renderer.hpp @@ -119,12 +119,16 @@ public: // shown while downloads run (and briefly after) with the browser closed. void setDownloads(bool visible, const std::string& title, const std::vector& rows, - const std::string& status) { + const std::string& status, + const std::vector& kinds = {}) { downloadsVisible_ = visible; downloadsTitle_ = title; downloadsRows_ = rows; downloadsStatus_ = status; + downloadsKinds_ = kinds; // 0 = item text, 1 = button, 2 = primary button } + // Draws a stronger modal backdrop behind the panel (for confirmations). + void setDownloadsModal(bool modal) { downloadsModal_ = modal; } // The downloads-window row under the pointer, or -1 if none. int downloadsHovered() const { return downloadsHovered_; } @@ -292,6 +296,8 @@ private: std::string downloadsStatus_; bool downloadsActive_ = false; bool downloadsAllDone_ = false; + bool downloadsModal_ = false; + std::vector downloadsKinds_; int downloadsHovered_ = -1; // Flat-screen placement in the app reference space (updated by recenter and diff --git a/src/ui/browser.cpp b/src/ui/browser.cpp index b7030b4..99395cf 100644 --- a/src/ui/browser.cpp +++ b/src/ui/browser.cpp @@ -267,6 +267,15 @@ void Browser::scanSmbInto(const std::string& uri, ScanResult& out) { // Starts a background scan so SMB discovery (NetBIOS + mDNS, seconds) and large // directories never freeze the render loop. poll() publishes the result. void Browser::beginScan(const std::string& location, bool isSmb) { + if (scanning_.load()) { + // A scan is already running (possibly waiting on the SMB lock held by a + // download). Defer this request rather than joining on the main thread, + // which would freeze the UI until the download's read returns. + pendingScanLoc_ = location; + pendingScanSmb_ = isSmb; + hasPendingScan_ = true; + return; + } if (scanThread_.joinable()) scanThread_.join(); // previous scan has finished scanReady_.store(false); scanning_.store(true); @@ -360,6 +369,7 @@ const Browser::Entry* Browser::focused() const { Browser::~Browser() { if (scanThread_.joinable()) scanThread_.join(); if (dlThread_.joinable()) dlThread_.join(); + if (measureThread_.joinable()) measureThread_.join(); } void Browser::startDownload(const std::string& uri, const std::string& nameIn, @@ -711,19 +721,81 @@ void Browser::clearFinished() { status_ = "Cleared finished downloads"; } +void Browser::measureFolder(const std::string& uri) { +#ifdef PIXELVR_HAVE_SMB + if (measuring_.load()) return; + if (measureThread_.joinable()) measureThread_.join(); + measuredSize_.store(0); + measuring_.store(true); + measureThread_ = std::thread([this, uri]() { + long long total = 0; + std::function sum = [&](const std::string& src) { + int d = -1; + { + std::lock_guard smb(smbGlobalLock()); + d = smbc_opendir(src.c_str()); + } + if (d < 0) return; + for (;;) { + std::string nm; + int typ = 0; + { + std::lock_guard smb(smbGlobalLock()); + struct smbc_dirent* de = smbc_readdir(d); + if (de == nullptr) break; + nm = de->name; + typ = de->smbc_type; + } + if (nm.empty() || nm == "." || nm == "..") continue; + std::string s = src; + if (!s.empty() && s.back() != '/') s += '/'; + s += nm; + if (typ == SMBC_DIR) { + sum(s); + } else { + struct stat st{}; + { + std::lock_guard smb(smbGlobalLock()); + smbc_stat(s.c_str(), &st); + } + total += static_cast(st.st_size); + measuredSize_.store(total); // live-updating estimate + } + } + { + std::lock_guard smb(smbGlobalLock()); + smbc_closedir(d); + } + }; + sum(uri); + measuredSize_.store(total); + measuring_.store(false); + }); +#else + (void)uri; +#endif +} + Browser::Result Browser::poll(std::string& playPath) { // Publish a finished async directory scan (SMB discovery / large dirs). if (scanReady_.exchange(false)) { if (scanThread_.joinable()) scanThread_.join(); - std::lock_guard lk(scanMutex_); - entries_ = std::move(scanResult_.entries); - title_ = std::move(scanResult_.title); - status_ = std::move(scanResult_.status); - location_ = scanResult_.location; - inSmb_ = scanResult_.inSmb; - selected_ = 0; - lastLocation_ = location_; // remember for the next open() - lastInSmb_ = inSmb_; + { + std::lock_guard lk(scanMutex_); + entries_ = std::move(scanResult_.entries); + title_ = std::move(scanResult_.title); + status_ = std::move(scanResult_.status); + location_ = scanResult_.location; + inSmb_ = scanResult_.inSmb; + selected_ = 0; + lastLocation_ = location_; // remember for the next open() + lastInSmb_ = inSmb_; + } + // Run a navigation the user requested while this scan was busy. + if (hasPendingScan_) { + hasPendingScan_ = false; + beginScan(pendingScanLoc_, pendingScanSmb_); + } } if (dlActive_.load()) { const long long b = dlBytes_.load(); diff --git a/src/ui/browser.hpp b/src/ui/browser.hpp index 5bb9ab0..f451da3 100644 --- a/src/ui/browser.hpp +++ b/src/ui/browser.hpp @@ -99,6 +99,11 @@ public: // Removes finished/stopped/failed jobs (deleting any partial files). void clearFinished(); + // Background size probe for a folder (shown on the download confirmation). + void measureFolder(const std::string& uri); + bool measuring() const { return measuring_.load(); } + long long measuredSize() const { return measuredSize_.load(); } + ~Browser(); Browser(const Browser&) = delete; Browser& operator=(const Browser&) = delete; @@ -114,6 +119,11 @@ private: }; void beginScan(const std::string& location, bool isSmb); // async navigate void runScan(std::string location, bool isSmb); // worker body + // Navigation requested while a scan was already running (deferred so the + // main thread never blocks joining a scan stuck on the SMB lock). + std::string pendingScanLoc_; + bool pendingScanSmb_ = false; + bool hasPendingScan_ = false; void scanLocalInto(const std::string& dir, ScanResult& out); void scanSmbInto(const std::string& uri, ScanResult& out); void startDownloadFile(const std::string& uri, const std::string& local); @@ -133,8 +143,7 @@ private: // Async directory scan (SMB discovery / large dirs) off the main thread. std::thread scanThread_; std::atomic scanning_{false}; - std::atomic scanReady_{false}; // results waiting to publish - std::mutex scanMutex_; // guards scanResult_ + std::atomic scanReady_{false}; // results waiting to publish std::mutex scanMutex_; // guards scanResult_ ScanResult scanResult_; // Async SMB download. @@ -152,6 +161,10 @@ private: std::atomic dlPaused_{false}; std::atomic dlAbort_{false}; std::atomic dlStopped_{false}; + // Folder size probe (background). + std::thread measureThread_; + std::atomic measuring_{false}; + std::atomic measuredSize_{0}; double dlSpeed_ = 0.0; // EMA bytes/sec for the status readout long long speedBytes_ = 0; std::chrono::steady_clock::time_point speedTime_{};