@@ -5,7 +5,10 @@
# include "mining_stats.h"
# include "mining_tab_helpers.h" // FormatHashrate, EstimateHoursToBlock
# include "../../app.h"
# include "../../config/settings.h"
# include "../../util/i18n.h"
# include "../../util/pool_registry.h"
# include "../schema/ui_schema.h"
# include "../material/type.h"
# include "../material/draw_helpers.h"
@@ -17,6 +20,7 @@
# include <algorithm>
# include <cmath>
# include <cstdio>
# include <cstring>
# include <string>
# include <vector>
@@ -29,297 +33,495 @@ using namespace material;
static bool s_show_pool_log = false ;
static bool s_show_solo_log = false ;
// ---------------------------------------------------------------------------
// Sparkline chart of a hashrate history, drawn inside [regionMin, regionMax]
// (the plot area within a card, already inset by padding). Extracted so both
// the solo full-width card and the pool split card share one implementation.
// ---------------------------------------------------------------------------
static void DrawHashrateSparkline ( ImDrawList * dl , ImVec2 regionMin , ImVec2 regionMax ,
const std : : vector < double > & chartHistory ,
ImFont * capFont , float dp )
{
if ( chartHistory . size ( ) < 2 ) return ;
const float chartBot = regionMax . y ;
double yMin = * std : : min_element ( chartHistory . begin ( ) , chartHistory . end ( ) ) ;
double yMax = * std : : max_element ( chartHistory . begin ( ) , chartHistory . end ( ) ) ;
if ( yMax < = yMin ) { yMax = yMin + 1.0 ; }
double yRange = yMax - yMin ;
double yPad2 = yRange * 0.1 ;
yMin - = yPad2 ;
yMax + = yPad2 ;
const float plotLeft = regionMin . x ;
const float plotRight = regionMax . x ;
const float plotTop = regionMin . y + capFont - > LegacySize + 4 * dp ;
const float plotBottom = chartBot - capFont - > LegacySize * 2 - 16 * dp ;
const float plotW = plotRight - plotLeft ;
const float plotH = std : : max ( 1.0f , plotBottom - plotTop ) ;
size_t n = chartHistory . size ( ) ;
float stepW = ( n > 1 ) ? plotW / ( float ) ( n - 1 ) : plotW ;
std : : vector < ImVec2 > rawPts ( n ) ;
for ( size_t i = 0 ; i < n ; i + + ) {
float x = plotLeft + ( float ) i * stepW ;
float y = plotBottom - ( float ) ( ( chartHistory [ i ] - yMin ) / ( yMax - yMin ) ) * plotH ;
rawPts [ i ] = ImVec2 ( x , y ) ;
}
// Catmull-Rom spline interpolation for a smooth curve (adaptive subdivision).
std : : vector < ImVec2 > points ;
if ( n < = 2 ) {
points = rawPts ;
} else {
points . reserve ( n * 4 ) ;
for ( size_t i = 0 ; i + 1 < n ; i + + ) {
ImVec2 p0 = rawPts [ i > 0 ? i - 1 : 0 ] ;
ImVec2 p1 = rawPts [ i ] ;
ImVec2 p2 = rawPts [ i + 1 ] ;
ImVec2 p3 = rawPts [ i + 2 < n ? i + 2 : n - 1 ] ;
float dx = p2 . x - p1 . x , dy = p2 . y - p1 . y ;
float dist = sqrtf ( dx * dx + dy * dy ) ;
int subdivs = std : : clamp ( ( int ) ( dist / 3.0f ) , 1 , 16 ) ;
for ( int s = 0 ; s < subdivs ; s + + ) {
float t = ( float ) s / ( float ) subdivs ;
float t2 = t * t ;
float t3 = t2 * t ;
float q0 = - t3 + 2.0f * t2 - t ;
float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f ;
float q2 = - 3.0f * t3 + 4.0f * t2 + t ;
float q3 = t3 - t2 ;
float sx = 0.5f * ( p0 . x * q0 + p1 . x * q1 + p2 . x * q2 + p3 . x * q3 ) ;
float sy = 0.5f * ( p0 . y * q0 + p1 . y * q1 + p2 . y * q2 + p3 . y * q3 ) ;
sy = std : : clamp ( sy , plotTop , plotBottom ) ;
points . push_back ( ImVec2 ( sx , sy ) ) ;
}
}
points . push_back ( rawPts [ n - 1 ] ) ;
}
if ( points . size ( ) > = 2 ) {
for ( size_t i = 0 ; i < points . size ( ) ; i + + )
dl - > PathLineTo ( points [ i ] ) ;
dl - > PathLineTo ( ImVec2 ( points . back ( ) . x , plotBottom ) ) ;
dl - > PathLineTo ( ImVec2 ( points . front ( ) . x , plotBottom ) ) ;
dl - > PathFillConcave ( WithAlpha ( Success ( ) , 25 ) ) ;
}
dl - > AddPolyline ( points . data ( ) , ( int ) points . size ( ) , WithAlpha ( Success ( ) , 200 ) ,
ImDrawFlags_None ,
schema : : UI ( ) . drawElement ( " tabs.mining " , " chart-line-thickness " ) . size ) ;
// Y-axis labels
std : : string yMaxStr = FormatHashrate ( yMax ) ;
std : : string yMinStr = FormatHashrate ( yMin ) ;
dl - > AddText ( capFont , capFont - > LegacySize , ImVec2 ( plotLeft + 2 * dp , plotTop - capFont - > LegacySize - 2 * dp ) , OnSurfaceDisabled ( ) , yMaxStr . c_str ( ) ) ;
dl - > AddText ( capFont , capFont - > LegacySize , ImVec2 ( plotLeft + 2 * dp , plotBottom + 4 * dp ) , OnSurfaceDisabled ( ) , yMinStr . c_str ( ) ) ;
// X-axis labels
dl - > AddText ( capFont , capFont - > LegacySize ,
ImVec2 ( plotLeft , chartBot - capFont - > LegacySize - 2 * dp ) , OnSurfaceDisabled ( ) ,
chartHistory . size ( ) > = 300 ? TR ( " mining_chart_5m_ago " ) :
chartHistory . size ( ) > = 60 ? TR ( " mining_chart_1m_ago " ) : TR ( " mining_chart_start " ) ) ;
std : : string nowLbl = TR ( " mining_chart_now " ) ;
ImVec2 nowSz = capFont - > CalcTextSizeA ( capFont - > LegacySize , 10000 , 0 , nowLbl . c_str ( ) ) ;
dl - > AddText ( capFont , capFont - > LegacySize ,
ImVec2 ( plotRight - nowSz . x , chartBot - capFont - > LegacySize - 2 * dp ) ,
OnSurfaceDisabled ( ) , nowLbl . c_str ( ) ) ;
}
// ---------------------------------------------------------------------------
// Read-only, selectable/copyable log view filling [regionMin, size].
// ---------------------------------------------------------------------------
static void DrawMiningLogView ( ImVec2 regionMin , float regionW , float regionH ,
const std : : vector < std : : string > & logLines , const char * inputId )
{
static std : : string s_log_buf ;
s_log_buf . clear ( ) ;
for ( const auto & line : logLines )
if ( ! line . empty ( ) ) { s_log_buf + = line ; s_log_buf + = ' \n ' ; }
ImGui : : SetCursorScreenPos ( regionMin ) ;
ImGui : : PushStyleColor ( ImGuiCol_Text , ImGui : : ColorConvertU32ToFloat4 ( OnSurface ( ) ) ) ;
ImGui : : PushStyleColor ( ImGuiCol_FrameBg , ImVec4 ( 0 , 0 , 0 , 0 ) ) ;
ImGui : : PushFont ( Type ( ) . body2 ( ) ) ;
ImGui : : InputTextMultiline ( inputId , const_cast < char * > ( s_log_buf . c_str ( ) ) , s_log_buf . size ( ) + 1 ,
ImVec2 ( regionW , regionH ) , ImGuiInputTextFlags_ReadOnly ) ;
ImGui : : PopFont ( ) ;
ImGui : : PopStyleColor ( 2 ) ;
}
// ---------------------------------------------------------------------------
// The left pool card (pool mode only): Manual/Auto toggle, a metric stack, and
// a fixed-height scrolling pool list. Pool rows are clickable to select in
// Manual mode (routed through the parent's dirty flag → save + restart) and
// show a "mining here" dot in Auto mode. The list scrolls, so any number of
// pools keeps the card — and everything below it — at a constant height.
// ---------------------------------------------------------------------------
static void RenderLeftPoolCard ( App * app , const WalletState & state , ImDrawList * dl ,
ImFont * capFont , ImFont * sub1 , ImFont * ovFont ,
float dp , float gap , float pad ,
ImVec2 cardMin , ImVec2 cardMax ,
char ( & s_pool_url ) [ 256 ] , bool & s_pool_settings_dirty )
{
const bool autoMode = app - > settings ( ) - > getPoolSelectMode ( )
= = config : : Settings : : PoolSelectMode : : AutoBalance ;
const float x = cardMin . x + pad ;
const float w = ( cardMax . x - cardMin . x ) - pad * 2 ;
float y = cardMin . y + pad * 0.75f ;
const float toggleH = sub1 - > LegacySize + 10 * dp ;
const float rowH = sub1 - > LegacySize + 7 * dp ;
// --- Manual | Auto-balance toggle ---
{
ImFont * segFont = Type ( ) . caption ( ) ;
const char * segA = TR ( " mining_select_manual " ) ;
const char * segB = TR ( " mining_select_auto " ) ;
const float rnd = 4 * dp ;
const float half = w * 0.5f ;
const ImVec2 aMin ( x , y ) , aMax ( x + half , y + toggleH ) ;
const ImVec2 bMin ( x + half , y ) , bMax ( x + w , y + toggleH ) ;
dl - > AddRectFilled ( aMin , bMax , WithAlpha ( OnSurface ( ) , 15 ) , rnd ) ;
dl - > AddRect ( aMin , bMax , WithAlpha ( OnSurface ( ) , 40 ) , rnd ) ;
auto seg = [ & ] ( const ImVec2 & mn , const ImVec2 & mx , const char * label , bool selected ) {
if ( selected ) dl - > AddRectFilled ( mn , mx , WithAlpha ( Primary ( ) , 180 ) , rnd ) ;
else if ( material : : IsRectHovered ( mn , mx ) ) dl - > AddRectFilled ( mn , mx , WithAlpha ( OnSurface ( ) , 20 ) , rnd ) ;
ImVec2 sz = segFont - > CalcTextSizeA ( segFont - > LegacySize , FLT_MAX , 0 , label ) ;
dl - > AddText ( segFont , segFont - > LegacySize ,
ImVec2 ( mn . x + ( mx . x - mn . x - sz . x ) * 0.5f , mn . y + ( mx . y - mn . y - sz . y ) * 0.5f ) ,
selected ? IM_COL32 ( 255 , 255 , 255 , 230 ) : OnSurfaceMedium ( ) , label ) ;
} ;
seg ( aMin , aMax , segA , ! autoMode ) ;
seg ( bMin , bMax , segB , autoMode ) ;
ImGui : : SetCursorScreenPos ( aMin ) ;
ImGui : : InvisibleButton ( " ##CardSelManual " , ImVec2 ( half , toggleH ) ) ;
if ( ImGui : : IsItemClicked ( ) & & autoMode ) {
app - > settings ( ) - > setPoolSelectMode ( config : : Settings : : PoolSelectMode : : Manual ) ;
app - > settings ( ) - > save ( ) ;
}
if ( material : : IsRectHovered ( aMin , aMax ) ) {
ImGui : : SetMouseCursor ( ImGuiMouseCursor_Hand ) ;
material : : Tooltip ( " %s " , TR ( " mining_manual_desc " ) ) ;
}
ImGui : : SetCursorScreenPos ( bMin ) ;
ImGui : : InvisibleButton ( " ##CardSelAuto " , ImVec2 ( w - half , toggleH ) ) ;
if ( ImGui : : IsItemClicked ( ) & & ! autoMode ) {
app - > settings ( ) - > setPoolSelectMode ( config : : Settings : : PoolSelectMode : : AutoBalance ) ;
app - > settings ( ) - > save ( ) ;
app - > requestPoolBalanceRefresh ( ) ;
}
if ( material : : IsRectHovered ( bMin , bMax ) ) {
ImGui : : SetMouseCursor ( ImGuiMouseCursor_Hand ) ;
material : : Tooltip ( " %s " , TR ( " mining_auto_balance_desc " ) ) ;
}
}
y + = toggleH + gap ;
// --- Metric stack (label left, value right) ---
auto metricRow = [ & ] ( const char * label , const std : : string & val , ImU32 valCol ) {
dl - > AddText ( ovFont , ovFont - > LegacySize ,
ImVec2 ( x , y + ( rowH - ovFont - > LegacySize ) * 0.5f ) , OnSurfaceMedium ( ) , label ) ;
ImVec2 vSz = sub1 - > CalcTextSizeA ( sub1 - > LegacySize , FLT_MAX , 0 , val . c_str ( ) ) ;
dl - > AddText ( sub1 , sub1 - > LegacySize ,
ImVec2 ( x + w - vSz . x , y + ( rowH - sub1 - > LegacySize ) * 0.5f ) , valCol , val . c_str ( ) ) ;
y + = rowH ;
} ;
const auto & pm = state . pool_mining ;
metricRow ( TR ( " mining_local_hashrate " ) , FormatHashrate ( pm . hashrate_10s ) ,
pm . xmrig_running ? Success ( ) : OnSurfaceDisabled ( ) ) ;
metricRow ( TR ( " mining_pool_hashrate " ) , FormatHashrate ( pm . pool_hashrate ) ,
pm . pool_hashrate > 0 ? OnSurface ( ) : OnSurfaceDisabled ( ) ) ;
{
char sharesBuf [ 64 ] ;
snprintf ( sharesBuf , sizeof ( sharesBuf ) , " %lld / %lld " ,
( long long ) pm . accepted , ( long long ) pm . rejected ) ;
metricRow ( TR ( " mining_shares " ) , sharesBuf , OnSurface ( ) ) ;
}
{
char uptBuf [ 64 ] ;
int64_t up = pm . uptime_sec ;
if ( up < = 0 ) snprintf ( uptBuf , sizeof ( uptBuf ) , " N/A " ) ;
else if ( up < 3600 ) snprintf ( uptBuf , sizeof ( uptBuf ) , " %lldm %llds " , ( long long ) ( up / 60 ) , ( long long ) ( up % 60 ) ) ;
else snprintf ( uptBuf , sizeof ( uptBuf ) , " %lldh %lldm " , ( long long ) ( up / 3600 ) , ( long long ) ( ( up % 3600 ) / 60 ) ) ;
metricRow ( TR ( " mining_uptime " ) , uptBuf , OnSurface ( ) ) ;
}
y + = gap * 0.5f ;
// --- POOLS (N) header + Refresh ---
{
char hdr [ 48 ] ;
snprintf ( hdr , sizeof ( hdr ) , " %s (%d) " , TR ( " mining_pools_header " ) ,
( 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 ) ;
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 ( ) ;
}
y + = ovFont - > LegacySize + 4 * dp ;
// --- Scrollable pool list (fixed height → constant card height for any N) ---
const float listH = std : : max ( rowH , cardMax . y - y - pad * 0.75f ) ;
ImGui : : SetCursorScreenPos ( ImVec2 ( x , y ) ) ;
ImGui : : BeginChild ( " ##PoolList " , ImVec2 ( w , listH ) , false , ImGuiWindowFlags_NoBackground ) ;
{
ImDrawList * cdl = ImGui : : GetWindowDrawList ( ) ;
const auto snap = app - > poolStatsSnapshot ( ) ;
const util : : KnownPool * current = util : : findKnownPoolByUrl ( app - > settings ( ) - > getPoolUrl ( ) ) ;
const float childW = ImGui : : GetContentRegionAvail ( ) . x ;
const float listRowH = capFont - > LegacySize + 10 * dp ;
for ( const auto & kp : util : : knownPools ( ) ) {
ImGui : : PushID ( kp . id . c_str ( ) ) ;
const bool isCurrent = current & & current - > id = = kp . id ;
const auto it = snap . byId . find ( kp . id ) ;
const bool haveHr = ( it ! = snap . byId . end ( ) & & it - > second . ok ) ;
ImVec2 rMin = ImGui : : GetCursorScreenPos ( ) ;
ImVec2 rMax ( rMin . x + childW , rMin . y + listRowH ) ;
bool hov = false , clk = false ;
if ( ! autoMode ) {
ImGui : : InvisibleButton ( " ##prow " , ImVec2 ( childW , listRowH ) ) ;
hov = ImGui : : IsItemHovered ( ) ;
clk = ImGui : : IsItemClicked ( ) ;
} else {
ImGui : : Dummy ( ImVec2 ( childW , listRowH ) ) ;
}
if ( isCurrent ) cdl - > AddRectFilled ( rMin , rMax , WithAlpha ( Primary ( ) , 30 ) , 3 * dp ) ;
else if ( hov ) cdl - > AddRectFilled ( rMin , rMax , StateHover ( ) , 3 * dp ) ;
const float cy = rMin . y + listRowH * 0.5f ;
cdl - > AddCircleFilled ( ImVec2 ( rMin . x + 6 * dp , cy ) , 3.0f * dp ,
isCurrent ? Success ( ) : OnSurfaceDisabled ( ) ) ;
const float textY = rMin . y + ( listRowH - capFont - > LegacySize ) * 0.5f ;
// Show the short host label (the narrow card can't fit host:port); the
// full stratum URL is in the hover tooltip.
cdl - > AddText ( capFont , capFont - > LegacySize , ImVec2 ( rMin . x + 16 * dp , textY ) ,
isCurrent ? Primary ( ) : OnSurface ( ) , kp . label . c_str ( ) ) ;
char right [ 64 ] ;
std : : string hrStr = haveHr ? FormatHashrate ( it - > second . hashrateHs ) : std : : string ( " — " ) ;
snprintf ( right , sizeof ( right ) , " %s %.0f%% " , hrStr . c_str ( ) , kp . feePercent ) ;
ImVec2 rSz = capFont - > CalcTextSizeA ( capFont - > LegacySize , FLT_MAX , 0 , right ) ;
cdl - > AddText ( capFont , capFont - > LegacySize ,
ImVec2 ( rMax . x - rSz . x - 6 * dp , textY ) , OnSurfaceMedium ( ) , right ) ;
if ( ! autoMode & & hov ) {
ImGui : : SetMouseCursor ( ImGuiMouseCursor_Hand ) ;
material : : Tooltip ( " %s " , kp . stratum . c_str ( ) ) ;
}
if ( clk ) {
strncpy ( s_pool_url , kp . stratum . c_str ( ) , sizeof ( s_pool_url ) - 1 ) ;
s_pool_url [ sizeof ( s_pool_url ) - 1 ] = ' \0 ' ;
s_pool_settings_dirty = true ;
}
ImGui : : PopID ( ) ;
}
}
ImGui : : EndChild ( ) ;
}
void RenderMiningStats ( const WalletState & state , const MiningInfo & mining ,
ImDrawList * dl , ImFont * capFont , ImFont * sub1 , ImFont * ovFont ,
float dp , float vs , float gap , float pad , float availWidth ,
const GlassPanelSpec & glassSpec , float chartBudgetH , bool poolMode )
const GlassPanelSpec & glassSpec , float chartBudgetH , bool poolMode ,
App * app , char ( & s_pool_url ) [ 256 ] , bool & s_pool_settings_dirty )
{
const bool s_pool_mode = poolMode ; // alias: keep the moved body's identifier verbatim
( void ) vs ;
const bool s_pool_mode = poolMode ;
ImU32 greenCol = Success ( ) ;
// Determine view mode first
bool showLogView = s_pool_mode
? ( s_show_pool_log & & ! state . pool_mining . log_lines . empty ( ) )
: ( s_show_solo_log & & ! mining . log_lines . empty ( ) ) ;
bool hasLogContent = s_pool_mode
? ! state . pool_mining . log_lines . empty ( )
: ! mining . log_lines . empty ( ) ;
// Use pool hashrate history when in pool mode, solo otherwise
const std : : vector < double > & chartHistory = s_pool_mode
? state . pool_mining . hashrate_history
: mining . hashrate_history ;
bool hasChartContent = chartHistory . size ( ) > = 2 ;
// Stat row height (single line: overline + value)
float statRowH = ovFont - > LegacySize + Layout : : spacingXs ( ) + sub1 - > LegacySize + Layout : : spacingSm ( ) ;
float totalCardH = statRowH + chartBudgetH + pad ;
bool & showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log ;
// ================================================================
// POOL MODE — split card: [ left pool card ][ chart / log ]
// ================================================================
if ( s_pool_mode ) {
// Left card height driven by its content so the pool list always fits;
// the whole row is at least as tall as the old stats+chart card.
const float toggleH = sub1 - > LegacySize + 10 * dp ;
const float mRowH = sub1 - > LegacySize + 7 * dp ;
const float listMinH = ( capFont - > LegacySize + 10 * dp ) * 2.6f ; // ~2– 3 rows before scroll
float leftContentH = pad * 0.75f + toggleH + gap
+ mRowH * 4.0f + gap * 0.5f
+ ( ovFont - > LegacySize + 4 * dp ) + listMinH + pad * 0.75f ;
float cardH = std : : max ( leftContentH , statRowH + chartBudgetH + pad ) ;
ImVec2 cardMin = ImGui : : GetCursorScreenPos ( ) ;
float leftW = std : : clamp ( availWidth * 0.25f , 180.0f * dp , availWidth * 0.45f ) ;
float colGap = gap ;
float rightW = availWidth - leftW - colGap ;
ImVec2 leftMin = cardMin ;
ImVec2 leftMax ( cardMin . x + leftW , cardMin . y + cardH ) ;
ImVec2 rightMin ( leftMax . x + colGap , cardMin . y ) ;
ImVec2 rightMax ( cardMin . x + availWidth , cardMin . y + cardH ) ;
DrawGlassPanel ( dl , leftMin , leftMax , glassSpec ) ;
DrawGlassPanel ( dl , rightMin , rightMax , glassSpec ) ;
RenderLeftPoolCard ( app , state , dl , capFont , sub1 , ovFont , dp , gap , pad ,
leftMin , leftMax , s_pool_url , s_pool_settings_dirty ) ;
// Right panel: live log (if toggled + available) else the sparkline.
if ( showLogView ) {
float logPad = pad * 0.5f ;
DrawMiningLogView ( ImVec2 ( rightMin . x + logPad , rightMin . y + logPad ) ,
rightW - logPad * 2 , cardH - logPad * 2 ,
state . pool_mining . log_lines , " ##PoolLogText " ) ;
} else if ( hasChartContent ) {
DrawHashrateSparkline ( dl ,
ImVec2 ( rightMin . x + pad , rightMin . y + statRowH * 0.5f ) ,
ImVec2 ( rightMax . x - pad , rightMax . y ) ,
chartHistory , capFont , dp ) ;
} else {
const char * hint = TR ( " mining_chart_start " ) ;
ImVec2 hs = capFont - > CalcTextSizeA ( capFont - > LegacySize , FLT_MAX , 0 , hint ) ;
dl - > AddText ( capFont , capFont - > LegacySize ,
ImVec2 ( ( rightMin . x + rightMax . x - hs . x ) * 0.5f ,
( rightMin . y + rightMax . y - hs . y ) * 0.5f ) ,
OnSurfaceDisabled ( ) , hint ) ;
}
// Chart/log toggle button, top-right of the RIGHT panel.
if ( hasLogContent | | hasChartContent ) {
ImFont * iconFont = Type ( ) . iconSmall ( ) ;
const char * toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE ;
const char * toggleTip = showLogFlag ? TR ( " mining_show_chart " ) : TR ( " mining_show_log " ) ;
ImVec2 iconSz = iconFont - > CalcTextSizeA ( iconFont - > LegacySize , 1000.0f , 0.0f , toggleIcon ) ;
float btnSize = iconSz . y + 8 * dp ;
ImVec2 btnMin ( rightMax . x - pad - btnSize , rightMin . y + pad * 0.5f ) ;
ImVec2 btnMax ( btnMin . x + btnSize , btnMin . y + btnSize ) ;
ImVec2 btnCenter ( ( btnMin . x + btnMax . x ) * 0.5f , ( btnMin . y + btnMax . y ) * 0.5f ) ;
bool hov = IsRectHovered ( btnMin , btnMax ) ;
if ( hov ) {
dl - > AddCircleFilled ( btnCenter , btnSize * 0.5f , StateHover ( ) ) ;
ImGui : : SetMouseCursor ( ImGuiMouseCursor_Hand ) ;
material : : Tooltip ( " %s " , toggleTip ) ;
}
dl - > AddText ( iconFont , iconFont - > LegacySize ,
ImVec2 ( btnCenter . x - iconSz . x * 0.5f , btnCenter . y - iconSz . y * 0.5f ) ,
OnSurfaceMedium ( ) , toggleIcon ) ;
if ( hov & & ImGui : : IsMouseClicked ( 0 ) ) showLogFlag = ! showLogFlag ;
}
ImGui : : SetCursorScreenPos ( cardMin ) ;
ImGui : : Dummy ( ImVec2 ( availWidth , cardH ) ) ;
ImGui : : Dummy ( ImVec2 ( 0 , gap ) ) ;
return ;
}
// ================================================================
// SOLO MODE — full-width card: stat columns on top, chart/log below
// ================================================================
float totalCardH = statRowH + chartBudgetH + pad ;
ImVec2 cardMin = ImGui : : GetCursorScreenPos ( ) ;
ImVec2 cardMax ( cardMin . x + availWidth , cardMin . y + totalCardH ) ;
DrawGlassPanel ( dl , cardMin , cardMax , glassSpec ) ;
bool & showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log ;
if ( showLogView ) {
// --- Full-card log view (selectable + copyable) ---
const std : : vector < std : : string > & logLines = s_pool_mode
? state . pool_mining . log_lines
: mining . log_lines ;
// Build a single string buffer for InputTextMultiline
static std : : string s_log_buf ;
s_log_buf . clear ( ) ;
for ( const auto & line : logLines ) {
if ( ! line . empty ( ) ) {
s_log_buf + = line ;
s_log_buf + = ' \n ' ;
}
}
float logPad = pad * 0.5f ;
float logW = availWidth - logPad * 2 ;
float logH = totalCardH - logPad * 2 ;
ImGui : : SetCursorScreenPos ( ImVec2 ( cardM in. x + logPad , cardMin . y + logPad ) ) ;
ImGui : : PushStyleColor ( ImGuiCol_Text , ImGui : : ColorConvertU32ToFloat4 ( OnSurface ( ) ) ) ;
ImGui : : PushStyleColor ( ImGuiCol_FrameBg , ImVec4 ( 0 , 0 , 0 , 0 ) ) ;
ImFont * monoFont = Type ( ) . body2 ( ) ;
ImGui : : PushFont ( monoFont ) ;
const char * inputId = s_pool_mode ? " ##PoolLogText " : " ##SoloLogText " ;
ImGui : : InputTextMultiline ( inputId ,
const_cast < char * > ( s_log_buf . c_str ( ) ) , s_log_buf . size ( ) + 1 ,
ImVec2 ( logW , logH ) ,
ImGuiInputTextFlags_ReadOnly ) ;
ImGui : : PopFont ( ) ;
ImGui : : PopStyleColor ( 2 ) ;
// Reset cursor to end of card after the input
DrawMiningLogView ( ImVec2 ( cardMin . x + logPad , cardMin . y + logPad ) ,
availWidth - logPad * 2 , totalCardH - logPad * 2 ,
min ing . log_lines , " ##SoloLogText " ) ;
ImGui : : SetCursorScreenPos ( ImVec2 ( cardMin . x , cardMax . y ) ) ;
ImGui : : Dummy ( ImVec2 ( availWidth , 0 ) ) ; // Register position with layout system
ImGui : : Dummy ( ImVec2 ( availWidth , 0 ) ) ;
} else {
// --- Stats + Chart view ---
// Pool vs Solo stats — different columns
std : : string col1Str , col2Str , col3Str , col4Str ;
const char * col1Label ;
const char * col2Label ;
const char * col3Label ;
const char * col4 Label = nullptr ;
ImU32 col1Col , col2Col , col3Col , col4Col = OnSurface ( ) ;
double est_hours = EstimateHoursToBlock ( mining . localHashrate , mining . networkHashrate , mining . difficulty ) ;
const char * col1Label = TR ( " mining_local_hashrate " ) ;
std : : string col1Str = FormatHashrate ( mining . localHashrate ) ;
ImU32 col1Col = mining . generate ? greenCol : OnSurfaceDisabled ( ) ;
const char * col2Label = TR ( " mining_network " ) ;
std : : string col2Str = FormatHashrate ( mining . networkHashrate ) ;
const char * col3 Label = TR ( " mining_est_block " ) ;
std : : string col3Str = FormatEstTime ( est_hours ) ;
int numStats = 3 ;
if ( s_pool_mode ) {
col1Label = TR ( " mining_local_hashrate " ) ;
col1Str = FormatHashrate ( state . pool_mining . hashrate_10s ) ;
col1Col = state . pool_mining . xmrig_running ? greenCol : OnSurfaceDisabled ( ) ;
float statColW = ( availWidth - pad * 2 ) / ( float ) numStats ;
float sy = cardMin . y + pad * 0.5f ;
struct StatEntry { const char * label ; std : : string value ; ImU32 col ; } ;
StatEntry stats [ ] = {
{ col1Label , col1Str , col1Col } ,
{ col2Label , col2Str , OnSurface ( ) } ,
{ col3Label , col3Str , OnSurface ( ) } ,
} ;
for ( int si = 0 ; si < numStats ; si + + ) {
float sx = cardMin . x + pad + si * statColW ;
float centerX = sx + statColW * 0.5f ;
ImVec2 lblSz = ovFont - > CalcTextSizeA ( ovFont - > LegacySize , 10000 , 0 , stats [ si ] . label ) ;
dl - > AddText ( ovFont , ovFont - > LegacySize ,
ImVec2 ( centerX - lblSz . x * 0.5f , sy ) , OnSurfaceMedium ( ) , stats [ si ] . label ) ;
ImVec2 valSz = sub1 - > CalcTextSizeA ( sub1 - > LegacySize , 10000 , 0 , stats [ si ] . value . c_str ( ) ) ;
float valY = sy + ovFont - > LegacySize + Layout : : spacingXs ( ) ;
dl - > AddText ( sub1 , sub1 - > LegacySize ,
ImVec2 ( centerX - valSz . x * 0.5f , valY ) , stats [ si ] . col , stats [ si ] . value . c_str ( ) ) ;
col2Label = TR ( " mining_pool_hashrate " ) ;
col2Str = FormatHashrate ( state . pool_mining . pool_hashrate ) ;
col2Col = state . pool_mining . pool_hashrate > 0 ? OnSurface ( ) : OnSurfaceDisabled ( ) ;
col3Label = TR ( " mining_shares " ) ;
char sharesBuf [ 64 ] ;
snprint f( sharesBuf , sizeof ( sharesBuf ) , " %lld / %lld " ,
( long long ) state . pool_mining . accepted ,
( long long ) state . pool_mining . rejected ) ;
col3Str = sharesBuf ;
col3Col = OnSurface ( ) ;
col4Label = TR ( " mining_uptime " ) ;
int64_t up = state . pool_mining . uptime_sec ;
char uptBuf [ 64 ] ;
if ( up < = 0 )
snprintf ( uptBuf , sizeof ( uptBuf ) , " N/A " ) ;
else if ( up < 3600 )
snprintf ( uptBuf , sizeof ( uptBuf ) , " %lldm %llds " , ( long long ) ( up / 60 ) , ( long long ) ( up % 60 ) ) ;
else
snprintf ( uptBuf , sizeof ( uptBuf ) , " %lldh %lldm " , ( long long ) ( up / 3600 ) , ( long long ) ( ( up % 3600 ) / 60 ) ) ;
col4Str = uptBuf ;
col4Col = OnSurface ( ) ;
numStats = 4 ;
} else {
double est_hours = EstimateHoursToBlock ( mining . localHashrate , mining . networkHashrate , mining . difficulty ) ;
col1Label = TR ( " mining_local_hashrate " ) ;
col1Str = FormatHashrate ( mining . localHashrate ) ;
col1Col = mining . generate ? greenCol : OnSurfaceDisabled ( ) ;
col2Label = TR ( " mining_network " ) ;
col2Str = FormatHashrate ( mining . networkHashrate ) ;
col2Col = OnSurface ( ) ;
col3Label = TR ( " mining_est_block " ) ;
col3Str = FormatEstTime ( est_hours ) ;
col3Col = OnSurface ( ) ;
}
// Draw stat values as inline columns at top of card
{
float statColW = ( availWidth - pad * 2 ) / ( float ) numStats ;
float sy = cardMin . y + pad * 0.5f ;
struct StatEntry { const char * label ; const char * value ; ImU32 col ; } ;
char c1Buf [ 64 ] , c2Buf [ 64 ] , c3Buf [ 64 ] , c4Buf [ 64 ] ;
snprintf ( c1Buf , sizeof ( c1Buf ) , " %s " , col1Str . c_str ( ) ) ;
snprintf ( c2Buf , sizeof ( c2Buf ) , " %s " , col2Str . c_str ( ) ) ;
snprintf ( c3Buf , sizeof ( c3Buf ) , " %s " , col3Str . c_str ( ) ) ;
if ( numStats > 3 ) snprintf ( c4Buf , sizeof ( c4Buf ) , " %s " , col4Str . c_str ( ) ) ;
StatEntry stats [ ] = {
{ col1Label , c1Buf , col1Col } ,
{ col2Label , c2Buf , col2Col } ,
{ col3Label , c3Buf , col3Col } ,
{ col4Label ? col4Label : " " , c4Buf , col4Col } ,
} ;
for ( int si = 0 ; si < numStats ; si + + ) {
float sx = cardMin . x + pad + si * statColW ;
float centerX = sx + statColW * 0.5f ;
ImVec2 lblSz = ovFont - > CalcTextSizeA ( ovFont - > LegacySize , 10000 , 0 , stats [ si ] . label ) ;
dl - > AddText ( ovFont , ovFont - > LegacySize ,
ImVec2 ( centerX - lblSz . x * 0.5f , sy ) , OnSurfaceMedium ( ) , stats [ si ] . label ) ;
ImVec2 valSz = sub1 - > CalcTextSizeA ( sub1 - > LegacySize , 10000 , 0 , stats [ si ] . value ) ;
float valY = sy + ovFont - > LegacySize + Layout : : spacingXs ( ) ;
dl - > AddText ( sub1 , sub1 - > LegacySize ,
ImVec2 ( centerX - valSz . x * 0.5f , valY ) , stats [ si ] . col , stats [ si ] . value ) ;
// Trend arrow for hashrate (first column only)
if ( si = = 0 & & chartHistory . size ( ) > = 6 ) {
size_t hn = chartHistory . size ( ) ;
double recent = ( chartHistory [ hn - 1 ] + chartHistory [ hn - 2 ] + chartHistory [ hn - 3 ] ) / 3.0 ;
double older = ( chartHistory [ hn - 4 ] + chartHistory [ hn - 5 ] + chartHistory [ hn - 6 ] ) / 3.0 ;
ImFont * iconFont = Type ( ) . iconSmall ( ) ;
float arrowX = centerX + valSz . x * 0.5f + Layout : : spacingSm ( ) ;
if ( recent > older * 1.02 ) {
const char * icon = ICON_MD_TRENDING_UP ;
ImVec2 iSz = iconFont - > CalcTextSizeA ( iconFont - > LegacySize , 1000.0f , 0.0f , icon ) ;
dl - > AddText ( iconFont , iconFont - > LegacySize ,
ImVec2 ( arrowX , valY + sub1 - > LegacySize * 0.5f - iSz . y * 0.5f ) ,
WithAlpha ( Success ( ) , 220 ) , icon ) ;
} else if ( recent < older * 0.98 ) {
const char * icon = ICON_MD_TRENDING_DOWN ;
ImVec2 iSz = iconFont - > CalcTextSizeA ( iconFont - > LegacySize , 1000.0f , 0.0f , icon ) ;
dl - > AddText ( iconFont , iconFont - > LegacySize ,
ImVec2 ( arrowX , valY + sub1 - > LegacySize * 0.5f - iSz . y * 0.5f ) ,
WithAlpha ( Error ( ) , 220 ) , icon ) ;
}
if ( si = = 0 & & chartHistory . size ( ) > = 6 ) {
size_t hn = chartHistory . size ( ) ;
double recent = ( chartHistory [ hn - 1 ] + chartHistory [ hn - 2 ] + chartHistory [ hn - 3 ] ) / 3.0 ;
double older = ( chartHistory [ hn - 4 ] + chartHistory [ hn - 5 ] + chartHistory [ hn - 6 ] ) / 3.0 ;
ImFont * iconFont = Type ( ) . iconSmall ( ) ;
float arrowX = centerX + valSz . x * 0.5f + Layout : : spacingSm ( ) ;
i f ( recent > older * 1.02 ) {
const char * icon = ICON_MD_TRENDING_UP ;
ImVec2 iSz = iconFont - > CalcTextSizeA ( iconFont - > LegacySize , 1000.0f , 0.0f , icon ) ;
dl - > AddText ( iconFont , iconFont - > LegacySize ,
ImVec2 ( arrowX , valY + sub1 - > LegacySize * 0.5f - iSz . y * 0.5f ) ,
WithAlpha ( Success ( ) , 220 ) , icon ) ;
} else if ( recent < older * 0.98 ) {
const char * icon = ICON_MD_TRENDING_DOWN ;
ImVec2 iSz = iconFont - > CalcTextSizeA ( iconFont - > LegacySize , 1000.0f , 0.0f , icon ) ;
dl - > AddText ( iconFont , iconFont - > LegacySize ,
ImVec2 ( arrowX , valY + sub1 - > LegacySize * 0.5f - iSz . y * 0.5f ) ,
WithAlpha ( Error ( ) , 220 ) , icon ) ;
}
}
}
// Sparkline chart below stats
if ( hasChartContent ) {
float chartTop = cardM in. y + statRowH ;
float chartBot = cardMax . y ;
// Compute Y range
double yMin = * std : : min_element ( chartHistory . begin ( ) , chartHistory . end ( ) ) ;
double yMax = * std : : max_element ( chartHistory . begin ( ) , chartHistory . end ( ) ) ;
if ( yMax < = yMin ) { yMax = yMin + 1.0 ; }
double yRange = yMax - yMin ;
double yPad2 = yRange * 0.1 ;
yMin - = yPad2 ;
yMax + = yPad2 ;
float plotLeft = cardMin . x + pad ;
float plotRight = cardMax . x - pad ;
float plotTop = chartTop + capFont - > LegacySize + 4 * dp ;
float plotBottom = chartBot - capFont - > LegacySize * 2 - 16 * dp ;
float plotW = plotRight - plotLeft ;
float plotH = std : : max ( 1.0f , plotBottom - plotTop ) ;
// Build raw data points — evenly spaced across the plot.
// No smooth-scroll animation: the chart updates in-place
// when new data arrives without any interim compression.
size_t n = chartHistory . size ( ) ;
float stepW = ( n > 1 ) ? plotW / ( float ) ( n - 1 ) : plotW ;
std : : vector < ImVec2 > rawPts ( n ) ;
for ( size_t i = 0 ; i < n ; i + + ) {
float x = plotLeft + ( float ) i * stepW ;
float y = plotBottom - ( float ) ( ( chartHistory [ i ] - yMin ) / ( yMax - yMin ) ) * plotH ;
rawPts [ i ] = ImVec2 ( x , y ) ;
}
// Catmull-Rom spline interpolation for smooth curve
// Subdivisions are adaptive: more when points are far apart,
// none when points are already sub-2px apart.
std : : vector < ImVec2 > points ;
if ( n < = 2 ) {
points = rawPts ;
} else {
points . reserve ( n * 4 ) ; // conservative estimate
for ( size_t i = 0 ; i + 1 < n ; i + + ) {
ImVec2 p0 = rawPts [ i > 0 ? i - 1 : 0 ] ;
ImVec2 p1 = rawPts [ i ] ;
ImVec2 p2 = rawPts [ i + 1 ] ;
ImVec2 p3 = rawPts [ i + 2 < n ? i + 2 : n - 1 ] ;
// Adaptive subdivision: ~1 segment per 3px of distance
float dx = p2 . x - p1 . x , dy = p2 . y - p1 . y ;
float dist = sqrtf ( dx * dx + dy * dy ) ;
int subdivs = std : : clamp ( ( int ) ( dist / 3.0f ) , 1 , 16 ) ;
for ( int s = 0 ; s < subdivs ; s + + ) {
float t = ( float ) s / ( float ) subdivs ;
float t2 = t * t ;
float t3 = t2 * t ;
float q0 = - t3 + 2.0f * t2 - t ;
float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f ;
float q2 = - 3.0f * t3 + 4.0f * t2 + t ;
float q3 = t3 - t2 ;
float sx = 0.5f * ( p0 . x * q0 + p1 . x * q1 + p2 . x * q2 + p3 . x * q3 ) ;
float sy = 0.5f * ( p0 . y * q0 + p1 . y * q1 + p2 . y * q2 + p3 . y * q3 ) ;
// Clamp Y to plot bounds to prevent Catmull-Rom overshoot
sy = std : : clamp ( sy , plotTop , plotBottom ) ;
points . push_back ( ImVec2 ( sx , sy ) ) ;
}
}
points . push_back ( rawPts [ n - 1 ] ) ; // final point
}
// Fill under curve (single concave polygon to avoid AA seam shimmer)
if ( points . size ( ) > = 2 ) {
for ( size_t i = 0 ; i < points . size ( ) ; i + + )
dl - > PathLineTo ( points [ i ] ) ;
dl - > PathLineTo ( ImVec2 ( points . back ( ) . x , plotBottom ) ) ;
dl - > PathLineTo ( ImVec2 ( points . front ( ) . x , plotBottom ) ) ;
dl - > PathFillConcave ( WithAlpha ( Success ( ) , 25 ) ) ;
}
// Green line
dl - > AddPolyline ( points . data ( ) , ( int ) points . size ( ) ,
WithAlpha ( Success ( ) , 200 ) , ImDrawFlags_None , schema : : UI ( ) . drawElement ( " tabs.mining " , " chart-line-thickness " ) . size ) ;
// Y-axis labels
std : : string yMaxStr = FormatHashrate ( yMax ) ;
std : : string yMinStr = FormatHashrate ( yMin ) ;
dl - > AddText ( capFont , capFont - > LegacySize , ImVec2 ( plotLeft + 2 * dp , plotTop - capFont - > LegacySize - 2 * dp ) , OnSurfaceDisabled ( ) , yMaxStr . c_str ( ) ) ;
dl - > AddText ( capFont , capFont - > LegacySize , ImVec2 ( plotLeft + 2 * dp , plotBottom + 4 * dp ) , OnSurfaceDisabled ( ) , yMinStr . c_str ( ) ) ;
// X-axis labels
dl - > AddText ( capFont , capFont - > LegacySize ,
ImVec2 ( plotLeft , chartBot - capFont - > LegacySize - 2 * dp ) ,
OnSurfaceDisabled ( ) ,
chartHistory . size ( ) > = 300 ? TR ( " mining_chart_5m_ago " ) :
chartHistory . size ( ) > = 60 ? TR ( " mining_chart_1m_ago " ) : TR ( " mining_chart_start " ) ) ;
std : : string nowLbl = TR ( " mining_chart_now " ) ;
ImVec2 nowSz = capFont - > CalcTextSizeA ( capFont - > LegacySize , 10000 , 0 , nowLbl . c_str ( ) ) ;
dl - > AddText ( capFont , capFont - > LegacySize ,
ImVec2 ( plotRight - nowSz . x , chartBot - capFont - > LegacySize - 2 * dp ) ,
OnSurfaceDisabled ( ) , nowLbl . c_str ( ) ) ;
DrawHashrateSparkl ine ( dl ,
ImVec2 ( cardMin . x + pad , cardMin . y + statRowH ) ,
ImVec2 ( cardMax . x - pad , cardMax . y ) ,
chartHistory , capFont , dp ) ;
}
// Advance cursor past the card (stats/chart view only)
ImGui : : Dummy ( ImVec2 ( availWidth , totalCardH ) ) ;
}
// --- Toggle button in top-right corner ---
// Rendered after content so the Hand cursor takes priority over
// the InputTextMultiline text-cursor when hovering the button.
if ( hasLogContent | | hasChartContent ) {
ImFont * iconFont = Type ( ) . iconSmall ( ) ;
const char * toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE ;
@@ -331,7 +533,6 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
ImVec2 btnMin ( btnX , btnY ) ;
ImVec2 btnMax ( btnX + btnSize , btnY + btnSize ) ;
ImVec2 btnCenter ( ( btnMin . x + btnMax . x ) * 0.5f , ( btnMin . y + btnMax . y ) * 0.5f ) ;
bool hov = IsRectHovered ( btnMin , btnMax ) ;
if ( hov ) {
dl - > AddCircleFilled ( btnCenter , btnSize * 0.5f , StateHover ( ) ) ;