From 5d6e78c71907a56233021eae1d2a918ca0bfe817 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 2 Jul 2026 00:08:32 -0500 Subject: [PATCH] =?UTF-8?q?feat(settings):=20masonry=20Node=20&=20Security?= =?UTF-8?q?=20=E2=80=94=20fill=20the=20empty=20bottom-right?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 60/40 split left the right (Security) column short while Node/RPC on the left was tall, and Daemon Binary sat full-width below — leaving a large empty gap in the card's bottom-right. Rework it as a greedy two-column masonry: even 50/50 columns, and the Daemon Binary section is placed into whichever column is currently shorter (clipped to that column's width). In practice Node/RPC fills the left and Security + Daemon Binary stack on the right, so the gap is filled and the two sides balance in height. When the window is narrow the columns stack and Daemon Binary spans full width below, as before. Daemon Binary now sizes to its column (dbColW) instead of the full card width. Full-node + lite build clean; source hygiene clean. Visual QA needed (column balance + the daemon button rows in the narrower column). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/pages/settings_page.cpp | 148 ++++++++++++++++++--------------- 1 file changed, 82 insertions(+), 66 deletions(-) diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 5f42efe..094ad6e 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1515,10 +1515,10 @@ void RenderSettingsPage(App* app) { // grid especially), so stack them into one full-width column instead. float stackWidth = S.drawElement("components.settings-page", "node-sec-stack-width").sizeOr(600.0f); bool stacked = contentW < stackWidth; - // 70/30 when side by side: NODE holds far more (data dir + RPC grid) than the - // short SECURITY column, so give it the width and shrink the right-side gap. - float leftColW = stacked ? contentW : (contentW - colGap) * 0.7f; - float rightColW = stacked ? contentW : (contentW - colGap) * 0.3f; + // Even 50/50 columns so the Daemon Binary section (masonry-placed into the + // shorter column below) has enough width, and Node/RPC vs Security balance. + float leftColW = stacked ? contentW : (contentW - colGap) * 0.5f; + float rightColW = stacked ? contentW : (contentW - colGap) * 0.5f; ImVec2 sectionOrigin = ImGui::GetCursorScreenPos(); float leftX = sectionOrigin.x; float rightX = stacked ? sectionOrigin.x : (sectionOrigin.x + leftColW + colGap); @@ -2148,68 +2148,24 @@ void RenderSettingsPage(App* app) { } } - // Advanced — rare/destructive daemon actions (full-node only), placed in the - // right column below Security so that space isn't left empty. Buttons stack - // vertically to fit the narrow column. - if (app->supportsFullNodeLifecycleActions()) { - ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y)); - ImVec2 advPos = ImGui::GetCursorScreenPos(); - const char* advArrow = s_settingsState.node_advanced_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); - if (ImGui::Button("##NodeAdvancedToggle", ImVec2(rightColW, ImGui::GetFrameHeight()))) - s_settingsState.node_advanced_expanded = !s_settingsState.node_advanced_expanded; - ImGui::PopStyleColor(3); - ImFont* ovA = Type().overline(); - float advTextY = advPos.y + (ImGui::GetFrameHeight() - ovA->LegacySize) * 0.5f; - dl->AddText(ovA, ovA->LegacySize, ImVec2(advPos.x, advTextY), OnSurfaceMedium(), TR("advanced")); - ImFont* icoA = Type().iconSmall(); if (!icoA) icoA = ovA; - float arrowW = icoA->CalcTextSizeA(icoA->LegacySize, FLT_MAX, 0, advArrow).x; - dl->AddText(icoA, icoA->LegacySize, ImVec2(advPos.x + rightColW - arrowW, advTextY), OnSurfaceMedium(), advArrow); - - if (s_settingsState.node_advanced_expanded) { - ImFont* advBtnFont = S.resolveFont("button"); - auto advRow = [&]() { ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y)); }; - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - advRow(); - ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !s_settingsState.bundled_daemon.available); - if (TactileButton(TR("daemon_install_bundled"), ImVec2(rightColW, 0), advBtnFont)) - s_settingsState.confirm_reinstall_daemon = true; - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled")); - ImGui::EndDisabled(); - advRow(); - ImGui::BeginDisabled(!app->isConnected()); - if (TactileButton(TR("rescan"), ImVec2(rightColW, 0), advBtnFont)) { - s_settingsState.confirm_rescan = true; - s_settingsState.rescan_height_detecting = false; - s_settingsState.rescan_height_detected = false; - } - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan")); - ImGui::EndDisabled(); - advRow(); - ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon()); - if (TactileButton(TR("delete_blockchain"), ImVec2(rightColW, 0), advBtnFont)) - s_settingsState.confirm_delete_blockchain = true; - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain")); - ImGui::EndDisabled(); - advRow(); - if (TactileButton(TR("repair_wallet"), ImVec2(rightColW, 0), advBtnFont)) - s_settingsState.confirm_repair_wallet = true; - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet")); - } - } - float rightBottom = ImGui::GetCursorScreenPos().y; ImGui::PopClipRect(); - // Advance cursor past both columns - float maxBottom = std::max(leftBottom, rightBottom); - ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom)); - - // ---- DAEMON BINARY — full-width row beneath the NODE + SECURITY columns ---- + // ---- DAEMON BINARY — masonry-placed into the shorter column ---- + // Node/RPC (left) is taller than Security (right), so Daemon Binary drops under + // Security, filling the empty bottom-right and balancing the two columns. When + // the columns are stacked (narrow), it spans full width below both. if (app->supportsFullNodeLifecycleActions()) { + const bool dbFull = stacked; + const bool dbLeftCol = leftBottom <= rightBottom; + const float dbColW = dbFull ? contentW : (dbLeftCol ? leftColW : rightColW); + const float dbTopY = dbFull ? std::max(leftBottom, rightBottom) + : (dbLeftCol ? leftBottom : rightBottom); + const float dbLeftX = dbFull ? sectionOrigin.x : (dbLeftCol ? leftX : rightX); + ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbTopY)); + ImGui::PushClipRect(ImVec2(dbLeftX, dbTopY), + ImVec2(dbLeftX + dbColW, dbTopY + 9999.0f), true); + ImFont* dbBtnFont = S.resolveFont("button"); if (!s_settingsState.daemon_info_loaded) { s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo(); @@ -2233,7 +2189,6 @@ void RenderSettingsPage(App* app) { return std::string(buf); }; - const float dbLeftX = sectionOrigin.x; const ImVec4 dbDim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium()); ImGui::Dummy(ImVec2(0, Layout::spacingLg())); @@ -2243,7 +2198,7 @@ void RenderSettingsPage(App* app) { // Installed | Bundled side by side across the full container width. const float dbStartY = ImGui::GetCursorScreenPos().y; const float dbLineH = ImGui::GetTextLineHeightWithSpacing(); - const float dbCol2X = dbLeftX + std::min(contentW * 0.4f, 340.0f); + const float dbCol2X = dbLeftX + std::min(dbColW * 0.4f, 340.0f); ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY)); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_installed")); @@ -2317,9 +2272,70 @@ void RenderSettingsPage(App* app) { } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn")); ImGui::EndDisabled(); - // (The "Advanced" destructive-actions expander now lives in the Security - // column above, filling what used to be empty space.) + + // Advanced — rare + destructive daemon actions behind a collapsible header + // (reuses the DEBUG LOGGING expander idiom). + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + { + ImVec2 advPos = ImGui::GetCursorScreenPos(); + const char* advArrow = s_settingsState.node_advanced_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); + if (ImGui::Button("##NodeAdvancedToggle", ImVec2(dbColW, ImGui::GetFrameHeight()))) { + s_settingsState.node_advanced_expanded = !s_settingsState.node_advanced_expanded; + } + ImGui::PopStyleColor(3); + ImFont* ovFont = Type().overline(); + float textY = advPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f; + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(advPos.x, textY), OnSurfaceMedium(), TR("advanced")); + ImFont* iconFont = Type().iconSmall(); + if (!iconFont) iconFont = ovFont; + float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, advArrow).x; + dl->AddText(iconFont, iconFont->LegacySize, ImVec2(advPos.x + dbColW - arrowW, textY), OnSurfaceMedium(), advArrow); + } + if (s_settingsState.node_advanced_expanded) { + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y)); + ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available); + if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), dbBtnFont)) { + s_settingsState.confirm_reinstall_daemon = true; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled")); + ImGui::EndDisabled(); + ImGui::SameLine(0, Layout::spacingMd()); + ImGui::BeginDisabled(!app->isConnected()); + if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) { + s_settingsState.confirm_rescan = true; + // Re-probe the available block range each time the dialog is opened. + s_settingsState.rescan_height_detecting = false; + s_settingsState.rescan_height_detected = false; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan")); + ImGui::EndDisabled(); + ImGui::SameLine(0, Layout::spacingMd()); + ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon()); + if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), dbBtnFont)) { + s_settingsState.confirm_delete_blockchain = true; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain")); + ImGui::SameLine(0, Layout::spacingMd()); + if (TactileButton(TR("repair_wallet"), ImVec2(0, 0), dbBtnFont)) { + s_settingsState.confirm_repair_wallet = true; + } + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet")); + ImGui::EndDisabled(); + } + + float dbBottom = ImGui::GetCursorScreenPos().y; + ImGui::PopClipRect(); + if (dbFull || dbLeftCol) leftBottom = std::max(leftBottom, dbBottom); + if (dbFull || !dbLeftCol) rightBottom = std::max(rightBottom, dbBottom); } + + // Advance the card cursor past the taller column. + float maxBottom = std::max(leftBottom, rightBottom); + ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom)); } ImGui::Dummy(ImVec2(0, bottomPad));