// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // In-app, Material-styled folder picker. A modal overlay that lets the user browse the filesystem // and pick a directory (used by the Wallets dialog's "Scan another folder" action instead of a // raw path input field). Single overlay at a time — the overlay framework doesn't nest, so the // caller suppresses its own overlay while FolderPicker::isOpen(). #pragma once #include #include #include #include #include #include #include "imgui.h" #include "../../util/i18n.h" #include "../../util/platform.h" #include "../../embedded/IconsMaterialDesign.h" #include "../layout.h" #include "../material/colors.h" #include "../material/draw_helpers.h" #include "../material/type.h" namespace dragonx { namespace ui { class FolderPicker { public: // Open the picker starting at `startDir` (falls back to home if empty/invalid). `onPick` is // invoked with the chosen absolute path when the user confirms; nothing runs on cancel. static void open(const std::string& startDir, std::function onPick) { namespace fs = std::filesystem; std::error_code ec; if (startDir.empty()) { // Reopen where the user last browsed (nice for repeated scans); else start at home. if (s_dir.empty() || !fs::is_directory(fs::path(s_dir), ec)) navigate(util::Platform::getHomeDir()); else navigate(s_dir); } else { fs::path p(startDir); if (!fs::is_directory(p, ec)) p = fs::path(util::Platform::getHomeDir()); navigate(p.string()); } s_onPick = std::move(onPick); s_open = true; } static bool isOpen() { return s_open; } static void close() { s_open = false; } static void render() { if (!s_open) return; using namespace material; const float dp = Layout::dpiScale(); ImFont* rowFont = Type().body1(); ImFont* icoFont = Type().iconSmall(); ImFont* metaFont = Type().caption(); const float rowH = rowFont->LegacySize + Layout::spacingMd(); const float listPad = Layout::spacingXs(); // The card takes a generous share of the window (a file browser wants room) — not a fixed // row count. The list inside FLEXES to fill whatever height is left above the footer, so the // footer is always pinned + visible and nothing clips, at any window size or DPI. const float vpH = ImGui::GetMainViewport()->Size.y; const float cardH = (vpH * 0.82f) / dp; // logical; the framework re-applies dp + caps at vp-32 OverlayDialogSpec ov; ov.title = TR("picker_title"); ov.p_open = &s_open; ov.style = OverlayStyle::BlurFloat; ov.cardWidth = 760.0f; ov.cardHeight = cardH; ov.idSuffix = "folderpicker"; if (!BeginOverlayDialog(ov)) return; if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_open = false; const float fullW = ImGui::GetContentRegionAvail().x; auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; }; // End-truncate, popping whole UTF-8 code points. auto fit = [&](std::string s, ImFont* f, float maxW){ bool t=false; while (s.size()>1 && tw(f,s)>maxW){ while (s.size()>1 && (static_cast(s.back()) & 0xC0) == 0x80) s.pop_back(); if (s.size()>1) s.pop_back(); t=true; } if (t) s += "\xE2\x80\xA6"; return s; }; // Left-truncate a path (keep the tail — the current folder + nearest parents). auto fitPath = [&](std::string s, ImFont* f, float maxW){ const std::string ell = "\xE2\x80\xA6"; if (tw(f,s) <= maxW) return s; while (s.size()>1 && tw(f, ell+s) > maxW){ s.erase(s.begin()); while (!s.empty() && (static_cast(s.front()) & 0xC0) == 0x80) s.erase(s.begin()); } return ell + s; }; // ---- Path bar: Up + Home + current path strip ----------------------------------------- std::string pendingNav; { const float bh = ImGui::GetFrameHeight(); IconButtonStyle st; st.color = OnSurfaceMedium(); st.hoverBg = StateHover(); st.bgRounding = 5.0f * dp; std::filesystem::path cur(s_dir); const bool hasParent = cur.has_parent_path() && cur.parent_path() != cur; st.tooltip = TR("picker_up"); if (IconButton("##pickerUp", ICON_MD_ARROW_UPWARD, icoFont, ImVec2(bh, bh), st) && hasParent) pendingNav = cur.parent_path().string(); ImGui::SameLine(0.0f, Layout::spacingXs()); IconButtonStyle hst = st; hst.tooltip = TR("picker_home"); if (IconButton("##pickerHome", ICON_MD_HOME, icoFont, ImVec2(bh, bh), hst)) pendingNav = util::Platform::getHomeDir(); ImGui::SameLine(0.0f, Layout::spacingSm()); // Framed path strip ImVec2 sMin = ImGui::GetCursorScreenPos(); const float stripW = ImGui::GetContentRegionAvail().x; ImVec2 sMax(sMin.x + stripW, sMin.y + bh); ImDrawList* wdl = ImGui::GetWindowDrawList(); wdl->AddRectFilled(sMin, sMax, WithAlpha(OnSurface(), 14), 6.0f * dp); wdl->AddRect(sMin, sMax, WithAlpha(OnSurface(), 40), 6.0f * dp, 0, 1.0f); const float tpad = Layout::spacingSm(); wdl->AddText(rowFont, rowFont->LegacySize, ImVec2(sMin.x + tpad, sMin.y + (bh - rowFont->LegacySize) * 0.5f), OnSurface(), fitPath(s_dir, rowFont, stripW - tpad * 2.0f).c_str()); ImGui::Dummy(ImVec2(stripW, bh)); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // ---- Directory list: folders (clickable) then *.dat wallets present (informational) ---- // Flex: the list fills the space between the path bar and the footer block. Reserving a // (slightly generous) footer height keeps the status line + buttons pinned and never clipped. const ImGuiStyle& gstyle = ImGui::GetStyle(); const float footerBlockH = Layout::spacingXs() + metaFont->LegacySize // status line + Layout::spacingSm() + 1.0f + Layout::spacingSm() // separator block + ImGui::GetFrameHeight() // footer buttons + 6.0f * gstyle.ItemSpacing.y; // inter-item gaps float listH = ImGui::GetContentRegionAvail().y - footerBlockH; listH = std::max(listH, rowH * 3.0f + listPad * 2.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(listPad, listPad)); ImGui::PushStyleColor(ImGuiCol_ChildBg, WithAlpha(OnSurface(), 20)); ImGui::BeginChild("##pickerList", ImVec2(fullW, listH), true); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); ImDrawList* dl = ImGui::GetWindowDrawList(); const float rowW = ImGui::GetContentRegionAvail().x; const float padX = Layout::spacingSm(); if (s_subdirs.empty() && s_datfiles.empty()) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Indent(padX); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("picker_empty")); ImGui::Unindent(padX); } // Folder rows for (std::size_t i = 0; i < s_subdirs.size(); ++i) { ImVec2 rMin = ImGui::GetCursorScreenPos(); ImVec2 rMax(rMin.x + rowW, rMin.y + rowH); ImGui::PushID((int)i); const bool clicked = ImGui::InvisibleButton("##dir", ImVec2(rowW, rowH)); const bool hov = ImGui::IsItemHovered(); ImGui::PopID(); if (hov) { dl->AddRectFilled(rMin, rMax, WithAlpha(OnSurface(), 22), 6.0f * dp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } const float midY = (rMin.y + rMax.y) * 0.5f; const float icoSz = rowFont->LegacySize * 1.05f; float x = rMin.x + padX; dl->AddText(icoFont, icoSz, ImVec2(x, midY - icoSz * 0.5f), hov ? Primary() : OnSurfaceMedium(), ICON_MD_FOLDER); x += icoSz + Layout::spacingSm(); // chevron affordance on the right const float chSz = metaFont->LegacySize; const float chX = rMax.x - padX - chSz; if (hov) dl->AddText(icoFont, chSz, ImVec2(chX, midY - chSz * 0.5f), OnSurfaceMedium(), ICON_MD_CHEVRON_RIGHT); std::string nm = fit(s_subdirs[i], rowFont, (chX - Layout::spacingSm()) - x); dl->AddText(rowFont, rowFont->LegacySize, ImVec2(x, midY - rowFont->LegacySize * 0.5f), OnSurface(), nm.c_str()); if (clicked) pendingNav = (std::filesystem::path(s_dir) / s_subdirs[i]).string(); } // Wallet-file rows (informational — dimmed, not clickable) for (std::size_t i = 0; i < s_datfiles.size(); ++i) { ImVec2 rMin = ImGui::GetCursorScreenPos(); const float midY = rMin.y + rowH * 0.5f; const float icoSz = rowFont->LegacySize * 1.05f; float x = rMin.x + padX; dl->AddText(icoFont, icoSz, ImVec2(x, midY - icoSz * 0.5f), OnSurfaceDisabled(), ICON_MD_ACCOUNT_BALANCE_WALLET); x += icoSz + Layout::spacingSm(); std::string nm = fit(s_datfiles[i], rowFont, (rMin.x + rowW - padX) - x); dl->AddText(rowFont, rowFont->LegacySize, ImVec2(x, midY - rowFont->LegacySize * 0.5f), OnSurfaceDisabled(), nm.c_str()); ImGui::Dummy(ImVec2(rowW, rowH)); } ImGui::PopStyleVar(); // ItemSpacing ImGui::EndChild(); ImGui::PopStyleColor(); // ChildBg ImGui::PopStyleVar(); // WindowPadding // ---- Status: how many wallet files are in the selected folder -------------------------- ImGui::Dummy(ImVec2(0, Layout::spacingXs())); if (!s_datfiles.empty()) { char buf[96]; snprintf(buf, sizeof(buf), TR("picker_dat_count"), (int)s_datfiles.size()); Type().textColored(TypeStyle::Caption, Success(), buf); } else { Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("picker_dat_none")); } // ---- Footer: Select this folder / Cancel ---------------------------------------------- ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Separator(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); if (StyledButton(TR("picker_select"), ImVec2(200.0f * dp, 0))) { if (s_onPick) s_onPick(s_dir); s_open = false; } ImGui::SameLine(); if (StyledButton(TR("cancel"), ImVec2(120.0f * dp, 0))) s_open = false; EndOverlayDialog(); if (!pendingNav.empty()) navigate(pendingNav); } private: static void navigate(const std::string& dir) { namespace fs = std::filesystem; std::error_code ec; fs::path p(dir); if (!fs::is_directory(p, ec)) return; fs::path abs = fs::absolute(p, ec); s_dir = ec ? dir : abs.lexically_normal().string(); s_subdirs.clear(); s_datfiles.clear(); fs::directory_iterator it(p, ec), end; int guard = 0; for (; !ec && it != end && guard < 4000; it.increment(ec), ++guard) { std::error_code fec; std::string name = it->path().filename().string(); if (name.empty()) continue; if (it->is_directory(fec)) { s_subdirs.push_back(name); } else if (it->is_regular_file(fec)) { if (name.size() > 4 && name.substr(name.size() - 4) == ".dat") s_datfiles.push_back(name); } } auto ci = [](const std::string& a, const std::string& b){ return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), [](char x, char y){ return std::tolower((unsigned char)x) < std::tolower((unsigned char)y); }); }; std::sort(s_subdirs.begin(), s_subdirs.end(), ci); std::sort(s_datfiles.begin(), s_datfiles.end(), ci); } static inline bool s_open = false; static inline std::string s_dir; static inline std::vector s_subdirs; static inline std::vector s_datfiles; static inline std::function s_onPick; }; } // namespace ui } // namespace dragonx