From 3b828e8a491c7e8cb4d32d9c285e81b8973ee5d1 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 26 Oct 2026 15:09:27 -0500 Subject: [PATCH] fix(ui): stop the Consolidate modal from jumping around during a run The overlay dialog is auto-height and vertically centred, so it re-centers whenever its content height changes. During a consolidation run two lines toggle and made it jump: the pause notice (waiting for a batch gap / sync / a send), which flickers on and off every batch, and the "run again" leftovers hint, which appears partway through. Reserve a fixed slot for the pause notice and, while the run is live, reserve the leftovers hint's measured height up front, so the content height stays constant and the modal holds still. The end-of-run transition to the result view is unchanged. Build + tests + hygiene green. Co-Authored-By: Claude Opus 4.8 --- src/ui/windows/shield_dialog.cpp | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/ui/windows/shield_dialog.cpp b/src/ui/windows/shield_dialog.cpp index 1f88b6c..79172d3 100644 --- a/src/ui/windows/shield_dialog.cpp +++ b/src/ui/windows/shield_dialog.cpp @@ -720,7 +720,14 @@ void ShieldDialog::render(App* app) std::max(1, p.batchesEstimated), p.notesMerged); wrappedCaption(material::OnSurfaceMedium(), cap); if (!p.stopping) { - if (const char* key = services::runWaitKey(run.lastWait())) wrappedCaption(material::OnSurfaceMedium(), TR(key)); + // The pause notice (waiting for a batch gap / sync / a send) toggles on and off every batch. + // Reserve a fixed two-line slot for it so its coming and going doesn't change the content + // height and make the auto-height, vertically-centred modal jump around during the run. + const float waitTop = ImGui::GetCursorPosY(); + if (const char* key = services::runWaitKey(run.lastWait())) + wrappedCaption(material::OnSurfaceMedium(), TR(key)); + const float waitSlotH = ImGui::GetTextLineHeightWithSpacing() * 2.0f; + if (ImGui::GetCursorPosY() < waitTop + waitSlotH) ImGui::SetCursorPosY(waitTop + waitSlotH); } } ImGui::ProgressBar(static_cast(p.batchesDone) / static_cast(std::max(1, p.batchesEstimated)), @@ -765,9 +772,20 @@ void ShieldDialog::render(App* app) } } ImGui::EndChild(); - if (leftovers) { - ImGui::Spacing(); - wrappedCaption(material::OnSurfaceMedium(), TR("merge_keep_each_run_again")); + { + // The "run again" hint appears partway through a run (once an address has leftover notes). While + // the run is live, reserve its measured height up front so it appearing doesn't shift the modal. + const float loTop = ImGui::GetCursorPosY(); + if (leftovers) { + ImGui::Spacing(); + wrappedCaption(material::OnSurfaceMedium(), TR("merge_keep_each_run_again")); + } + if (live) { + const float loH = ImGui::GetStyle().ItemSpacing.y + + ImGui::CalcTextSize(TR("merge_keep_each_run_again"), nullptr, false, + ImGui::GetContentRegionAvail().x).y; + if (ImGui::GetCursorPosY() < loTop + loH) ImGui::SetCursorPosY(loTop + loH); + } } if (!live) { // below the rows: merge_keep_each_some_failed says "see the list above" std::string endLine;