From 8b9cd5bf80dbedadb0c1ec205132cf40237a5dd8 Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 13 Jul 2026 20:49:04 -0500 Subject: [PATCH] feat(material): extract DialogActionFooter + GlassSectionScope helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0b of the settings-modal redesign — pull the reference dialog's two repeated patterns into shared helpers so the upcoming migrations are mechanical and identical: - material::DialogActionFooter(primary, enabled, close, &outPrimary, &outClose) — the centered fixed-width primary+Close TactileButton pair with no divider. - material::GlassSectionScope — an RAII auto-sized glass sub-section using a LOCAL ImDrawListSplitter (combo-safe, unlike GlassCardScope's shared ChannelsSplit) with the reference's 12/10 padding and 8dp rounding; interiorWidth() for full-width kids. Dogfooded by refactoring renderImportKeyDialog's footer and scan-height panel onto them — same padding/spec/centering, so the render is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.cpp | 44 +++++------------------ src/ui/material/draw_helpers.h | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 626ee20..d7b6c9e 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -3049,20 +3049,9 @@ void App::renderImportKeyDialog() sodium_memzero(import_key_scan_height_, sizeof(import_key_scan_height_)); // no stale height const int tip = state_.sync.blocks; // chain tip = slider max; 0 when unknown - // Render the content on an upper draw channel, then paint the glass panel behind it once the - // block's height is known (immediate-mode auto-sized panel via a draw-list splitter). - ImDrawList* dl = ImGui::GetWindowDrawList(); - ImDrawListSplitter splitter; - splitter.Split(dl, 2); - splitter.SetCurrentChannel(dl, 1); - - const float padX = 12.0f * dp, padY = 10.0f * dp; - const float panelW = ImGui::GetContentRegionAvail().x; - const ImVec2 panelMin = ImGui::GetCursorScreenPos(); - - ImGui::Dummy(ImVec2(0, padY)); - ImGui::Indent(padX); - const float availW = ImGui::GetContentRegionAvail().x - padX; // interior width (both side paddings) + // Auto-sized glass sub-section (combo-safe local splitter; see GlassSectionScope). + m::GlassSectionScope section; + const float availW = section.interiorWidth(); // Label + current chain height, right-aligned within the interior. const float rowStartX = ImGui::GetCursorPosX(); @@ -3083,18 +3072,6 @@ void App::renderImportKeyDialog() // start" hint was dropped as self-evident (0 is the field's default). if (transparentKey) ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("import_scan_transparent")); - - ImGui::Unindent(padX); - ImGui::Dummy(ImVec2(0, padY)); - - const ImVec2 panelMax(panelMin.x + panelW, ImGui::GetCursorScreenPos().y); - splitter.SetCurrentChannel(dl, 0); - m::GlassPanelSpec spec; - spec.rounding = 8.0f * dp; - spec.fillAlpha = 20; - spec.borderAlpha = 30; - m::DrawGlassPanel(dl, panelMin, panelMax, spec); - splitter.Merge(dl); } ImGui::Dummy(ImVec2(0, 2.0f * dp)); @@ -3147,17 +3124,15 @@ void App::renderImportKeyDialog() ImGui::Spacing(); - float btnW = 130.0f * dp; const bool running = sweepMode ? sweepRunning : import_in_progress_; // Sweep needs a spending key AND a destination (a picked address when not using a fresh one). const bool destReady = !sweepMode || sweep_dest_mode_ == 0 || sweep_dest_pick_[0] != '\0'; const bool canAct = recognized && state_.connected && !running && destReady; - // Center the action + Close buttons in the (wider) dialog. - const float footTotal = btnW * 2.0f + ImGui::GetStyle().ItemSpacing.x; - const float footOff = (ImGui::GetContentRegionAvail().x - footTotal) * 0.5f; - if (footOff > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + footOff); - ImGui::BeginDisabled(!canAct); - if (m::TactileButton(sweepMode ? TR("sweep_button") : TR("import_key_import"), ImVec2(btnW, 0))) { + bool doAction = false, doClose = false; + m::DialogActionFooter(sweepMode ? TR("sweep_button") : TR("import_key_import"), canAct, + TR("close"), doAction, doClose); + if (doClose) show_import_key_ = false; + if (doAction) { int startHeight = 0; // 0 = full rescan. Shielded imports (z_importkey) honor it; transparent if (import_key_scan_height_[0]) { // WIF (importprivkey) ignores it. startHeight = std::atoi(import_key_scan_height_); @@ -3187,9 +3162,6 @@ void App::renderImportKeyDialog() }); } } - ImGui::EndDisabled(); - ImGui::SameLine(); - if (m::TactileButton(TR("close"), ImVec2(btnW, 0))) show_import_key_ = false; // Wipe the key the moment the dialog closes (Close button OR outside-click, which clears // show_import_key_ mid-frame). in_progress is left to the callback to clear. diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index c1f193a..b65f0c4 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -1706,6 +1706,70 @@ inline void DialogConfirmFooter(const char* cancelId, const char* confirmLabel, } } +// Reference dialog footer: a centered primary + Close (or Cancel) TactileButton pair, fixed width, +// NO divider above (the reference dropped the separator). Sets outPrimary/outClose when the respective +// button is clicked — the caller runs the bodies (so the primary's action can be arbitrary). The +// primary is BeginDisabled'd when !primaryEnabled. btnW<=0 → 130·dp default. +inline void DialogActionFooter(const char* primaryLabel, bool primaryEnabled, + const char* closeLabel, bool& outPrimary, bool& outClose, + float btnW = 0.0f) +{ + const float dp = Layout::dpiScale(); + if (btnW <= 0.0f) btnW = 130.0f * dp; + const float total = btnW * 2.0f + ImGui::GetStyle().ItemSpacing.x; + const float off = (ImGui::GetContentRegionAvail().x - total) * 0.5f; + if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off); + ImGui::BeginDisabled(!primaryEnabled); + if (TactileButton(primaryLabel, ImVec2(btnW, 0))) outPrimary = true; + ImGui::EndDisabled(); + ImGui::SameLine(); + if (TactileButton(closeLabel, ImVec2(btnW, 0))) outClose = true; +} + +// RAII glass sub-section for grouping an optional/advanced block inside a dialog (the reference's +// scan-height panel). Auto-sizes to its content and paints a subtle glass panel behind it. Uses a +// LOCAL ImDrawListSplitter — safe with BeginCombo popups, unlike the shared ChannelsSplit that +// GlassCardScope uses. padX/padY are the interior insets (in logical px, dp-scaled here). Render the +// section body between construction and destruction; use interiorWidth() for full-width children. +struct GlassSectionScope { + ImDrawListSplitter splitter; + ImDrawList* dl; + ImVec2 cardMin; + float panelW, padX, padY; + GlassPanelSpec spec; + + explicit GlassSectionScope(float padXLogical = 12.0f, float padYLogical = 10.0f) + { + const float dp = Layout::dpiScale(); + padX = padXLogical * dp; + padY = padYLogical * dp; + spec.rounding = 8.0f * dp; + spec.fillAlpha = 20; + spec.borderAlpha = 30; + dl = ImGui::GetWindowDrawList(); + panelW = ImGui::GetContentRegionAvail().x; + cardMin = ImGui::GetCursorScreenPos(); + splitter.Split(dl, 2); + splitter.SetCurrentChannel(dl, 1); // content above the panel background + ImGui::Dummy(ImVec2(0, padY)); + ImGui::Indent(padX); + } + ~GlassSectionScope() + { + ImGui::Unindent(padX); + ImGui::Dummy(ImVec2(0, padY)); + const ImVec2 cardMax(cardMin.x + panelW, ImGui::GetCursorScreenPos().y); + splitter.SetCurrentChannel(dl, 0); + DrawGlassPanel(dl, cardMin, cardMax, spec); + splitter.Merge(dl); + } + // Interior content width (panel width minus both side insets) for full-width children. + float interiorWidth() const { return panelW - 2.0f * padX; } + + GlassSectionScope(const GlassSectionScope&) = delete; + GlassSectionScope& operator=(const GlassSectionScope&) = delete; +}; + } // namespace material } // namespace ui } // namespace dragonx