From c83348ab63978f352d9c94733b44aefd4b918484 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 4 Jul 2026 19:25:20 -0500 Subject: [PATCH] refactor(ui): add material::IconButton helper + adopt at 8 frameless icon buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI-standardization audit (icon-button discovery pass): the app had ~22 clean hand-rolled icon buttons repeating the same "InvisibleButton hit-target + centered glyph + hover feedback + tooltip" boilerplate. Add a shared material::IconButton(id, glyph, font, size, IconButtonStyle) — centered glyph, optional resting + hover backgrounds (pill/rounded-rect), hover recolor, hand cursor + tooltip; caller resolves state-dependent glyph/colors and returns clicked. Adopt at the homogeneous, self-contained sites, pixel-faithful: - mining_mode_toggle: pool/worker dropdown + bookmark + reset (×5) - network: server hide/unhide eye (×1) - mining_controls: pool benchmark start (×1) - mining_stats: pool-balance refresh (×1) Deferred (own follow-up + sweep): the idle/scale/gpu active-pill toggles use a "draw pill+glyph earlier, hit-test later" idiom needing restructuring; the raw-rect (IsRectHovered, no InvisibleButton) sites in market/mining_stats/ balance are a different hit-test mechanism left intentionally. Framed icon pagers already ride TactileButton after the prior commit. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/material/draw_helpers.h | 41 ++++++++ src/ui/windows/mining_controls.cpp | 25 +---- src/ui/windows/mining_mode_toggle.cpp | 143 +++++++------------------- src/ui/windows/mining_stats.cpp | 22 ++-- src/ui/windows/network_tab.cpp | 23 ++--- 5 files changed, 98 insertions(+), 156 deletions(-) diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index 32c8a9a..d3fc7c1 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -354,6 +354,47 @@ inline ImVec2 DrawPill(ImDrawList* dl, const ImVec2& pos, const char* text, ImFo return sz; } +// ── Frameless icon button ──────────────────────────────────────────────── +// Style for IconButton. All colors default to 0 (= unset): color falls back to OnSurfaceMedium(); +// hoverColor==0 means "no hover recolor"; hoverBg/restBg==0 mean "no background". +struct IconButtonStyle { + ImU32 color = 0; // resting glyph color (0 => OnSurfaceMedium()) + ImU32 hoverColor = 0; // glyph color while hovered (0 => same as color) + ImU32 hoverBg = 0; // fill drawn behind the glyph on hover only (0 => none) + ImU32 restBg = 0; // fill drawn every frame, e.g. an "active" pill (0 => none) + float bgRounding = -1.0f; // <0 => size.y*0.5 (pill/circle for a square); else that radius + bool handCursor = true; + const char* tooltip = nullptr; +}; + +// A frameless / pill icon button: an InvisibleButton(id, size) at the current cursor with a centered +// `glyph` (in `font`), optional resting + hover backgrounds, hover recolor, hand cursor + tooltip. +// Returns true the frame it is clicked. The caller resolves any state-dependent glyph/colors/tooltip +// before the call and positions the cursor (SetCursorScreenPos / SameLine) as needed. +inline bool IconButton(const char* id, const char* glyph, ImFont* font, const ImVec2& size, + const IconButtonStyle& st) +{ + const ImVec2 pos = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton(id, size); + const bool hov = ImGui::IsItemHovered(); + const bool clk = ImGui::IsItemClicked(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImVec2 pMax(pos.x + size.x, pos.y + size.y); + const float round = st.bgRounding < 0.0f ? size.y * 0.5f : st.bgRounding; + if (st.restBg != 0) dl->AddRectFilled(pos, pMax, st.restBg, round); + if (hov && st.hoverBg != 0) dl->AddRectFilled(pos, pMax, st.hoverBg, round); + if (hov) { + if (st.handCursor) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (st.tooltip && st.tooltip[0]) Tooltip("%s", st.tooltip); + } + const ImU32 gcol = (hov && st.hoverColor != 0) ? st.hoverColor + : (st.color != 0 ? st.color : OnSurfaceMedium()); + const ImVec2 gsz = font->CalcTextSizeA(font->LegacySize, FLT_MAX, 0, glyph); + dl->AddText(font, font->LegacySize, + ImVec2(pos.x + (size.x - gsz.x) * 0.5f, pos.y + (size.y - gsz.y) * 0.5f), gcol, glyph); + return clk; +} + // ── Tactile wrappers ──────────────────────────────────────────────────── // Drop-in replacements for ImGui::Button / SmallButton that automatically // add the tactile highlight overlay after rendering. diff --git a/src/ui/windows/mining_controls.cpp b/src/ui/windows/mining_controls.cpp index faadf8f..b7fbe9c 100644 --- a/src/ui/windows/mining_controls.cpp +++ b/src/ui/windows/mining_controls.cpp @@ -487,27 +487,12 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& float btnX = idleRightEdge - btnSz; float btnY = curY + (headerH - btnSz) * 0.5f; + material::IconButtonStyle st; + st.color = OnSurfaceMedium(); + st.hoverBg = StateHover(); + st.tooltip = TR("mining_benchmark_tooltip"); ImGui::SetCursorScreenPos(ImVec2(btnX, btnY)); - ImGui::InvisibleButton("##BenchStart", ImVec2(btnSz, btnSz)); - bool benchHovered = ImGui::IsItemHovered(); - bool benchClicked = ImGui::IsItemClicked(); - - // Hover highlight - if (benchHovered) { - dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz), - StateHover(), btnSz * 0.5f); - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - material::Tooltip("%s", TR("mining_benchmark_tooltip")); - } - - const char* benchIcon = ICON_MD_SPEED; - ImVec2 bIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, benchIcon); - dl->AddText(icoFont, icoFont->LegacySize, - ImVec2(btnX + (btnSz - bIcoSz.x) * 0.5f, - btnY + (btnSz - bIcoSz.y) * 0.5f), - OnSurfaceMedium(), benchIcon); - - if (benchClicked) { + if (material::IconButton("##BenchStart", ICON_MD_SPEED, icoFont, ImVec2(btnSz, btnSz), st)) { // Require a wallet address for pool mining std::string worker(s_pool_worker); if (!worker.empty()) { diff --git a/src/ui/windows/mining_mode_toggle.cpp b/src/ui/windows/mining_mode_toggle.cpp index ac08d71..bad367e 100644 --- a/src/ui/windows/mining_mode_toggle.cpp +++ b/src/ui/windows/mining_mode_toggle.cpp @@ -190,26 +190,13 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo // --- 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); - material::Tooltip("%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) { + material::IconButtonStyle st; + st.color = OnSurfaceMedium(); + st.hoverBg = StateHover(); + st.bgRounding = 4.0f * dp; + st.tooltip = TR("mining_saved_pools"); + if (material::IconButton("##PoolDropdown", ICON_MD_ARROW_DROP_DOWN, Type().iconSmall(), + ImVec2(iconBtnW, inputFrameH2), st)) { ImGui::OpenPopup("##SavedPoolsPopup"); } } @@ -217,28 +204,16 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo // --- 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); - material::Tooltip("%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) { + material::IconButtonStyle st; + st.color = alreadySaved ? Primary() : OnSurfaceMedium(); + st.hoverBg = StateHover(); + st.bgRounding = 4.0f * dp; + st.tooltip = alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url"); + if (material::IconButton("##SavePoolUrl", alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER, + Type().iconSmall(), ImVec2(iconBtnW, inputFrameH2), st) + && !currentUrl.empty() && !alreadySaved) { app->settings()->addSavedPoolUrl(currentUrl); app->settings()->save(); } @@ -412,26 +387,13 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo // --- 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); - material::Tooltip("%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) { + material::IconButtonStyle st; + st.color = OnSurfaceMedium(); + st.hoverBg = StateHover(); + st.bgRounding = 4.0f * dp; + st.tooltip = TR("mining_saved_addresses"); + if (material::IconButton("##WorkerDropdown", ICON_MD_ARROW_DROP_DOWN, Type().iconSmall(), + ImVec2(iconBtnW, inputFrameH2), st)) { ImGui::OpenPopup("##SavedWorkersPopup"); } } @@ -439,28 +401,16 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo // --- 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); - material::Tooltip("%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) { + material::IconButtonStyle st; + st.color = alreadySaved ? Primary() : OnSurfaceMedium(); + st.hoverBg = StateHover(); + st.bgRounding = 4.0f * dp; + st.tooltip = alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address"); + if (material::IconButton("##SaveWorkerAddr", alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER, + Type().iconSmall(), ImVec2(iconBtnW, inputFrameH2), st) + && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) { app->settings()->addSavedPoolWorker(currentWorker); app->settings()->save(); } @@ -579,32 +529,13 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo // === 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); - material::Tooltip("%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) { + material::IconButtonStyle st; + st.color = OnSurfaceMedium(); + st.hoverBg = StateHover(); + st.bgRounding = 4.0f * dp; + st.tooltip = TR("mining_reset_defaults"); + if (material::IconButton("##ResetPoolDefaults", ICON_MD_REFRESH, Type().iconSmall(), + ImVec2(inputFrameH2, inputFrameH2), st)) { 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. diff --git a/src/ui/windows/mining_stats.cpp b/src/ui/windows/mining_stats.cpp index 0c0a46f..91e446b 100644 --- a/src/ui/windows/mining_stats.cpp +++ b/src/ui/windows/mining_stats.cpp @@ -258,24 +258,16 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d (int)util::knownPools().size()); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), hdr); - ImFont* icoFont = Type().iconSmall(); - const char* icon = ICON_MD_REFRESH; - ImVec2 iSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, icon); float btnS = ovFont->LegacySize + 6 * dp; ImVec2 rbMin(x + w - btnS, y - 2 * dp); - ImVec2 rbMax(x + w, y - 2 * dp + btnS); + material::IconButtonStyle st; + st.color = OnSurfaceMedium(); + st.hoverBg = StateHover(); + st.bgRounding = 3 * dp; + st.tooltip = TR("mining_refresh"); ImGui::SetCursorScreenPos(rbMin); - ImGui::InvisibleButton("##PoolRefresh", ImVec2(btnS, btnS)); - bool rhov = ImGui::IsItemHovered(); - if (rhov) { - dl->AddRectFilled(rbMin, rbMax, StateHover(), 3 * dp); - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - material::Tooltip("%s", TR("mining_refresh")); - } - dl->AddText(icoFont, icoFont->LegacySize, - ImVec2((rbMin.x + rbMax.x - iSz.x) * 0.5f, (rbMin.y + rbMax.y - iSz.y) * 0.5f), - OnSurfaceMedium(), icon); - if (ImGui::IsItemClicked()) app->requestPoolBalanceRefresh(); + if (material::IconButton("##PoolRefresh", ICON_MD_REFRESH, Type().iconSmall(), ImVec2(btnS, btnS), st)) + app->requestPoolBalanceRefresh(); } y += ovFont->LegacySize + 4 * dp; diff --git a/src/ui/windows/network_tab.cpp b/src/ui/windows/network_tab.cpp index b93ae31..115064d 100644 --- a/src/ui/windows/network_tab.cpp +++ b/src/ui/windows/network_tab.cpp @@ -294,25 +294,18 @@ void RenderLiteNetworkTab(App* app) } // Hide / unhide button (far-right strip). - ImVec2 hideMin(cardMin.x + cardBtnW, cardMin.y); - ImGui::SetCursorScreenPos(hideMin); - ImGui::InvisibleButton(("##litehide_" + sv.url).c_str(), ImVec2(hideW, cardH)); - bool hideHov = ImGui::IsItemHovered(); - if (hideHov) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - material::Tooltip("%s", hiddenList ? TR("lite_net_unhide") : TR("lite_net_hide")); - } - if (ImGui::IsItemClicked()) { + ImGui::SetCursorScreenPos(ImVec2(cardMin.x + cardBtnW, cardMin.y)); + material::IconButtonStyle hideSt; + hideSt.color = OnSurfaceDisabled(); + hideSt.hoverColor = OnSurface(); + hideSt.tooltip = hiddenList ? TR("lite_net_unhide") : TR("lite_net_hide"); + if (material::IconButton(("##litehide_" + sv.url).c_str(), + hiddenList ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF, + Type().iconSmall(), ImVec2(hideW, cardH), hideSt)) { if (hiddenList) st->unhideLiteServer(sv.url); else st->hideLiteServer(sv.url); st->save(); } - ImFont* ico = Type().iconSmall(); - const char* eye = hiddenList ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF; - ImVec2 icoSz = ico->CalcTextSizeA(ico->LegacySize, FLT_MAX, 0, eye); - dl->AddText(ico, ico->LegacySize, - ImVec2(hideMin.x + (hideW - icoSz.x) * 0.5f, cardMin.y + (cardH - icoSz.y) * 0.5f), - hideHov ? OnSurface() : OnSurfaceDisabled(), eye); // Advance to the next card via a real item (Dummy) below the card, so the scroll region's // content height actually grows — ImGui won't extend bounds from a bare SetCursorScreenPos.