fix(modals): hold dialog hidden until its height settles (no reveal-time slide)
An ImGui auto-resize child reports a too-small height on its first render and the true height a frame later, so centering on last frame's height made the card visibly slide for ~2 frames as it converged. Hiding only the very first frame wasn't enough — the next frame or two still moved. Track per-dialog height stability: keep the card hidden (laid out + measured, but clipped) until its measured height holds steady across two frames, then reveal it already centered. A dialog re-arms on (re)open so each open reveals stable; once shown it won't re-hide on a mid-dialog content change (just repositions); a frame cap prevents an ever-changing height from hiding it forever. Fixed-height dialogs are unaffected. Verified migrate / about / seed-backup / import-key reveal centered at the settled frame. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1283,10 +1283,19 @@ inline int SegmentedControl(ImDrawList* dl, ImVec2 origin, float totalW, float h
|
||||
// Creates a fullscreen semi-transparent overlay with a centered card dialog.
|
||||
// Similar to the shutdown screen pattern but for interactive dialogs.
|
||||
|
||||
// Per-dialog content height (keyed by the child's id) measured at the end of each frame, so the next
|
||||
// frame can size the glass card to its content instead of a fixed viewport band. g_overlayCurrentKey
|
||||
// carries the active dialog's key from BeginOverlayDialog to EndOverlayDialog (overlays don't nest).
|
||||
inline std::unordered_map<std::string, float> g_overlayCardHeights;
|
||||
// Per-dialog content state (keyed by the child's id), measured at the end of each frame so the next
|
||||
// frame can position/size the (auto-height) card to its content. An auto-resize child reports a
|
||||
// too-small height on its first render and the true height a frame later, so we keep the card hidden
|
||||
// until the height holds steady (stableCount) before revealing it centered — otherwise it visibly
|
||||
// slides for a couple frames as the height converges. g_overlayCurrentKey carries the active dialog's
|
||||
// key from BeginOverlayDialog to EndOverlayDialog (overlays don't nest).
|
||||
struct OverlayCardState {
|
||||
float height = 0.0f; // last measured content-child height
|
||||
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
|
||||
};
|
||||
inline std::unordered_map<std::string, OverlayCardState> g_overlayCardHeights;
|
||||
inline std::string g_overlayCurrentKey;
|
||||
// Set when BeginOverlayDialog clips the card on an auto-height dialog's measuring frame (so it never
|
||||
// flashes off-center); EndOverlayDialog pops the clip. Overlays don't nest, so a single flag is fine.
|
||||
@@ -1365,6 +1374,10 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
return false;
|
||||
}
|
||||
|
||||
// True only on the frame the dialog (re)opens — used to re-arm the auto-height settle so each
|
||||
// fresh open reveals already-centered (rather than flashing last-open's stale position).
|
||||
const bool scrimAppearing = ImGui::IsWindowAppearing();
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// Live-blur backdrop with capture-once: (re)capture the content drawn BELOW this overlay for a
|
||||
@@ -1414,18 +1427,26 @@ inline bool BeginOverlayDialog(const OverlayDialogSpec& spec)
|
||||
g_overlayCurrentKey.clear();
|
||||
} else {
|
||||
g_overlayCurrentKey = childId;
|
||||
auto it = g_overlayCardHeights.find(childId);
|
||||
const float measuredH = (it != g_overlayCardHeights.end()) ? it->second : 0.0f;
|
||||
if (measuredH > 0.0f) {
|
||||
OverlayCardState& cs = g_overlayCardHeights[childId];
|
||||
if (scrimAppearing) { cs.shown = false; cs.stableCount = 0; cs.appearFrames = 0; }
|
||||
if (!cs.shown) cs.appearFrames++;
|
||||
const float measuredH = cs.height;
|
||||
// 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.
|
||||
const bool ready = measuredH > 0.0f &&
|
||||
(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;
|
||||
} else {
|
||||
// First frame: content height not yet known. Lay it out (so the auto-height child gets
|
||||
// measured) but keep the card hidden (hideForMeasure below) so it never flashes off-center
|
||||
// — it appears, already centered, on the next frame once the height is known.
|
||||
// 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,
|
||||
// already centered and stable, once the height stops changing.
|
||||
hideForMeasure = true;
|
||||
cardY = vp_pos.y + vp_size.y * 0.15f;
|
||||
cardBottomY = vp_pos.y + vp_size.y * spec.cardBottomViewportRatio;
|
||||
@@ -1509,10 +1530,16 @@ inline void EndOverlayDialog()
|
||||
{
|
||||
if (g_overlayHideFrameClip) { ImGui::PopClipRect(); g_overlayHideFrameClip = false; }
|
||||
ImGui::EndChild();
|
||||
// Remember the rendered card height (the child is the last item) so the next frame's
|
||||
// BeginOverlayDialog can size the glass to the content — kills the empty band under short dialogs.
|
||||
// Remember the rendered card height (the child is the last item) + track whether it has stopped
|
||||
// changing, so next frame's BeginOverlayDialog can reveal the card only once its height (and thus
|
||||
// its centered position) is stable.
|
||||
if (!g_overlayCurrentKey.empty()) {
|
||||
g_overlayCardHeights[g_overlayCurrentKey] = ImGui::GetItemRectSize().y;
|
||||
OverlayCardState& cs = g_overlayCardHeights[g_overlayCurrentKey];
|
||||
const float h = ImGui::GetItemRectSize().y;
|
||||
const float d = (h >= cs.height) ? (h - cs.height) : (cs.height - h);
|
||||
if (h > 0.0f && d <= 1.0f) cs.stableCount++;
|
||||
else cs.stableCount = 0;
|
||||
cs.height = h;
|
||||
}
|
||||
ImGui::PopStyleColor(); // ChildBg
|
||||
ImGui::PopStyleVar(2); // ChildRounding, WindowPadding (for child)
|
||||
|
||||
Reference in New Issue
Block a user