fix(wallets): cap card at viewport + flex list so many wallets never clip

The wallets dialog hand-computed a content-sized card height. With many wallets
on a small / HiDPI (150%) window the computed height could exceed the viewport,
and the framework clips a too-tall card — footer and all.

Cap the card at 0.86 of the viewport height. When capped, the wallet list flexes
to the space left above the create/scan/footer controls and scrolls internally,
so those controls stay pinned and visible; uncapped (the common case, few
wallets) nothing changes and there's no spurious scrollbar. Add a
modal-wallets-many sweep surface (8 wallets, self-cleaning teardown) to exercise
the capped-scroll path. Verified at 100% + 150%.

This is the one real fixed-height clip risk from the modal audit; the other 44
modals use auto-height (grow-to-fit, capped at a viewport ratio) and were
confirmed non-clipping — e.g. the 24-word seed-backup grid renders fully at 150%
in a 720px window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 14:47:59 -05:00
parent f8fbc32463
commit 3df516be70
2 changed files with 46 additions and 2 deletions

View File

@@ -81,7 +81,14 @@ public:
const float headH = Type().h6()->LegacySize + Layout::spacingXs() // framework h6 title
+ Type().caption()->LegacySize + Layout::spacingSm(); // intro + gap
const float padV = 48.0f; // content-child padding (top+bottom) + margin
const float cardH = (headH + listH + belowH + padV) / dp;
// Size to content, but cap at a viewport fraction: with many wallets on a small / HiDPI
// screen the content-sized card could exceed the window (the framework would clip a too-tall
// card, footer and all). When capped, the wallet list flexes + scrolls and the controls
// below it stay pinned. In the common case (few wallets) nothing changes.
const float desiredH = headH + listH + belowH + padV; // physical
const float vpH = ImGui::GetMainViewport()->Size.y;
const bool capped = desiredH > vpH * 0.86f;
const float cardH = std::min(desiredH, vpH * 0.86f) / dp;
OverlayDialogSpec ov;
ov.title = TR("wallets_title");
@@ -98,8 +105,13 @@ public:
// ---- Wallet cards (Material-style rows; no table) -------------------------------------
const float listW = ImGui::GetContentRegionAvail().x;
// Flex only when the card was viewport-capped: give the list the space left above the
// create/scan/footer controls so it scrolls internally and those stay visible. Uncapped,
// it equals the content height exactly (no spurious scrollbar — the common case).
float listHFit = listH;
if (capped) listHFit = std::max(walRowH, ImGui::GetContentRegionAvail().y - belowH);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
ImGui::BeginChild("##walletList", ImVec2(listW, listH), false);
ImGui::BeginChild("##walletList", ImVec2(listW, listHFit), false);
ImGui::PopStyleVar();
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); // inter-card gaps are explicit
ImDrawList* dl = ImGui::GetWindowDrawList();