From fedfe3d60ca2b73655cd250ef6d2d4e1e6830ad4 Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 21 Jul 2026 15:53:28 -0500 Subject: [PATCH] feat(settings): rebuild Node data-dir + Daemon binary on the new components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 3. Node "Data Dir" is now a clean key/value block: the directory on its own row as a click-to-open accent link with a copy button, the wallet size below, and the folder buttons on their own row (icon ActionButtons) — no more path font-scaling or right-align overflow math. Daemon binary shows Installed / Bundled as key/value rows plus a Success/Warning status chip (replacing the fragile atom-by-atom wrapped status line); "Check for updates…" is now the accent Primary action, and the maintenance actions are a separate row with Delete Blockchain styled Destructive (Test/Rescan/Repair Secondary). All handlers, disabled-state predicates and tooltips are preserved; the buttons wrap instead of overflowing. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/pages/settings_page.cpp | 311 +++++++++++++-------------------- 1 file changed, 117 insertions(+), 194 deletions(-) diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index 1a50796..6e681fd 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1993,103 +1993,64 @@ void RenderSettingsPage(App* app) { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); { - std::string dirPath = util::Platform::getDragonXDataDir(); - std::string walletPath = dirPath + "wallet.dat"; - uint64_t wallet_size = util::Platform::getFileSize(walletPath); - std::string size_str = (wallet_size > 0) - ? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found"); + const std::string dirPath = util::Platform::getDragonXDataDir(); + const std::string walletPath = dirPath + "wallet.dat"; + const uint64_t wallet_size = util::Platform::getFileSize(walletPath); + const std::string size_str = (wallet_size > 0) + ? util::Platform::formatFileSize(wallet_size) : std::string(TR("settings_not_found")); + const float leftX = ImGui::GetCursorPosX(); + const float labelW = std::max(ImGui::CalcTextSize(TR("settings_data_dir")).x, + ImGui::CalcTextSize(TR("settings_wallet_size_label")).x) + + Layout::spacingLg(); + const ImU32 metaCol = OnSurfaceMedium(); - // Right-aligned "Open data folder" button trailing the row; the path - // gets whatever width is left after the data-dir label, wallet-size - // label+value, and the trailing button. - ImFont* nodeBtnFont = S.resolveFont("button"); - if (!nodeBtnFont) nodeBtnFont = Type().button(); - // Measure with the BUTTON font (not the current font) so the width matches - // the real TactileButton size — the old estimate mis-measured and the button - // overflowed the card's right edge, worse at high DPI. - float openBtnW = nodeBtnFont->CalcTextSizeA(nodeBtnFont->LegacySize, FLT_MAX, 0, - TR("settings_open_data_dir")).x + ImGui::GetStyle().FramePadding.x * 2.0f; - // "Open app folder" (the ObsidianDragon config folder), left of the data button. - float appBtnW = nodeBtnFont->CalcTextSizeA(nodeBtnFont->LegacySize, FLT_MAX, 0, - TR("settings_open_app_dir")).x + ImGui::GetStyle().FramePadding.x * 2.0f; - float openGroupW = appBtnW + spMd + openBtnW; - float dataDirLabelW = ImGui::CalcTextSize(TR("settings_data_dir")).x; - float sizeLabelW = ImGui::CalcTextSize(TR("settings_wallet_size_label")).x; - float sizeValueW = ImGui::CalcTextSize(size_str.c_str()).x; - // Space reserved to the right of the path for the size block + both buttons. - float trailW = sizeLabelW + Layout::spacingXs() + sizeValueW + spMd + openGroupW; - float availForPath = contentW - dataDirLabelW - Layout::spacingXs() - spMd - trailW; - if (availForPath < 60.0f) availForPath = contentW - dataDirLabelW - Layout::spacingXs(); - - ImVec4 linkCol = ImGui::ColorConvertU32ToFloat4(Primary()); - ImVec4 linkHoverCol = linkCol; - linkHoverCol.w = std::min(1.0f, linkCol.w + 0.2f); - - float rowTopY = ImGui::GetCursorScreenPos().y; // top of the data-dir row + // Row 1: Data directory — a clickable link (opens the folder) + a copy button. ImGui::AlignTextToFramePadding(); + ImGui::PushStyleColor(ImGuiCol_Text, metaCol); ImGui::TextUnformatted(TR("settings_data_dir")); - ImGui::SameLine(0, Layout::spacingXs()); - ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str()); - bool hovered = false; + ImGui::PopStyleColor(); + ImGui::SameLine(0, 0); + ImGui::SetCursorPosX(leftX + labelW); ImGui::AlignTextToFramePadding(); - if (pathSize.x > availForPath && availForPath > 0) { - float scale = availForPath / pathSize.x; - float condensedSize = body2Info->LegacySize * std::max(scale, 0.65f); - ImGui::SetWindowFontScale(condensedSize / body2Info->LegacySize); - ImGui::TextColored(linkCol, "%s", dirPath.c_str()); - hovered = ImGui::IsItemHovered(); - if (hovered) { - ImVec2 tMin = ImGui::GetItemRectMin(); - ImVec2 tMax = ImGui::GetItemRectMax(); - dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol)); - } - ImGui::SetWindowFontScale(1.0f); - } else { - ImGui::TextColored(linkCol, "%s", dirPath.c_str()); - hovered = ImGui::IsItemHovered(); - if (hovered) { - ImVec2 tMin = ImGui::GetItemRectMin(); - ImVec2 tMax = ImGui::GetItemRectMax(); - dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol)); - } - } - if (hovered) { - material::Tooltip("%s", TR("tt_open_dir")); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", dirPath.c_str()); + if (ImGui::IsItemHovered()) { + const ImVec2 tmn = ImGui::GetItemRectMin(), tmx = ImGui::GetItemRectMax(); + dl->AddLine(ImVec2(tmn.x, tmx.y), ImVec2(tmx.x, tmx.y), Primary()); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + material::Tooltip("%s", TR("tt_open_dir")); + } + if (ImGui::IsItemClicked()) util::Platform::openFolder(dirPath); + ImGui::SameLine(0, Layout::spacingSm()); + { + material::IconButtonStyle cs; + cs.hoverBg = material::WithAlpha(OnSurface(), 30); + cs.tooltip = TR("copy"); + const float ih = ImGui::GetFrameHeight(); + if (material::IconButton("##copydir", ICON_MD_CONTENT_COPY, Type().iconSmall(), ImVec2(ih, ih), cs)) + ImGui::SetClipboardText(dirPath.c_str()); } - if (ImGui::IsItemClicked()) - util::Platform::openFolder(dirPath); - // Wallet Size label + value, trailing the path on the same row. - ImGui::SameLine(0, spMd); + // Row 2: Wallet size. ImGui::AlignTextToFramePadding(); + ImGui::PushStyleColor(ImGuiCol_Text, metaCol); ImGui::TextUnformatted(TR("settings_wallet_size_label")); - ImGui::SameLine(0, Layout::spacingXs()); + ImGui::PopStyleColor(); + ImGui::SameLine(0, 0); + ImGui::SetCursorPosX(leftX + labelW); ImGui::AlignTextToFramePadding(); - if (wallet_size > 0) - ImGui::TextUnformatted(size_str.c_str()); - else - ImGui::TextDisabled("%s", TR("settings_not_found")); + if (wallet_size > 0) ImGui::TextUnformatted(size_str.c_str()); + else ImGui::TextDisabled("%s", TR("settings_not_found")); - // Folder buttons: [Open app folder] [Open data folder], right-aligned as a - // group on the data-dir row (rowTopY), each an EXPLICIT width so the group's - // right edge lands exactly on the card edge (no overflow at any DPI). - float rowY = rowTopY; - float rightEdge = sectionOrigin.x + contentW; - float groupX = rightEdge - openGroupW; - float afterSizeX = ImGui::GetItemRectMax().x + spMd; - ImGui::SameLine(0, spMd); - if (groupX > afterSizeX) - ImGui::SetCursorScreenPos(ImVec2(groupX, rowY)); - // App (ObsidianDragon config) folder — opens ~/.config/ObsidianDragon etc. - if (TactileButton(TR("settings_open_app_dir"), ImVec2(appBtnW, 0), nodeBtnFont)) { + // Row 3: folder buttons (their own row so the path gets the full width). + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + material::ButtonFlow ff(contentW); + ff.next(material::ActionButtonWidth(TR("settings_open_app_dir"), ICON_MD_FOLDER)); + if (material::ActionButton("##openapp", TR("settings_open_app_dir"), ICON_MD_FOLDER, material::ActionTier::Secondary)) util::Platform::openFolder(util::Platform::getObsidianDragonDir()); - } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_app_dir")); - ImGui::SameLine(0, spMd); - if (TactileButton(TR("settings_open_data_dir"), ImVec2(openBtnW, 0), nodeBtnFont)) { + ff.next(material::ActionButtonWidth(TR("settings_open_data_dir"), ICON_MD_FOLDER_OPEN)); + if (material::ActionButton("##opendata", TR("settings_open_data_dir"), ICON_MD_FOLDER_OPEN, material::ActionTier::Secondary)) util::Platform::openFolder(dirPath); - } if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir")); } @@ -2218,100 +2179,90 @@ void RenderSettingsPage(App* app) { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - // Status line: Installed ( · ) · Bundled () · . - // Built as color-tagged atoms and laid out with manual wrapping — as one long inline - // row of SameLine() Text calls it overran the content width and clipped its colored - // tail at narrow windows / higher font scale (the status was effectively hidden). - struct DbAtom { std::string text; ImU32 col; bool sep; }; - std::vector dbAtoms; - const ImU32 dbNorm = ImGui::GetColorU32(ImGuiCol_Text); - const ImU32 dbDimU = OnSurfaceMedium(); - - dbAtoms.push_back({ TR("daemon_installed"), dbNorm, false }); + // Version info (Installed / Bundled) as key/value rows, then a status chip. + const float dLeftX = ImGui::GetCursorPosX(); + const float dLabelW = std::max(ImGui::CalcTextSize(TR("daemon_installed")).x, + ImGui::CalcTextSize(TR("daemon_bundled")).x) + Layout::spacingLg(); + auto dkv = [&](const char* label, const std::string& value, bool dim) { + ImGui::AlignTextToFramePadding(); + ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium()); + ImGui::TextUnformatted(label); + ImGui::PopStyleColor(); + ImGui::SameLine(0, 0); + ImGui::SetCursorPosX(dLeftX + dLabelW); + ImGui::AlignTextToFramePadding(); + if (dim) ImGui::TextDisabled("%s", value.c_str()); + else ImGui::TextUnformatted(value.c_str()); + }; if (inst.exists) { - dbAtoms.push_back({ inst.version.empty() ? std::string(TR("unknown")) : inst.version, dbNorm, false }); - char dbParen[96]; - std::snprintf(dbParen, sizeof(dbParen), "(%s · %s)", + char ibuf[176]; + std::snprintf(ibuf, sizeof(ibuf), "%s \xC2\xB7 %s \xC2\xB7 %s", + inst.version.empty() ? TR("unknown") : inst.version.c_str(), util::Platform::formatFileSize(inst.size).c_str(), fmtDate(inst.modifiedEpoch).c_str()); - dbAtoms.push_back({ dbParen, dbDimU, false }); + dkv(TR("daemon_installed"), ibuf, false); } else { - dbAtoms.push_back({ TR("daemon_not_installed"), dbDimU, false }); + dkv(TR("daemon_installed"), TR("daemon_not_installed"), true); } - dbAtoms.push_back({ "·", dbDimU, true }); - dbAtoms.push_back({ TR("daemon_bundled"), dbNorm, false }); if (bun.available) { - dbAtoms.push_back({ bun.version.empty() ? std::string(TR("unknown")) : bun.version, dbNorm, false }); - dbAtoms.push_back({ "(" + util::Platform::formatFileSize(bun.size) + ")", dbDimU, false }); + char bbuf[144]; + std::snprintf(bbuf, sizeof(bbuf), "%s \xC2\xB7 %s", + bun.version.empty() ? TR("unknown") : bun.version.c_str(), + util::Platform::formatFileSize(bun.size).c_str()); + dkv(TR("daemon_bundled"), bbuf, false); } else { - dbAtoms.push_back({ TR("daemon_none_bundled"), dbDimU, false }); + dkv(TR("daemon_bundled"), TR("daemon_none_bundled"), true); } if (bun.available) { const bool sameSize = inst.exists && inst.size == bun.size; - dbAtoms.push_back({ "·", dbDimU, true }); - if (!inst.exists) - dbAtoms.push_back({ TR("daemon_status_missing"), ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 0.8f, 0.2f, 1.0f)), false }); - else if (sameSize) - dbAtoms.push_back({ TR("daemon_status_match"), ImGui::ColorConvertFloat4ToU32(ImVec4(0.3f, 0.8f, 0.3f, 1.0f)), false }); - else - dbAtoms.push_back({ TR("daemon_status_differ"), ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 0.8f, 0.2f, 1.0f)), false }); + const char* chipTxt = !inst.exists ? TR("daemon_status_missing") + : sameSize ? TR("daemon_status_match") + : TR("daemon_status_differ"); + const ImU32 chipCol = sameSize ? Success() : Warning(); + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + const float dp = Layout::dpiScale(); + const ImVec2 cp = ImGui::GetCursorScreenPos(); + const float chpad = 8.0f * dp, chh = ImGui::GetFrameHeight(); + const ImVec2 cts = ImGui::CalcTextSize(chipTxt); + const float chw = cts.x + chpad * 2.0f; + dl->AddRectFilled(cp, ImVec2(cp.x + chw, cp.y + chh), material::WithAlpha(chipCol, 38), chh * 0.4f); + dl->AddRect(cp, ImVec2(cp.x + chw, cp.y + chh), material::WithAlpha(chipCol, 120), chh * 0.4f, 0, 1.0f); + dl->AddText(ImVec2(cp.x + chpad, cp.y + (chh - cts.y) * 0.5f), chipCol, chipTxt); + ImGui::Dummy(ImVec2(chw, chh)); } - // Lay the atoms out left-to-right, wrapping to a new line when the next one would - // exceed the content width. A "·" separator is dropped when it (plus the atom after - // it) doesn't fit, so a wrapped line never starts or ends with a dangling separator. - const float dbAvail = ImGui::GetContentRegionAvail().x; - const float dbGap = Layout::spacingXs(); - float dbX = 0.0f; - ImGui::AlignTextToFramePadding(); - for (std::size_t i = 0; i < dbAtoms.size(); ++i) { - const float w = ImGui::CalcTextSize(dbAtoms[i].text.c_str()).x; - if (dbAtoms[i].sep) { - const float nextW = (i + 1 < dbAtoms.size()) - ? ImGui::CalcTextSize(dbAtoms[i + 1].text.c_str()).x : 0.0f; - if (dbX > 0.0f && dbX + dbGap + w + dbGap + nextW > dbAvail) continue; - } - const bool wrap = (dbX > 0.0f) && (dbX + dbGap + w > dbAvail); - if (dbX > 0.0f && !wrap) { ImGui::SameLine(0, dbGap); dbX += dbGap; } - else if (wrap) { dbX = 0.0f; } - ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(dbAtoms[i].col), "%s", dbAtoms[i].text.c_str()); - dbX += w; - } - - // In-app node update: download + verify the latest dragonxd from the project Gitea. - // Refresh the cached daemon info once an install has completed. + // Refresh the cached daemon info once an in-app install has completed. if (ui::DaemonUpdateDialog::consumeInstalled()) s_settingsState.daemon_info_loaded = false; - // Action row: Check for updates | Refresh | Install bundled on the left, - // with the four maintenance actions right-aligned on the same row. + // Update actions: Check for updates (primary) | Refresh | Install bundled. ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - float daemonBtnRowY = ImGui::GetCursorScreenPos().y; - if (TactileButton(TR("daemon_update_check"), ImVec2(0, 0), dbBtnFont)) { - ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string()); + { + using AT = material::ActionTier; + material::ButtonFlow uf(contentW); + uf.next(material::ActionButtonWidth(TR("daemon_update_check"), ICON_MD_SYSTEM_UPDATE)); + if (material::ActionButton("##dupd", TR("daemon_update_check"), ICON_MD_SYSTEM_UPDATE, AT::Primary)) + ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string()); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check")); + uf.next(material::ActionButtonWidth(TR("refresh"), ICON_MD_REFRESH)); + if (material::ActionButton("##drefresh", TR("refresh"), ICON_MD_REFRESH, AT::Secondary)) + s_settingsState.daemon_info_loaded = false; + ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available); + uf.next(material::ActionButtonWidth(TR("daemon_install_bundled"), ICON_MD_DOWNLOAD)); + if (material::ActionButton("##dinst", TR("daemon_install_bundled"), ICON_MD_DOWNLOAD, AT::Secondary)) + s_settingsState.confirm_reinstall_daemon = true; + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled")); + ImGui::EndDisabled(); } - if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check")); - ImGui::SameLine(0, spMd); - // Unique ImGui ID (label still displays "Refresh") — the theme sections - // also have "Refresh" buttons, so a bare label collides on this page. - if (TactileButton((std::string(TR("refresh")) + "##daemonRefresh").c_str(), ImVec2(0, 0), dbBtnFont)) { - s_settingsState.daemon_info_loaded = false; - } - ImGui::SameLine(0, spMd); - 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(); - // The four maintenance actions (Test connection / Rescan / Delete blockchain - // / Repair wallet), right-aligned on the SAME row as Install bundled — no - // separate "Advanced" toggle. Every BeginDisabled/EndDisabled predicate is - // preserved exactly. Wraps to a right-aligned next row when too narrow. - auto renderDaemonMaintenanceButtons = [&]() { + // Maintenance actions: Test / Rescan / Repair | Delete blockchain (destructive). + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + { + using AT = material::ActionTier; + material::ButtonFlow mf(contentW); ImGui::BeginDisabled(!app->isConnected()); - if (TactileButton(TR("test_connection"), ImVec2(0, 0), dbBtnFont)) { + mf.next(material::ActionButtonWidth(TR("test_connection"), ICON_MD_LINK)); + if (material::ActionButton("##dtest", TR("test_connection"), ICON_MD_LINK, AT::Secondary)) { if (app->rpc() && app->rpc()->isConnected() && app->worker()) { app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb { try { @@ -2328,52 +2279,24 @@ void RenderSettingsPage(App* app) { } } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn")); - ImGui::EndDisabled(); - ImGui::SameLine(0, spMd); - ImGui::BeginDisabled(!app->isConnected()); - if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) { + mf.next(material::ActionButtonWidth(TR("rescan"), ICON_MD_REFRESH)); + if (material::ActionButton("##drescan", TR("rescan"), ICON_MD_REFRESH, AT::Secondary)) { 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, spMd); 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, spMd); - if (TactileButton(TR("repair_wallet"), ImVec2(0, 0), dbBtnFont)) { + mf.next(material::ActionButtonWidth(TR("repair_wallet"), ICON_MD_HEALING)); + if (material::ActionButton("##drepair", TR("repair_wallet"), ICON_MD_HEALING, AT::Secondary)) s_settingsState.confirm_repair_wallet = true; - } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet")); + mf.next(material::ActionButtonWidth(TR("delete_blockchain"), ICON_MD_DELETE)); + if (material::ActionButton("##ddelete", TR("delete_blockchain"), ICON_MD_DELETE, AT::Destructive)) + s_settingsState.confirm_delete_blockchain = true; + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain")); ImGui::EndDisabled(); - }; - { - // Group width (button font) to right-align the four maintenance actions. - const char* mLabels[] = { TR("test_connection"), TR("rescan"), - TR("delete_blockchain"), TR("repair_wallet") }; - float groupW = spMd * 3.0f; - for (const char* l : mLabels) - groupW += dbBtnFont->CalcTextSizeA(dbBtnFont->LegacySize, FLT_MAX, 0, l).x - + ImGui::GetStyle().FramePadding.x * 2.0f; - float afterTestX = ImGui::GetItemRectMax().x + spMd; // right edge of Install bundled - float rightEdge = sectionOrigin.x + contentW; - float groupX = rightEdge - groupW; - if (groupX >= afterTestX) { - // Fits on the Install-bundled row, right-aligned. - ImGui::SameLine(0, 0); - ImGui::SetCursorScreenPos(ImVec2(groupX, daemonBtnRowY)); - } else { - // Too narrow: wrap to the next row, right-aligned (clamped to left edge). - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - float wrapX = std::max(sectionOrigin.x, rightEdge - groupW); - ImGui::SetCursorScreenPos(ImVec2(wrapX, ImGui::GetCursorScreenPos().y)); - } - renderDaemonMaintenanceButtons(); } }