diff --git a/CMakeLists.txt b/CMakeLists.txt index b992e78..e3f78d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -445,6 +445,7 @@ set(APP_SOURCES src/ui/windows/mining_earnings.cpp src/ui/windows/mining_stats.cpp src/ui/windows/mining_controls.cpp + src/ui/windows/mining_mode_toggle.cpp src/ui/windows/mining_benchmark.cpp src/ui/windows/mining_pool_panel.cpp src/ui/windows/mining_tab_helpers.cpp diff --git a/src/ui/windows/mining_mode_toggle.cpp b/src/ui/windows/mining_mode_toggle.cpp new file mode 100644 index 0000000..efa3cf9 --- /dev/null +++ b/src/ui/windows/mining_mode_toggle.cpp @@ -0,0 +1,575 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 + +#include "mining_mode_toggle.h" +#include "mining_tab_helpers.h" +#include "mining_pool_panel.h" + +#include "../../app.h" +#include "../../config/version.h" +#include "../../config/settings.h" +#include "../../util/i18n.h" +#include "../../util/platform.h" +#include "../schema/ui_schema.h" +#include "../material/type.h" +#include "../material/draw_helpers.h" +#include "../material/colors.h" +#include "../material/components/buttons.h" +#include "../layout.h" +#include "../notifications.h" +#include "../../embedded/IconsMaterialDesign.h" +#include "imgui.h" +#include "imgui_internal.h" + +#include +#include +#include +#include +#include +#include + +namespace dragonx { +namespace ui { + +using namespace material; + +void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo& mining, + ImDrawList* dl, ImFont* capFont, ImFont* ovFont, + float dp, float hs, float gap, float availWidth, + bool& s_pool_mode, char (&s_pool_url)[256], char (&s_pool_worker)[256], + bool& s_pool_settings_dirty) +{ + const bool soloMiningAvailable = app->supportsSoloMining(); + float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs; + float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size; + float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size; + float totalW = soloMiningAvailable ? (toggleW * 2) : toggleW; + + ImVec2 tMin = ImGui::GetCursorScreenPos(); + ImVec2 tMax(tMin.x + totalW, tMin.y + toggleH); + + // Glass background for the segmented control + dl->AddRectFilled(tMin, tMax, WithAlpha(OnSurface(), 15), toggleRnd); + dl->AddRect(tMin, tMax, WithAlpha(OnSurface(), 40), toggleRnd); + + ImVec2 soloMin = tMin; + ImVec2 soloMax(tMin.x + toggleW, tMax.y); + bool soloHov = material::IsRectHovered(soloMin, soloMax); + if (soloMiningAvailable) { + // SOLO button (left half) + if (!s_pool_mode) { + dl->AddRectFilled(soloMin, soloMax, WithAlpha(Primary(), 180), toggleRnd); + } else if (soloHov) { + dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd); + } + const char* label = TR("mining_solo"); + ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label); + float lx = soloMin.x + (toggleW - sz.x) * 0.5f; + float ly = soloMin.y + (toggleH - sz.y) * 0.5f; + ImU32 col = !s_pool_mode ? IM_COL32(255, 255, 255, 230) : OnSurfaceMedium(); + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), col, label); + } + + // POOL button (right half) — disabled when solo mining is active + bool soloMiningActive = mining.generate; + ImVec2 poolMin(tMin.x + (soloMiningAvailable ? toggleW : 0.0f), tMin.y); + ImVec2 poolMax = tMax; + bool poolHov = material::IsRectHovered(poolMin, poolMax); + if (s_pool_mode) { + dl->AddRectFilled(poolMin, poolMax, WithAlpha(Primary(), 180), toggleRnd); + } else if (soloMiningActive) { + // Dimmed — solo mining blocks pool mode + } else if (poolHov) { + dl->AddRectFilled(poolMin, poolMax, WithAlpha(OnSurface(), 20), toggleRnd); + } + { + const char* label = TR("mining_pool"); + ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label); + float lx = poolMin.x + (toggleW - sz.x) * 0.5f; + float ly = poolMin.y + (toggleH - sz.y) * 0.5f; + ImU32 col = s_pool_mode ? IM_COL32(255, 255, 255, 230) + : (soloMiningActive ? OnSurfaceDisabled() : OnSurfaceMedium()); + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), col, label); + } + + // Invisible buttons for click targets + if (soloMiningAvailable) { + ImGui::SetCursorScreenPos(soloMin); + ImGui::InvisibleButton("##SoloMode", ImVec2(toggleW, toggleH)); + if (ImGui::IsItemClicked() && s_pool_mode) { + s_pool_mode = false; + app->settings()->setPoolMode(false); + app->settings()->save(); + app->stopPoolMining(); + } + if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + } + + ImGui::SetCursorScreenPos(poolMin); + ImGui::InvisibleButton("##PoolMode", ImVec2(toggleW, toggleH)); + if (soloMiningAvailable && ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) { + s_pool_mode = true; + app->settings()->setPoolMode(true); + app->settings()->save(); + // Note: soloMiningActive is already false (checked above), + // so no need to call stopMining() — it would just set the + // toggle-in-progress flag and make the button show "STARTING". + } + if (soloMiningAvailable && poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (poolHov && soloMiningActive && !s_pool_mode) { + ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool")); + } + + ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y)); + ImGui::Dummy(ImVec2(totalW, 0)); + + // Pool URL + Worker inputs inline next to toggle (pool mode only) + if (s_pool_mode && soloMiningActive) { + // Solo mining is active — show disabled message instead of inputs + float inputFrameH = ImGui::GetFrameHeight(); + float vertOff = (toggleH - inputFrameH) * 0.5f; + ImGui::SetCursorScreenPos(ImVec2(tMax.x + Layout::spacingLg(), tMin.y + vertOff)); + + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); + ImGui::AlignTextToFramePadding(); + ImGui::PushFont(Type().iconSmall()); + ImGui::TextUnformatted(ICON_MD_INFO); + ImGui::PopFont(); + ImGui::SameLine(0, Layout::spacingSm()); + ImGui::PushFont(capFont); + ImGui::TextUnformatted(TR("mining_stop_solo_for_pool_settings")); + ImGui::PopFont(); + ImGui::PopStyleColor(); + } else if (s_pool_mode) { + // Position inputs to the right of the toggle + float inputFrameH = ImGui::GetFrameHeight(); + float vertOff = (toggleH - inputFrameH) * 0.5f; + float inputsStartX = tMax.x + Layout::spacingLg(); + ImGui::SetCursorScreenPos(ImVec2(inputsStartX, tMin.y + vertOff)); + + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8 * dp, 4 * dp)); + + float inputFrameH2 = ImGui::GetFrameHeight(); + float iconBtnW = inputFrameH2; + float resetBtnW = iconBtnW; + float contentEndX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x; + // Each input group: [input][▼][bookmark] + // Layout: [URL group] [spacing] [Worker group] [spacing] [reset] + float perGroupExtra = iconBtnW * 2; // dropdown + bookmark + float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW + - Layout::spacingSm() - perGroupExtra * 2; + float urlW = std::max(60.0f, remainW * 0.30f); + float wrkW = std::max(40.0f, remainW * 0.70f); + + // Track positions for popup alignment + float urlGroupStartX = ImGui::GetCursorScreenPos().x; + float urlGroupStartY = ImGui::GetCursorScreenPos().y; + float urlGroupW = urlW + perGroupExtra; + + // === Pool URL input === + ImGui::SetNextItemWidth(urlW); + if (ImGui::InputTextWithHint("##PoolURL", TR("mining_pool_url"), s_pool_url, sizeof(s_pool_url))) { + s_pool_settings_dirty = true; + } + + // --- URL: Dropdown arrow button --- + ImGui::SameLine(0, 0); + { + ImVec2 btnPos = ImGui::GetCursorScreenPos(); + ImVec2 btnSize(iconBtnW, inputFrameH2); + ImGui::InvisibleButton("##PoolDropdown", btnSize); + bool btnHov = ImGui::IsItemHovered(); + bool btnClk = ImGui::IsItemClicked(); + ImDrawList* dl2 = ImGui::GetWindowDrawList(); + ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); + if (btnHov) { + dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), + StateHover(), 4.0f * dp); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", TR("mining_saved_pools")); + } + ImFont* icoFont = Type().iconSmall(); + const char* dropIcon = ICON_MD_ARROW_DROP_DOWN; + ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon); + dl2->AddText(icoFont, icoFont->LegacySize, + ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), + OnSurfaceMedium(), dropIcon); + if (btnClk) { + ImGui::OpenPopup("##SavedPoolsPopup"); + } + } + + // --- URL: Bookmark button --- + ImGui::SameLine(0, 0); + { + ImVec2 btnPos = ImGui::GetCursorScreenPos(); + ImVec2 btnSize(iconBtnW, inputFrameH2); + ImGui::InvisibleButton("##SavePoolUrl", btnSize); + bool btnHov = ImGui::IsItemHovered(); + bool btnClk = ImGui::IsItemClicked(); + ImDrawList* dl2 = ImGui::GetWindowDrawList(); + ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); + std::string currentUrl(s_pool_url); + bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolUrls(), currentUrl); + if (btnHov) { + dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), + StateHover(), 4.0f * dp); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url")); + } + ImFont* icoFont = Type().iconSmall(); + const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER; + ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon); + dl2->AddText(icoFont, icoFont->LegacySize, + ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), + alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon); + if (btnClk && !currentUrl.empty() && !alreadySaved) { + app->settings()->addSavedPoolUrl(currentUrl); + app->settings()->save(); + } + } + + // --- URL: Popup positioned below the input group --- + // Match popup width to input group; zero horizontal padding so + // item highlights are flush with the popup container edges. + ImGui::SetNextWindowPos(ImVec2(urlGroupStartX, urlGroupStartY + inputFrameH2)); + ImGui::SetNextWindowSize(ImVec2(urlGroupW, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + if (ImGui::BeginPopup("##SavedPoolsPopup")) { + const auto& savedUrls = app->settings()->getSavedPoolUrls(); + if (savedUrls.empty()) { + ImGui::SetCursorPosX(8 * dp); + ImGui::PushFont(Type().caption()); + ImGui::TextDisabled("%s", TR("mining_no_saved_pools")); + ImGui::PopFont(); + ImGui::SetCursorPosX(8 * dp); + ImGui::PushFont(Type().caption()); + ImGui::TextDisabled("%s", TR("mining_click")); + ImGui::PopFont(); + ImGui::SameLine(0, 2 * dp); + ImGui::PushFont(Type().iconSmall()); + ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER); + ImGui::PopFont(); + ImGui::SameLine(0, 2 * dp); + ImGui::PushFont(Type().caption()); + ImGui::TextDisabled("%s", TR("mining_to_save")); + ImGui::PopFont(); + } else { + std::string urlToRemove; + float popupInnerW = ImGui::GetContentRegionAvail().x; + float xZoneW = ImGui::GetFrameHeight(); + float textPadX = 8 * dp; + ImFont* rowFont = ImGui::GetFont(); + float rowFontSz = ImGui::GetFontSize(); + float rowH = ImGui::GetFrameHeight(); + for (const auto& url : savedUrls) { + ImGui::PushID(url.c_str()); + bool isCurrent = (std::string(s_pool_url) == url); + ImVec2 rowMin = ImGui::GetCursorScreenPos(); + ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH); + ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH)); + bool rowHov = ImGui::IsItemHovered(); + bool rowClk = ImGui::IsItemClicked(); + ImDrawList* pdl = ImGui::GetWindowDrawList(); + bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - xZoneW; + // Row background — flush with popup edges + if (isCurrent) + pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10)); + if (rowHov && !inXZone) + pdl->AddRectFilled(rowMin, rowMax, StateHover()); + // Item text with internal padding + float textY = rowMin.y + (rowH - rowFontSz) * 0.5f; + pdl->AddText(rowFont, rowFontSz, + ImVec2(rowMin.x + textPadX, textY), + isCurrent ? Primary() : OnSurface(), url.c_str()); + // X button — flush with right edge, icon centered + { + ImVec2 xMin(rowMax.x - xZoneW, rowMin.y); + ImVec2 xMax(rowMax.x, rowMax.y); + if (inXZone) { + pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30)); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", TR("mining_remove")); + } else if (rowHov) { + // Show faint X when row is hovered + ImFont* icoF = Type().iconSmall(); + const char* xIcon = ICON_MD_CLOSE; + ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); + ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); + pdl->AddText(icoF, icoF->LegacySize, + ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), + OnSurfaceDisabled(), xIcon); + } + // Always draw icon when hovering X zone + if (inXZone) { + ImFont* icoF = Type().iconSmall(); + const char* xIcon = ICON_MD_CLOSE; + ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); + ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); + pdl->AddText(icoF, icoF->LegacySize, + ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), + Error(), xIcon); + } + } + // Click handling + if (rowClk) { + if (inXZone) { + urlToRemove = url; + } else { + strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1); + s_pool_url[sizeof(s_pool_url) - 1] = '\0'; + s_pool_settings_dirty = true; + ImGui::CloseCurrentPopup(); + } + } + if (rowHov && !inXZone) + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::PopID(); + } + if (!urlToRemove.empty()) { + app->settings()->removeSavedPoolUrl(urlToRemove); + app->settings()->save(); + } + } + ImGui::EndPopup(); + } + ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup + ImGui::SameLine(0, Layout::spacingSm()); + float wrkGroupStartX = ImGui::GetCursorScreenPos().x; + float wrkGroupStartY = ImGui::GetCursorScreenPos().y; + float wrkGroupW = wrkW + perGroupExtra; + + ImGui::SetNextItemWidth(wrkW); + if (ImGui::InputTextWithHint("##PoolWorker", TR("mining_payout_address"), s_pool_worker, sizeof(s_pool_worker))) { + s_pool_settings_dirty = true; + } + if (ImGui::IsItemHovered()) { + std::string currentWorkerStr(s_pool_worker); + if (currentWorkerStr.empty()) { + ImGui::SetTooltip("%s", TR("mining_generate_z_address_hint")); + } else { + ImGui::SetTooltip("%s", TR("mining_payout_tooltip")); + } + } + + // --- Worker: Dropdown arrow button --- + ImGui::SameLine(0, 0); + { + ImVec2 btnPos = ImGui::GetCursorScreenPos(); + ImVec2 btnSize(iconBtnW, inputFrameH2); + ImGui::InvisibleButton("##WorkerDropdown", btnSize); + bool btnHov = ImGui::IsItemHovered(); + bool btnClk = ImGui::IsItemClicked(); + ImDrawList* dl2 = ImGui::GetWindowDrawList(); + ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); + if (btnHov) { + dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), + StateHover(), 4.0f * dp); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", TR("mining_saved_addresses")); + } + ImFont* icoFont = Type().iconSmall(); + const char* dropIcon = ICON_MD_ARROW_DROP_DOWN; + ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon); + dl2->AddText(icoFont, icoFont->LegacySize, + ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), + OnSurfaceMedium(), dropIcon); + if (btnClk) { + ImGui::OpenPopup("##SavedWorkersPopup"); + } + } + + // --- Worker: Bookmark button --- + ImGui::SameLine(0, 0); + { + ImVec2 btnPos = ImGui::GetCursorScreenPos(); + ImVec2 btnSize(iconBtnW, inputFrameH2); + ImGui::InvisibleButton("##SaveWorkerAddr", btnSize); + bool btnHov = ImGui::IsItemHovered(); + bool btnClk = ImGui::IsItemClicked(); + ImDrawList* dl2 = ImGui::GetWindowDrawList(); + ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); + std::string currentWorker(s_pool_worker); + bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker); + if (btnHov) { + dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), + StateHover(), 4.0f * dp); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address")); + } + ImFont* icoFont = Type().iconSmall(); + const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER; + ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon); + dl2->AddText(icoFont, icoFont->LegacySize, + ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), + alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon); + if (btnClk && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) { + app->settings()->addSavedPoolWorker(currentWorker); + app->settings()->save(); + } + } + + // --- Worker: Popup positioned below the input group --- + // Popup sized to fit full z-addresses without truncation; + // zero horizontal padding so item highlights are flush with edges. + float addrPopupW = std::max(wrkGroupW, availWidth * 0.55f); + ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2)); + ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + if (ImGui::BeginPopup("##SavedWorkersPopup")) { + const auto& savedWorkers = app->settings()->getSavedPoolWorkers(); + if (savedWorkers.empty()) { + ImGui::SetCursorPosX(8 * dp); + ImGui::PushFont(Type().caption()); + ImGui::TextDisabled("%s", TR("mining_no_saved_addresses")); + ImGui::PopFont(); + ImGui::SetCursorPosX(8 * dp); + ImGui::PushFont(Type().caption()); + ImGui::TextDisabled("%s", TR("mining_click")); + ImGui::PopFont(); + ImGui::SameLine(0, 2 * dp); + ImGui::PushFont(Type().iconSmall()); + ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER); + ImGui::PopFont(); + ImGui::SameLine(0, 2 * dp); + ImGui::PushFont(Type().caption()); + ImGui::TextDisabled("%s", TR("mining_to_save")); + ImGui::PopFont(); + } else { + std::string addrToRemove; + float wPopupInnerW = ImGui::GetContentRegionAvail().x; + float wXZoneW = ImGui::GetFrameHeight(); + float wTextPadX = 8 * dp; + ImFont* wRowFont = ImGui::GetFont(); + float wRowFontSz = ImGui::GetFontSize(); + float wRowH = ImGui::GetFrameHeight(); + for (const auto& addr : savedWorkers) { + ImGui::PushID(addr.c_str()); + bool isCurrent = (std::string(s_pool_worker) == addr); + ImVec2 rowMin = ImGui::GetCursorScreenPos(); + ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH); + ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH)); + bool rowHov = ImGui::IsItemHovered(); + bool rowClk = ImGui::IsItemClicked(); + ImDrawList* pdl = ImGui::GetWindowDrawList(); + bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - wXZoneW; + // Row background — flush with popup edges + if (isCurrent) + pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10)); + if (rowHov && !inXZone) + pdl->AddRectFilled(rowMin, rowMax, StateHover()); + // Full address text with internal padding + float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f; + pdl->AddText(wRowFont, wRowFontSz, + ImVec2(rowMin.x + wTextPadX, textY), + isCurrent ? Primary() : OnSurface(), addr.c_str()); + // Tooltip for long addresses + if (rowHov && !inXZone) + ImGui::SetTooltip("%s", addr.c_str()); + // X button — flush with right edge, icon centered + { + ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y); + ImVec2 xMax(rowMax.x, rowMax.y); + if (inXZone) { + pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30)); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", TR("mining_remove")); + } else if (rowHov) { + ImFont* icoF = Type().iconSmall(); + const char* xIcon = ICON_MD_CLOSE; + ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); + ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); + pdl->AddText(icoF, icoF->LegacySize, + ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), + OnSurfaceDisabled(), xIcon); + } + if (inXZone) { + ImFont* icoF = Type().iconSmall(); + const char* xIcon = ICON_MD_CLOSE; + ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); + ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); + pdl->AddText(icoF, icoF->LegacySize, + ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), + Error(), xIcon); + } + } + // Click handling + if (rowClk) { + if (inXZone) { + addrToRemove = addr; + } else { + strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1); + s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; + s_pool_settings_dirty = true; + ImGui::CloseCurrentPopup(); + } + } + if (rowHov && !inXZone) + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::PopID(); + } + if (!addrToRemove.empty()) { + app->settings()->removeSavedPoolWorker(addrToRemove); + app->settings()->save(); + } + } + ImGui::EndPopup(); + } + ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for Worker popup + + // === Reset to defaults button === + ImGui::SameLine(0, Layout::spacingSm()); + { + ImVec2 btnPos = ImGui::GetCursorScreenPos(); + ImVec2 btnSize(inputFrameH2, inputFrameH2); + ImGui::InvisibleButton("##ResetPoolDefaults", btnSize); + bool btnHov = ImGui::IsItemHovered(); + bool btnClk = ImGui::IsItemClicked(); + + ImDrawList* dl2 = ImGui::GetWindowDrawList(); + ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); + + // Hover highlight + if (btnHov) { + dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), + StateHover(), 4.0f * dp); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("%s", TR("mining_reset_defaults")); + } + + // Icon + ImFont* iconFont = Type().iconSmall(); + const char* resetIcon = ICON_MD_REFRESH; + ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, resetIcon); + dl2->AddText(iconFont, iconFont->LegacySize, + ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f), + OnSurfaceMedium(), resetIcon); + + if (btnClk) { + strncpy(s_pool_url, defaultPoolUrl(), sizeof(s_pool_url) - 1); + // Default to user's first shielded (z) address for pool payouts. + // Leave blank if no z-address exists yet. + std::string defaultAddr = defaultPoolWorkerAddress(state.addresses); + strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1); + s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; + s_pool_settings_dirty = true; + } + } + + ImGui::PopStyleVar(2); + } + + // Ensure cursor Y is at toggle bottom regardless of pool input widgets, + // so the cards below stay at the same position in both solo and pool modes. + ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y)); + ImGui::Dummy(ImVec2(0, gap * 0.5f)); +} + +} // namespace ui +} // namespace dragonx diff --git a/src/ui/windows/mining_mode_toggle.h b/src/ui/windows/mining_mode_toggle.h new file mode 100644 index 0000000..1742301 --- /dev/null +++ b/src/ui/windows/mining_mode_toggle.h @@ -0,0 +1,25 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 + +#pragma once + +#include "../../data/wallet_state.h" +#include "imgui.h" + +namespace dragonx { +class App; +namespace ui { + +// Renders the mining "Mode toggle" (SOLO | POOL segmented control + pool URL/worker inputs). +// Extracted verbatim from the monolithic mining tab. The state it mutates is passed BY REFERENCE: +// the pool-mode flag, the pool URL / worker text buffers, and the settings-dirty flag (named with +// their original identifiers so the moved body is byte-identical). +void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo& mining, + ImDrawList* dl, ImFont* capFont, ImFont* ovFont, + float dp, float hs, float gap, float availWidth, + bool& s_pool_mode, char (&s_pool_url)[256], char (&s_pool_worker)[256], + bool& s_pool_settings_dirty); + +} // namespace ui +} // namespace dragonx diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index b48adae..0656a45 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -9,6 +9,7 @@ #include "mining_earnings.h" #include "mining_stats.h" #include "mining_controls.h" +#include "mining_mode_toggle.h" #include "xmrig_download_dialog.h" #include "../../util/xmrig_updater.h" #include "../../app.h" @@ -258,537 +259,8 @@ static void RenderMiningTabContent(App* app) // ================================================================ // MODE TOGGLE — SOLO | POOL segmented control // ================================================================ - { - const bool soloMiningAvailable = app->supportsSoloMining(); - float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs; - float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size; - float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size; - float totalW = soloMiningAvailable ? (toggleW * 2) : toggleW; - - ImVec2 tMin = ImGui::GetCursorScreenPos(); - ImVec2 tMax(tMin.x + totalW, tMin.y + toggleH); - - // Glass background for the segmented control - dl->AddRectFilled(tMin, tMax, WithAlpha(OnSurface(), 15), toggleRnd); - dl->AddRect(tMin, tMax, WithAlpha(OnSurface(), 40), toggleRnd); - - ImVec2 soloMin = tMin; - ImVec2 soloMax(tMin.x + toggleW, tMax.y); - bool soloHov = material::IsRectHovered(soloMin, soloMax); - if (soloMiningAvailable) { - // SOLO button (left half) - if (!s_pool_mode) { - dl->AddRectFilled(soloMin, soloMax, WithAlpha(Primary(), 180), toggleRnd); - } else if (soloHov) { - dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd); - } - const char* label = TR("mining_solo"); - ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label); - float lx = soloMin.x + (toggleW - sz.x) * 0.5f; - float ly = soloMin.y + (toggleH - sz.y) * 0.5f; - ImU32 col = !s_pool_mode ? IM_COL32(255, 255, 255, 230) : OnSurfaceMedium(); - dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), col, label); - } - - // POOL button (right half) — disabled when solo mining is active - bool soloMiningActive = mining.generate; - ImVec2 poolMin(tMin.x + (soloMiningAvailable ? toggleW : 0.0f), tMin.y); - ImVec2 poolMax = tMax; - bool poolHov = material::IsRectHovered(poolMin, poolMax); - if (s_pool_mode) { - dl->AddRectFilled(poolMin, poolMax, WithAlpha(Primary(), 180), toggleRnd); - } else if (soloMiningActive) { - // Dimmed — solo mining blocks pool mode - } else if (poolHov) { - dl->AddRectFilled(poolMin, poolMax, WithAlpha(OnSurface(), 20), toggleRnd); - } - { - const char* label = TR("mining_pool"); - ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label); - float lx = poolMin.x + (toggleW - sz.x) * 0.5f; - float ly = poolMin.y + (toggleH - sz.y) * 0.5f; - ImU32 col = s_pool_mode ? IM_COL32(255, 255, 255, 230) - : (soloMiningActive ? OnSurfaceDisabled() : OnSurfaceMedium()); - dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), col, label); - } - - // Invisible buttons for click targets - if (soloMiningAvailable) { - ImGui::SetCursorScreenPos(soloMin); - ImGui::InvisibleButton("##SoloMode", ImVec2(toggleW, toggleH)); - if (ImGui::IsItemClicked() && s_pool_mode) { - s_pool_mode = false; - app->settings()->setPoolMode(false); - app->settings()->save(); - app->stopPoolMining(); - } - if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - } - - ImGui::SetCursorScreenPos(poolMin); - ImGui::InvisibleButton("##PoolMode", ImVec2(toggleW, toggleH)); - if (soloMiningAvailable && ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) { - s_pool_mode = true; - app->settings()->setPoolMode(true); - app->settings()->save(); - // Note: soloMiningActive is already false (checked above), - // so no need to call stopMining() — it would just set the - // toggle-in-progress flag and make the button show "STARTING". - } - if (soloMiningAvailable && poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (poolHov && soloMiningActive && !s_pool_mode) { - ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool")); - } - - ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y)); - ImGui::Dummy(ImVec2(totalW, 0)); - - // Pool URL + Worker inputs inline next to toggle (pool mode only) - if (s_pool_mode && soloMiningActive) { - // Solo mining is active — show disabled message instead of inputs - float inputFrameH = ImGui::GetFrameHeight(); - float vertOff = (toggleH - inputFrameH) * 0.5f; - ImGui::SetCursorScreenPos(ImVec2(tMax.x + Layout::spacingLg(), tMin.y + vertOff)); - - ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); - ImGui::AlignTextToFramePadding(); - ImGui::PushFont(Type().iconSmall()); - ImGui::TextUnformatted(ICON_MD_INFO); - ImGui::PopFont(); - ImGui::SameLine(0, Layout::spacingSm()); - ImGui::PushFont(capFont); - ImGui::TextUnformatted(TR("mining_stop_solo_for_pool_settings")); - ImGui::PopFont(); - ImGui::PopStyleColor(); - } else if (s_pool_mode) { - // Position inputs to the right of the toggle - float inputFrameH = ImGui::GetFrameHeight(); - float vertOff = (toggleH - inputFrameH) * 0.5f; - float inputsStartX = tMax.x + Layout::spacingLg(); - ImGui::SetCursorScreenPos(ImVec2(inputsStartX, tMin.y + vertOff)); - - ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8 * dp, 4 * dp)); - - float inputFrameH2 = ImGui::GetFrameHeight(); - float iconBtnW = inputFrameH2; - float resetBtnW = iconBtnW; - float contentEndX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x; - // Each input group: [input][▼][bookmark] - // Layout: [URL group] [spacing] [Worker group] [spacing] [reset] - float perGroupExtra = iconBtnW * 2; // dropdown + bookmark - float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW - - Layout::spacingSm() - perGroupExtra * 2; - float urlW = std::max(60.0f, remainW * 0.30f); - float wrkW = std::max(40.0f, remainW * 0.70f); - - // Track positions for popup alignment - float urlGroupStartX = ImGui::GetCursorScreenPos().x; - float urlGroupStartY = ImGui::GetCursorScreenPos().y; - float urlGroupW = urlW + perGroupExtra; - - // === Pool URL input === - ImGui::SetNextItemWidth(urlW); - if (ImGui::InputTextWithHint("##PoolURL", TR("mining_pool_url"), s_pool_url, sizeof(s_pool_url))) { - s_pool_settings_dirty = true; - } - - // --- URL: Dropdown arrow button --- - ImGui::SameLine(0, 0); - { - ImVec2 btnPos = ImGui::GetCursorScreenPos(); - ImVec2 btnSize(iconBtnW, inputFrameH2); - ImGui::InvisibleButton("##PoolDropdown", btnSize); - bool btnHov = ImGui::IsItemHovered(); - bool btnClk = ImGui::IsItemClicked(); - ImDrawList* dl2 = ImGui::GetWindowDrawList(); - ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); - if (btnHov) { - dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), - StateHover(), 4.0f * dp); - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::SetTooltip("%s", TR("mining_saved_pools")); - } - ImFont* icoFont = Type().iconSmall(); - const char* dropIcon = ICON_MD_ARROW_DROP_DOWN; - ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon); - dl2->AddText(icoFont, icoFont->LegacySize, - ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), - OnSurfaceMedium(), dropIcon); - if (btnClk) { - ImGui::OpenPopup("##SavedPoolsPopup"); - } - } - - // --- URL: Bookmark button --- - ImGui::SameLine(0, 0); - { - ImVec2 btnPos = ImGui::GetCursorScreenPos(); - ImVec2 btnSize(iconBtnW, inputFrameH2); - ImGui::InvisibleButton("##SavePoolUrl", btnSize); - bool btnHov = ImGui::IsItemHovered(); - bool btnClk = ImGui::IsItemClicked(); - ImDrawList* dl2 = ImGui::GetWindowDrawList(); - ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); - std::string currentUrl(s_pool_url); - bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolUrls(), currentUrl); - if (btnHov) { - dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), - StateHover(), 4.0f * dp); - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url")); - } - ImFont* icoFont = Type().iconSmall(); - const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER; - ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon); - dl2->AddText(icoFont, icoFont->LegacySize, - ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), - alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon); - if (btnClk && !currentUrl.empty() && !alreadySaved) { - app->settings()->addSavedPoolUrl(currentUrl); - app->settings()->save(); - } - } - - // --- URL: Popup positioned below the input group --- - // Match popup width to input group; zero horizontal padding so - // item highlights are flush with the popup container edges. - ImGui::SetNextWindowPos(ImVec2(urlGroupStartX, urlGroupStartY + inputFrameH2)); - ImGui::SetNextWindowSize(ImVec2(urlGroupW, 0)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - if (ImGui::BeginPopup("##SavedPoolsPopup")) { - const auto& savedUrls = app->settings()->getSavedPoolUrls(); - if (savedUrls.empty()) { - ImGui::SetCursorPosX(8 * dp); - ImGui::PushFont(Type().caption()); - ImGui::TextDisabled("%s", TR("mining_no_saved_pools")); - ImGui::PopFont(); - ImGui::SetCursorPosX(8 * dp); - ImGui::PushFont(Type().caption()); - ImGui::TextDisabled("%s", TR("mining_click")); - ImGui::PopFont(); - ImGui::SameLine(0, 2 * dp); - ImGui::PushFont(Type().iconSmall()); - ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER); - ImGui::PopFont(); - ImGui::SameLine(0, 2 * dp); - ImGui::PushFont(Type().caption()); - ImGui::TextDisabled("%s", TR("mining_to_save")); - ImGui::PopFont(); - } else { - std::string urlToRemove; - float popupInnerW = ImGui::GetContentRegionAvail().x; - float xZoneW = ImGui::GetFrameHeight(); - float textPadX = 8 * dp; - ImFont* rowFont = ImGui::GetFont(); - float rowFontSz = ImGui::GetFontSize(); - float rowH = ImGui::GetFrameHeight(); - for (const auto& url : savedUrls) { - ImGui::PushID(url.c_str()); - bool isCurrent = (std::string(s_pool_url) == url); - ImVec2 rowMin = ImGui::GetCursorScreenPos(); - ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH); - ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH)); - bool rowHov = ImGui::IsItemHovered(); - bool rowClk = ImGui::IsItemClicked(); - ImDrawList* pdl = ImGui::GetWindowDrawList(); - bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - xZoneW; - // Row background — flush with popup edges - if (isCurrent) - pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10)); - if (rowHov && !inXZone) - pdl->AddRectFilled(rowMin, rowMax, StateHover()); - // Item text with internal padding - float textY = rowMin.y + (rowH - rowFontSz) * 0.5f; - pdl->AddText(rowFont, rowFontSz, - ImVec2(rowMin.x + textPadX, textY), - isCurrent ? Primary() : OnSurface(), url.c_str()); - // X button — flush with right edge, icon centered - { - ImVec2 xMin(rowMax.x - xZoneW, rowMin.y); - ImVec2 xMax(rowMax.x, rowMax.y); - if (inXZone) { - pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30)); - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::SetTooltip("%s", TR("mining_remove")); - } else if (rowHov) { - // Show faint X when row is hovered - ImFont* icoF = Type().iconSmall(); - const char* xIcon = ICON_MD_CLOSE; - ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); - ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); - pdl->AddText(icoF, icoF->LegacySize, - ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), - OnSurfaceDisabled(), xIcon); - } - // Always draw icon when hovering X zone - if (inXZone) { - ImFont* icoF = Type().iconSmall(); - const char* xIcon = ICON_MD_CLOSE; - ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); - ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); - pdl->AddText(icoF, icoF->LegacySize, - ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), - Error(), xIcon); - } - } - // Click handling - if (rowClk) { - if (inXZone) { - urlToRemove = url; - } else { - strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1); - s_pool_url[sizeof(s_pool_url) - 1] = '\0'; - s_pool_settings_dirty = true; - ImGui::CloseCurrentPopup(); - } - } - if (rowHov && !inXZone) - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::PopID(); - } - if (!urlToRemove.empty()) { - app->settings()->removeSavedPoolUrl(urlToRemove); - app->settings()->save(); - } - } - ImGui::EndPopup(); - } - ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup - ImGui::SameLine(0, Layout::spacingSm()); - float wrkGroupStartX = ImGui::GetCursorScreenPos().x; - float wrkGroupStartY = ImGui::GetCursorScreenPos().y; - float wrkGroupW = wrkW + perGroupExtra; - - ImGui::SetNextItemWidth(wrkW); - if (ImGui::InputTextWithHint("##PoolWorker", TR("mining_payout_address"), s_pool_worker, sizeof(s_pool_worker))) { - s_pool_settings_dirty = true; - } - if (ImGui::IsItemHovered()) { - std::string currentWorkerStr(s_pool_worker); - if (currentWorkerStr.empty()) { - ImGui::SetTooltip("%s", TR("mining_generate_z_address_hint")); - } else { - ImGui::SetTooltip("%s", TR("mining_payout_tooltip")); - } - } - - // --- Worker: Dropdown arrow button --- - ImGui::SameLine(0, 0); - { - ImVec2 btnPos = ImGui::GetCursorScreenPos(); - ImVec2 btnSize(iconBtnW, inputFrameH2); - ImGui::InvisibleButton("##WorkerDropdown", btnSize); - bool btnHov = ImGui::IsItemHovered(); - bool btnClk = ImGui::IsItemClicked(); - ImDrawList* dl2 = ImGui::GetWindowDrawList(); - ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); - if (btnHov) { - dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), - StateHover(), 4.0f * dp); - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::SetTooltip("%s", TR("mining_saved_addresses")); - } - ImFont* icoFont = Type().iconSmall(); - const char* dropIcon = ICON_MD_ARROW_DROP_DOWN; - ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon); - dl2->AddText(icoFont, icoFont->LegacySize, - ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), - OnSurfaceMedium(), dropIcon); - if (btnClk) { - ImGui::OpenPopup("##SavedWorkersPopup"); - } - } - - // --- Worker: Bookmark button --- - ImGui::SameLine(0, 0); - { - ImVec2 btnPos = ImGui::GetCursorScreenPos(); - ImVec2 btnSize(iconBtnW, inputFrameH2); - ImGui::InvisibleButton("##SaveWorkerAddr", btnSize); - bool btnHov = ImGui::IsItemHovered(); - bool btnClk = ImGui::IsItemClicked(); - ImDrawList* dl2 = ImGui::GetWindowDrawList(); - ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); - std::string currentWorker(s_pool_worker); - bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker); - if (btnHov) { - dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), - StateHover(), 4.0f * dp); - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address")); - } - ImFont* icoFont = Type().iconSmall(); - const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER; - ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon); - dl2->AddText(icoFont, icoFont->LegacySize, - ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), - alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon); - if (btnClk && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) { - app->settings()->addSavedPoolWorker(currentWorker); - app->settings()->save(); - } - } - - // --- Worker: Popup positioned below the input group --- - // Popup sized to fit full z-addresses without truncation; - // zero horizontal padding so item highlights are flush with edges. - float addrPopupW = std::max(wrkGroupW, availWidth * 0.55f); - ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2)); - ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - if (ImGui::BeginPopup("##SavedWorkersPopup")) { - const auto& savedWorkers = app->settings()->getSavedPoolWorkers(); - if (savedWorkers.empty()) { - ImGui::SetCursorPosX(8 * dp); - ImGui::PushFont(Type().caption()); - ImGui::TextDisabled("%s", TR("mining_no_saved_addresses")); - ImGui::PopFont(); - ImGui::SetCursorPosX(8 * dp); - ImGui::PushFont(Type().caption()); - ImGui::TextDisabled("%s", TR("mining_click")); - ImGui::PopFont(); - ImGui::SameLine(0, 2 * dp); - ImGui::PushFont(Type().iconSmall()); - ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER); - ImGui::PopFont(); - ImGui::SameLine(0, 2 * dp); - ImGui::PushFont(Type().caption()); - ImGui::TextDisabled("%s", TR("mining_to_save")); - ImGui::PopFont(); - } else { - std::string addrToRemove; - float wPopupInnerW = ImGui::GetContentRegionAvail().x; - float wXZoneW = ImGui::GetFrameHeight(); - float wTextPadX = 8 * dp; - ImFont* wRowFont = ImGui::GetFont(); - float wRowFontSz = ImGui::GetFontSize(); - float wRowH = ImGui::GetFrameHeight(); - for (const auto& addr : savedWorkers) { - ImGui::PushID(addr.c_str()); - bool isCurrent = (std::string(s_pool_worker) == addr); - ImVec2 rowMin = ImGui::GetCursorScreenPos(); - ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH); - ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH)); - bool rowHov = ImGui::IsItemHovered(); - bool rowClk = ImGui::IsItemClicked(); - ImDrawList* pdl = ImGui::GetWindowDrawList(); - bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - wXZoneW; - // Row background — flush with popup edges - if (isCurrent) - pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10)); - if (rowHov && !inXZone) - pdl->AddRectFilled(rowMin, rowMax, StateHover()); - // Full address text with internal padding - float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f; - pdl->AddText(wRowFont, wRowFontSz, - ImVec2(rowMin.x + wTextPadX, textY), - isCurrent ? Primary() : OnSurface(), addr.c_str()); - // Tooltip for long addresses - if (rowHov && !inXZone) - ImGui::SetTooltip("%s", addr.c_str()); - // X button — flush with right edge, icon centered - { - ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y); - ImVec2 xMax(rowMax.x, rowMax.y); - if (inXZone) { - pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30)); - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::SetTooltip("%s", TR("mining_remove")); - } else if (rowHov) { - ImFont* icoF = Type().iconSmall(); - const char* xIcon = ICON_MD_CLOSE; - ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); - ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); - pdl->AddText(icoF, icoF->LegacySize, - ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), - OnSurfaceDisabled(), xIcon); - } - if (inXZone) { - ImFont* icoF = Type().iconSmall(); - const char* xIcon = ICON_MD_CLOSE; - ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); - ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); - pdl->AddText(icoF, icoF->LegacySize, - ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), - Error(), xIcon); - } - } - // Click handling - if (rowClk) { - if (inXZone) { - addrToRemove = addr; - } else { - strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1); - s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; - s_pool_settings_dirty = true; - ImGui::CloseCurrentPopup(); - } - } - if (rowHov && !inXZone) - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::PopID(); - } - if (!addrToRemove.empty()) { - app->settings()->removeSavedPoolWorker(addrToRemove); - app->settings()->save(); - } - } - ImGui::EndPopup(); - } - ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for Worker popup - - // === Reset to defaults button === - ImGui::SameLine(0, Layout::spacingSm()); - { - ImVec2 btnPos = ImGui::GetCursorScreenPos(); - ImVec2 btnSize(inputFrameH2, inputFrameH2); - ImGui::InvisibleButton("##ResetPoolDefaults", btnSize); - bool btnHov = ImGui::IsItemHovered(); - bool btnClk = ImGui::IsItemClicked(); - - ImDrawList* dl2 = ImGui::GetWindowDrawList(); - ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); - - // Hover highlight - if (btnHov) { - dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), - StateHover(), 4.0f * dp); - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - ImGui::SetTooltip("%s", TR("mining_reset_defaults")); - } - - // Icon - ImFont* iconFont = Type().iconSmall(); - const char* resetIcon = ICON_MD_REFRESH; - ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, resetIcon); - dl2->AddText(iconFont, iconFont->LegacySize, - ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f), - OnSurfaceMedium(), resetIcon); - - if (btnClk) { - strncpy(s_pool_url, defaultPoolUrl(), sizeof(s_pool_url) - 1); - // Default to user's first shielded (z) address for pool payouts. - // Leave blank if no z-address exists yet. - std::string defaultAddr = defaultPoolWorkerAddress(state.addresses); - strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1); - s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; - s_pool_settings_dirty = true; - } - } - - ImGui::PopStyleVar(2); - } - - // Ensure cursor Y is at toggle bottom regardless of pool input widgets, - // so the cards below stay at the same position in both solo and pool modes. - ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y)); - ImGui::Dummy(ImVec2(0, gap * 0.5f)); - } + RenderMiningModeToggle(app, state, mining, dl, capFont, ovFont, dp, hs, gap, availWidth, + s_pool_mode, s_pool_url, s_pool_worker, s_pool_settings_dirty); // ================================================================ // CONTROLS — Glass card with CPU core grid (no heading)