diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index dd8ea65..eb27652 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -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 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 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)