feat(wallets): in-app folder picker + roomier, full-width list controls

Wallets dialog refinements:
- More space between wallet cards (cardGap: spacingSm -> spacingMd).
- "Scan another folder" is now always visible as a full-width button (no
  expandable toggle) that opens the new picker.
- The Create-wallet row and the scan button span the full list width, so their
  right edges align with the wallet cards / Open|Import buttons.

New in-app folder picker (folder_picker.h): a Material-styled modal replacing
the raw path input. Browse the filesystem (Up / Home / click a sub-folder),
see the *.dat wallet files present + a count, and pick a folder to scan. The
picker takes over the modal surface while open (the overlay framework doesn't
nest); it remembers the last browsed location across opens. Fixed-height card
(no auto-height feedback), UTF-8-aware truncation, full-node icons confirmed
in the embedded MaterialIcons range. Added a modal-folder-picker sweep surface;
verified at 100% + 150%, dark + light.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 14:11:39 -05:00
parent 5eff3adc3c
commit 06c8b99bce
4 changed files with 317 additions and 29 deletions

View File

@@ -29,6 +29,7 @@
#include "../material/colors.h"
#include "../material/draw_helpers.h"
#include "../material/type.h"
#include "folder_picker.h"
namespace dragonx {
namespace ui {
@@ -39,13 +40,15 @@ public:
s_open = true;
s_app = app;
s_needScan = true;
s_newFolder[0] = '\0';
}
static void hide() { s_open = false; }
static void render() {
if (!s_open || !s_app) return;
// The folder picker (opened by "Scan another folder") takes over the modal surface while
// it's up — the overlay framework doesn't nest, so only one overlay renders at a time.
if (FolderPicker::isOpen()) { FolderPicker::render(); return; }
using namespace material;
App* app = s_app;
const float dp = Layout::dpiScale();
@@ -62,16 +65,16 @@ public:
const int visRows = std::min(std::max(1, (int)s_rows.size()), kMaxVisibleRows);
const float cardPadY = Layout::spacingMd(); // roomier cards (was spacingSm)
const float walRowH = cardPadY * 2.0f + nameFont->LegacySize + Layout::spacingSm() + metaFont->LegacySize;
const float cardGap = Layout::spacingSm();
const float cardGap = Layout::spacingMd(); // more breathing room between wallet cards
// ItemSpacing is zeroed inside the list child, so the content is exactly cards + inter-card
// gaps; a small bottom pad keeps the last card off the clip edge (no spurious scrollbar).
const float listH = visRows * walRowH + (float)std::max(0, visRows - 1) * cardGap + Layout::spacingSm();
const float ctrlRow = ImGui::GetFrameHeightWithSpacing();
const float capRow = Type().caption()->LegacySize + style.ItemSpacing.y;
const float belowH = 3.0f * Layout::spacingSm()
const float belowH = 4.0f * Layout::spacingSm()
+ (capRow + ctrlRow) // Create label + input row
+ (s_showAddFolder ? (capRow + ctrlRow) : capRow) // folder row OR toggle link
+ ctrlRow // full-width "scan folder" button
+ (style.ItemSpacing.y + 1.0f) // separator
+ ctrlRow // footer buttons
+ 6.0f * style.ItemSpacing.y; // uncounted inter-item gaps
@@ -206,12 +209,13 @@ public:
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// ---- Create a new wallet (primary) ----------------------------------------------------
// ---- Create a new wallet (primary) — the input + button span the full list width -----
const float createBtnW = 140.0f * dp;
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_new_label"));
ImGui::SetNextItemWidth(-150.0f * dp);
ImGui::SetNextItemWidth(listW - createBtnW - style.ItemSpacing.x);
ImGui::InputTextWithHint("##newWalletName", TR("wallets_new_hint"), s_newName, sizeof(s_newName));
ImGui::SameLine();
if (StyledButton(TR("wallets_create"), ImVec2(120.0f * dp, 0))) {
if (StyledButton(TR("wallets_create"), ImVec2(createBtnW, 0))) {
std::string name = normalizeWalletName(s_newName);
std::error_code ec;
if (name.empty()) {
@@ -228,29 +232,17 @@ public:
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// ---- Add-folder tucked behind a toggle so it doesn't compete with Create --------------
if (!s_showAddFolder) {
ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium());
ImGui::TextUnformatted(TR("wallets_add_folder_toggle"));
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsItemClicked()) s_showAddFolder = true;
} else {
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("wallets_add_folder"));
ImGui::SetNextItemWidth(-150.0f * dp);
ImGui::InputTextWithHint("##walletFolder", TR("wallets_folder_hint"), s_newFolder, sizeof(s_newFolder));
ImGui::SameLine();
if (StyledButton(TR("wallets_add"), ImVec2(120.0f * dp, 0))) {
// ---- Scan another folder — always visible, full-width, opens the in-app folder picker -
if (StyledButton(TR("wallets_scan_folder"), ImVec2(listW, 0))) {
FolderPicker::open("", [app](const std::string& dir) {
std::error_code ec;
std::string dir = s_newFolder;
if (!dir.empty() && std::filesystem::is_directory(dir, ec)) {
if (app->walletIndex().addExtraFolder(dir)) app->walletIndex().save();
s_newFolder[0] = '\0';
s_needScan = true;
} else {
Notifications::instance().warning(TR("wallets_folder_invalid"));
}
}
});
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
@@ -352,9 +344,7 @@ private:
static inline bool s_open = false;
static inline App* s_app = nullptr;
static inline bool s_needScan = false;
static inline char s_newFolder[512] = "";
static inline char s_newName[128] = "";
static inline bool s_showAddFolder = false;
static inline std::vector<WalletRow> s_rows;
};