fix(ui): make HiDPI-overflowing containers scrollable — wizard, overlay dialogs, balance recent-tx
At font_scale 1.5 (dpiScale 1.5) three fixed, non-scrolling containers clipped content off the bottom with no scroll escape: - First-run wizard: the hand-drawn cards grow ~1.5x past the fixed window, pushing Continue / Encrypt & Continue / Skip off-screen (a setup blocker). Inject a wheel-driven scroll offset into the layout seed + a scroll indicator; gate the wheel on !IsPopupOpen + NoPopupHierarchy so an open combo popup does not scroll the wizard behind it. No-op at 1.0x. - Overlay dialogs (BeginOverlayDialog): auto-height cards taller than the viewport (About, Request Payment) ran their footer off the bottom. Add a sticky per-open overflow flag that clamps the card to the viewport and makes the content child scrollable; short dialogs still center unchanged. Give the nested settings clear-history confirm its own idSuffix so it can't inherit the parent dialog's overflow state or collide on the child window id. - Balance Recent Transactions: the dp-scaled address card evicted the recent-tx list off the non-scrolling tab host. Cap the card inside RenderSharedAddressList against the space that actually remains (minus a caller-provided reserve) so the section below stays on-screen — covers all 10 balance layouts. Verified at font_scale 1.5 across full-node + Lite + Windows (ctest green) and an adversarial diff review (two low-severity regressions found + fixed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1403,6 +1403,7 @@ struct OverlayCardState {
|
||||
int stableCount = 0; // consecutive frames the height held steady (within 1px)
|
||||
int appearFrames = 0; // frames since (re)appearing while still hidden — a safety cap
|
||||
bool shown = false; // revealed (centered) at least once this open; don't re-hide after
|
||||
bool overflow = false; // content once exceeded the viewport → clamp to viewport + scroll (sticky/open)
|
||||
};
|
||||
inline std::unordered_map<std::string, OverlayCardState> g_overlayCardHeights;
|
||||
inline std::string g_overlayCurrentKey;
|
||||
@@ -1528,6 +1529,7 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
float cardX = vp_pos.x + (vp_size.x - cardWidth) * 0.5f;
|
||||
float cardY, cardBottomY;
|
||||
bool hideForMeasure = false; // true on an auto-height dialog's first (unmeasured) frame
|
||||
bool autoOverflow = false; // auto-height content taller than the viewport → clamp + scroll
|
||||
const bool fixedHeight = (spec.cardHeight > 0.0f);
|
||||
if (fixedHeight) {
|
||||
float cardH = std::min(spec.cardHeight * dp, vp_size.y - 32.0f);
|
||||
@@ -1537,9 +1539,16 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
} else {
|
||||
g_overlayCurrentKey = childId;
|
||||
OverlayCardState& cs = g_overlayCardHeights[childId];
|
||||
if (scrimAppearing) { cs.shown = false; cs.stableCount = 0; cs.appearFrames = 0; }
|
||||
if (scrimAppearing) { cs.shown = false; cs.stableCount = 0; cs.appearFrames = 0; cs.overflow = false; }
|
||||
if (!cs.shown) cs.appearFrames++;
|
||||
const float measuredH = cs.height;
|
||||
const float maxCardH = vp_size.y - 32.0f;
|
||||
// Once the measured content is taller than the viewport, lock the card to the viewport height and
|
||||
// let its content child scroll (autoOverflow) so the footer/actions stay reachable. Sticky for this
|
||||
// open: clamping makes next frame's measured height the clamped value, so re-deciding from it would
|
||||
// oscillate — decide once and hold until the dialog re-opens.
|
||||
if (measuredH > maxCardH) cs.overflow = true;
|
||||
autoOverflow = cs.overflow;
|
||||
// Reveal once the measured height has settled (auto-resize converges in ~2 frames) or it's
|
||||
// already been shown this open (don't re-hide on a mid-dialog content change); a frame cap
|
||||
// guarantees a pathological ever-changing height can't hide the dialog forever.
|
||||
@@ -1547,11 +1556,18 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
(cs.shown || cs.stableCount >= 1 || cs.appearFrames >= 8);
|
||||
if (ready) {
|
||||
cs.shown = true;
|
||||
// Center the measured content; if it's taller than the window, anchor at the top margin.
|
||||
cardY = (measuredH < vp_size.y - 32.0f)
|
||||
? vp_pos.y + (vp_size.y - measuredH) * 0.5f
|
||||
: vp_pos.y + 16.0f;
|
||||
cardBottomY = cardY + measuredH;
|
||||
if (autoOverflow) {
|
||||
// Taller than the screen: top-anchor at the 16px margin, clamp to the viewport; the
|
||||
// content child (below) becomes the scroll region so the footer/actions stay reachable.
|
||||
cardY = vp_pos.y + 16.0f;
|
||||
cardBottomY = cardY + maxCardH;
|
||||
} else {
|
||||
// Center the measured content; if it's taller than the window, anchor at the top margin.
|
||||
cardY = (measuredH < maxCardH)
|
||||
? vp_pos.y + (vp_size.y - measuredH) * 0.5f
|
||||
: vp_pos.y + 16.0f;
|
||||
cardBottomY = cardY + measuredH;
|
||||
}
|
||||
} else {
|
||||
// Still settling: lay the content out (so the auto-height child gets measured) but keep
|
||||
// the card hidden (hideForMeasure below) so it never flashes off-center — it appears,
|
||||
@@ -1584,14 +1600,21 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, floating ? 20.0f : 16.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, floating ? ImVec2(28, 20) : ImVec2(28, 24));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0)); // transparent (glass/blur behind)
|
||||
ImGuiChildFlags cflags = ImGuiChildFlags_AlwaysUseWindowPadding | (fixedHeight ? 0 : ImGuiChildFlags_AutoResizeY);
|
||||
// A card with a known height is a fixed frame (fixed-height dialogs, and auto-height dialogs whose
|
||||
// content overflowed the viewport); otherwise the child auto-resizes to its content.
|
||||
const bool clampedCard = fixedHeight || autoOverflow;
|
||||
ImGuiChildFlags cflags = ImGuiChildFlags_AlwaysUseWindowPadding | (clampedCard ? 0 : ImGuiChildFlags_AutoResizeY);
|
||||
// NoScrollWithMouse (not just NoScrollbar): a modal is a fixed frame — the wheel must never drift
|
||||
// the WHOLE card. If content marginally overflows a fixed card, the wheel would otherwise scroll
|
||||
// the entire dialog (title + footer and all). Inner scroll regions (lists, notes) still scroll on
|
||||
// their own; auto-height cards resize to content so they never overflow anyway.
|
||||
// their own; auto-height cards resize to content so they normally never overflow — EXCEPT when the
|
||||
// content is taller than the viewport (autoOverflow), where the card itself IS the scroll region.
|
||||
ImGuiWindowFlags childScroll = autoOverflow
|
||||
? ImGuiWindowFlags_None
|
||||
: (ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
bool childVisible = ImGui::BeginChild(childId.c_str(),
|
||||
ImVec2(cardWidth, fixedHeight ? (cardBottomY - cardY) : 0.0f),
|
||||
cflags, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ImVec2(cardWidth, clampedCard ? (cardBottomY - cardY) : 0.0f),
|
||||
cflags, childScroll);
|
||||
// Floating (portfolio-style) cards: the padding applies to this content child only, so pop it
|
||||
// now (nested children mustn't inherit it), and center button labels. Net style-var count stays
|
||||
// at 2 (ChildRounding + ButtonTextAlign) so EndOverlayDialog's PopStyleVar(2) is unchanged.
|
||||
|
||||
Reference in New Issue
Block a user