From 973bc0d33821fde9ce0418fe8d3276347af0af21 Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 18 Jul 2026 13:39:42 -0500 Subject: [PATCH] feat(market): trend sparkline in all portfolio styles + inline chart/style pickers Render the portfolio trend sparkline in every row style, each with a layout that features it: - Table: a dedicated header-labelled TREND column (48dp rows), aligned across rows. - Cards: a left-aligned info stack (icon + label / value+delta chip / DRGX) with a full-height sparkline filling the rest of the width (content-driven, so short groups give the chart more room). - Spotlight: a big-value-over-a-full-width-chart-band hero tile (92dp). Default sparkline interval is now MONTH (a real curve from the daily series, not the young minute buffer). Removed the per-row container accent line (redundant with the icon/sparkline colour) and the per-group shielded/transparent bar (private-by-default makes it near-always 100%, and dropped its now-dead SumPortfolioSplit helper). DPI-scale the sparkline stroke. Adds the "Trend" column string across 9 languages. Relocated the Market controls out of a removed settings modal + gear: the chart line/candle picker now sits top-right of the chart (left of the trade button, candle-capable ranges only), and the Table/Cards/Spotlight picker sits left of Manage. Dropped the market fetch-prices toggle (still in the general Settings page). Co-Authored-By: Claude Opus 4.8 (1M context) --- res/fonts/NotoSansCJK-Subset.ttf | Bin 653640 -> 653920 bytes res/lang/de.json | 1 + res/lang/es.json | 1 + res/lang/fr.json | 1 + res/lang/ja.json | 1 + res/lang/ko.json | 1 + res/lang/pt.json | 1 + res/lang/ru.json | 1 + res/lang/zh.json | 1 + src/config/settings.h | 3 +- src/data/portfolio.h | 13 -- src/ui/windows/market_tab.cpp | 344 ++++++++++++------------------- src/util/i18n.cpp | 1 + 13 files changed, 142 insertions(+), 227 deletions(-) diff --git a/res/fonts/NotoSansCJK-Subset.ttf b/res/fonts/NotoSansCJK-Subset.ttf index 018fb864fb6c9380566832c12eebd4c30ebf115b..27b81cf324c24328856580f46ec17d2f5b20961c 100644 GIT binary patch delta 12492 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 delta 12388 zcmbuFcU%-l`2U~1yPesYvv;w#U@su3pu#~Em8v2NXzU%OsDLeJkI`uCXK#rmY7`|( z#ID#CyJA=DioL|n_p^6^$?y05@B8BQ=5}YFdHU?l?cJ?6&PQH3&nRL2wLTHu@CylU zAKEf)35gknh$;v8weR5L_M}Qa5?eYGmAMk!zMe~utGzl9DcvC7t>>WbG54Z0Pomlz zh@6k~jT+wP!YxBbqOq8hFQ8xV?!9W9j9U)b&%nJO2J#gy_5kx6;=F3VK|>QSyuV+H ztfcHEs`)r7x@ULSM~_btg${+%`GdMA#+a*xKvwC5e{68~LA@(f@u)qSq%G0{VY zW=|p)62HP>su-^u*jy=^M0#VKSB-~($-jyhI7a|AACrPngTji&+;85sVAHpx7$0JU z@?A@s?xr8yAG7S2*%g&W95zG9TrK$rWvo;3oiZaz%r4l}DLKJqfBwSNZr;8Tf4TqZ ze5cK)#c9*!+sSRb3o44!>F40wS zH z*y6C(Vf8!g?cmnIr9-_ARXQl44?^#TW`^NVkxPkd7e%A^stC zL-K`);Mc*IgUbs#o$6gTY}aH%@3LzG%ILoQ176gLEVDFgTjJB zf`Wnqg93t@1X}M0-U+-FcrtKzU~*tm;PSv_f!4s$fzbi~1iT3t6yP3EFQ7_5<$&@5 zeVY_q@3mNwpPnzWH^ zjIBSke(AT`?{~iyehd8O`OWYf?bplC+poEwr(Z+A`hGS2%q`zoTV8H?spa{WM_RUP z8Pc*vi(@SgwAj&NZj0G1X82C@9p`KHP4sp5b@g@eZQxte*V(tSZzbO%zM9WlpL0H^ zebRgm`0Vsq;j_$VvCkZznLbl}VtfYq4Djji)6*y1C(Ossr>ajSA8C1Kd0}~K$+XRmR^=_mI#ZNrJ2RU;%0HR)V9>LI9n=O%34Zz zr+M%9-sAnZ_g3#s-gCXjd3W{h;vMMi=au1g!RxHoA+HdxHeSuW%6d6@mGmm)#hN=e z&1ky2>87UZo33u!zG+BPpQe_ko}LdqFMD3_w0U;(jPMNdEab7mW1+`vtH)%IK^_A< ze()&kQQX7O% zXV>?xFI{iAUUkiIJ>#10dc<|B>q6H=SLgn&&0Sqw>$&E4`QY-}<)zCrmrR$vE~{Lo zxQuiu+u*3ViA*GtiOIKGpApKd*<0GrZSW^cVh7kEc9h*Xb-{U8yxDa_v^~la>j@`&uO!wNj`h1qY`~wyq9zd=gy^CX1a8tS7P{m15F2uc z*wE9&hFvF?P>NV0^jU{{5*slEd?Gdy9*t^8Y;*q$@OCMVmwOOffoN8aCH5Pf{QWYq)tI~nC0M(a*t*8V z)+0NAPA0Yq{gV_(Z1V_W$?b`4MMkzgCzgW1wmT6^Jwj~9Nn$&965F+z*zR@2_BMuB z`yLWI03&Iqh#f+7hrc6sq#3cJ^N1ZonvcWN6Yq(gJWebfPM(@d>~w2lXMQDiwmQK1 zxst#}?0hW%xr?iaW$Yk!8S}67B6f8ov1`zG9ZuY=LhROLVwomlcdQV+>j_>HySI&sji`*by6v~VKLA)4bij4(#i5EvO z#rF{}(E!{cUNRBvCteCNrP7I)t_nJWzlb||f;bp(#T8&~)$c$LaG7{D4-f^Ch<|5g z_!kLQ5O;yuSj#wYiMU@l zSWCPW1FgXV03EGy+#2I;N`avulej+|^1q1s2RH$wIRKsp)(40_@C@;wCSV!3N<6qJ z7zwTu4?!A3{wChG1i<{ZPl&hc2auWeBLJE+Gz7dL-T}SQ;UV!b%n92|ykm3l7x7Mk z0I%WJ>iA~^@G>GAKuP34;+?UyIu8L4h;KUCNK{D~K=2P5O~Iv(X2kBBDJ z1(2T@296M)X9oAW2}xi!El;`3l|-ZkPje{hiae7w$oNqj*l zKt>k^fTzS4L2l6w;)|<;HN=;|p(RU*FSUB$-!0@-mP5^hYmHhye|KA zXJIfMphO!{0_(EFfyn@lBqO?HG|peez*^#4B|vtz z;%2ff0h}hDf?i7bjriXs0GjRZi^R8g0?WV^;;GKyCxG!Ctx^9SNXyPv;1A-vN&*<$ zjXLd~L3|IIW)JGU2kGA%4vrGvhaT8B5uiu*Bg*~zi61~Y4vYt%h^LJqeh_*NA{&R0 zxkI;!AGQ%cQjPdg0ge#I+d4mv$`sVT%y!}#gP#LuiJ zeilxhs{(L*{yPAtFCfwj=;({5h-dU6ez_y@D{$y4GItGWzYh5u<%!=M4Az13#BZU* zw}yiY#BamN+t#1)?*{Qq)G-sO&4l92bHwk!yE~}m9h~2741NIc`fdt%Nc>(^&;!7+ zdr2T2WD&oQa@|L>-G>wRX9BE}2P#0VAD|H*BCtn+08Twxh5A22T_3}%$6Wx@^cZP- ztdD&p{sd!BFyToi@vN?34e_Ui!F+I%__KKMmH2b?#dBPHj;Nl0BL2b|K;~sE_?!5v zvH*SZ>MsCYugxG1EJyucA0__A0FdrCNa369Ae;DGME4d--_--7i2q{()je3l!#>;-R#f2{-1 zwO=!cXM2L*NuxrbEjU6Nc1fDj1UZg(pX_|j5N(iny`>2 zqewGjJl_z~@~r6Wmm+D%$nZvdytjV7&p3DPRm1g}Y}c$KtD$4RR^m9#3*SLGUM)iCEf7<67t zTJ;$4jI^2>Y1Ue^NUMF0v^puI)xAzyy_uxd-$Ggg7cqkk+anX|3Cn)@B50{(q1bu#&XE zxugZ*ICuqFwUG0qwL_}feIzXuQFK6h!=SL!P}0H=kQQ;Dw9W-d>k?1e517~uk@SFk zPh9U6L0az_r1kw7Kylw4r1h%;rT~<#AF|XBf%Hda`@`Y>sQ~8#5XgY(;4=mX_6CyiVi?L6 zkD4bS6Nz0(8xC6|hLAQA^G6}jQ71_oZQV%PkEcRxSp=1Km zH!+;FNhsA%P&^sReulGChLbk6I%&TkJHJGO3#3g$R;IlrZTfoBesuxQNSl#K+RV+Q z&1R&{sSD7sbDxlAYXh6}lSx|u2~cS zZ8sv{y@|9vodC}F)&e_8+t-n_{YdeF0ssx1hEg7cjzbtbw1~9B$l~EWq#fyECG98y z2po$b?RZhJinJ3*=gIuwHfiZW;0Y6zcy1QM^RTU(O`$6+C^7TD>U*&Xe{Q^?ln390WH>dxx68Lu%iR z2B`BtpGkZF7ik}|Nc&Wqw9mNq1(AOZAT4_&393axDGP3sF!U#3G=rNYOhA1^f^DQS zf4kcl>e?u_7*w~+nja7gF7=bCJ*jTmb-Je|hFu;M9;@U?XtEFhR zuQNrxT4O}AI43QnkR&ymea(j>vtkm%#Ry>)v&3w1UN)9Zq^E3Z>oiL^7ac1mh)H6r zxF9|BXJgV!`lQU7CF&W}3E7LqB-^1mqHCE6*=b@@b~>q)KRb&GfZ5sU!ai#Ao-1lV zHWRXg=L)w$4%rMm>}m_S*z8P-15>Cv&CAZh9ONHbG96dpWgM77<#Vp4!%ildAdw0? zx(zr1%gk=M7c}+;gR@g{1+w_!KonOd*uI-5#=?mVjBJ=E>flhncw?TZQpbLXIY#;! zn&%^u2{_aBWx_%xMzeB8^(l32qGU<#;Oro~a27m^!sJ0Dab0)2{sK|FhMrp!GM<5qXV6f%tdFR9BSUgWZG#tx3Z-)` z={67@F6u|Rw$%$n9WTdVCK>d4>lgCU@h)cqHZu0ApiVRfSsV*%|H%+wNU&LeUv`|=3wuKADB4kDHGmk~0e@?Bilf#rWOi81^F>O0|l&e>FQ@1K6 z*iJ7JrO}$`yc{czf?l!*u3IrJ7OiVd$X;zvrNAt`r;_q?9!3f;*>&sj=@Sw$A;+IX zi|qx&=)5JO76$d}^_Pg!IK*PG1vw7&wvSq3?>PN%=@Q{<%`KT8xFd>7cJHAfr;~FX z&7{Y9aXGS?4i7VFFBPMG0P}Js@^X@HAAJPBv(Q7i;p_eEn444|m%bU)8$Ul9w18dM zv4oTCMxa73E4-euR1`;8dW)}HD$0dBO8@UgvH_{flSc`3o%S1n?tr~992G`7G2)o1 zyQ4dso>ROesIRz8bWtau4JO&fEfWoJv&ceHvg|$LNXmx1g#6cjCOymb7xD($kMtY> zdXgQ1J7PvWc8_i#^*6yS@Cp(N8m8iM`>jf^WYBWaHfNpdtI1I)`+~v6ZOes4Umm!3 z>h9~8^_1y%4n19kR)_{jmwwoCg*c~p+niPk=PKUP8+p;cLp4%9^1yRN1-ClQehV%v zB1LD>#kOvxaB685xFYfQ(SD69rrcYyS>ncTzoxvgU&}9gh`yq~7$^pbXb~%hh+!f@ z`pQ{6$Wq`tnyfgIwe+ zIg1*~IdTrUNt;|q?sBnQK^}6w+(6!Pqufb8a*xcQw(^?1Mg!$dd7Gl-6N%L;pUD># zD__ZXG*rHqA85GzWR+iOq**nq_?FrvB2l8wNKBXPRI|`ChmmxQq+ui}ZzKaFsTf&} zktDq5MN)N)WMU*s#=*rQR1EKd#pEzBi;5v##q65WAhj4$KVsfu`$&?o{(DHvaUcUi zX?9h)YKH4-;1G=2UHso9q#R|JLU&_sb+{mP7xK>R4%yA7!jUA{K#*1}3>8XqV@kzT z-IQZg&xSpAi6*y?nlPdpPnX9L-3h&cF*_+YGMQnQTb&c#x8NKjx;{Nm>Gojs>{{Pp zC<{Xw7|O6u%N@zY2-YTq^bx%%-=^tB(MR^=IOQmlUKG7dsrFfVx^v6)-x0mIsbthG zTQNm9ge43^nK^3pER@HnqXYC&U6Es&?vS3q)fkxtnOSyYdj9lG={=EVAJKE7CwVbO zlI$b8)Bhbw)$5fDiZtO;aB{(#Kr?&%8jJO!QEV`eLu3U@}y-&Y+u4h1ZO3%PIH}zQb48YAC$I|Ud z)hn#$)N!U4SMQx~8P;9Pkn`;sw&GrxquddVUTfV{n%$IMBVF8~PQ{tM4-iFGP84`Y z$SKHV$Ssyn?49IDxbAkYGmbpy8MKEc4?sg&cBU0(9KyLP-_c~c;deQPu?2t$U75pp zM9yN=J3((U`)Hn?e~+Z$o#NZ-m@GFVZWxtEVf+}5Pi9*)uw?Y!PbD)%bNyQl(M*|) zW#$mi$PrJ^k#-bHm;P3|+zzq#JmmC1z7_PFobI+>VtXs<@#{j4MVPL8hLLZTb7Uh| zwIjtF?G7)_Ysp#2(`vcVUZlnN;>e-izCI-OK1j75wI1aEf7O~UQ~z~YzXRsB-z=!q zuj(%xoOaA$6ZV9{gu}3&RJ|Oza*hmsJ5!JWyiw;!*;jx)&sGI*A2RPk?)~c94MR8M zc%#XkoGY{1UVyy2rTk)#Hq&um!z?{^$Ne*}b?qkg*49^r{f=wJqpg44&rqv(x~K9m z(tgCg-W{&w-p%xAzd5JJr{5DCOIMfq=B{o=zpl^7uv^K?4+@)w%w@?d+14z1HK*nD z`1D1n-!OFFzojcTV#k|O-VIA``&WH)-+~-BEj_5b)h~y_)G&a(8A_e0x2k=gBgx6u zW`n4Z-@dsKJ>~b(-z-w%HwdfI+C%g}D*KAzD9%VRl1hrv;zuea#))y{BqocQ_~>gB zHmW8Th_&>cSSPlSyVx#vk(byj_R;s^fH*)c#X)=~_7lg%X=)?RinG|5;PWfB(?7vd zD7Her6sB*7sG|&);S?de$gb2|_O!~L)KB)6(bQkY%6N*yUT8Qa%29GO_AO)N7#b@+|w#nP_ zE~Uta@)7O8*6IoEl27Gx+AUwom$Xm5#s+IYHdtTikjyp{oiH2B21>^+i_@EAq7?~gw z}RE2V(;tg{^!~bZTqaM*7PDbnf@~3ryLPB8DoZ zuu%GmNV71TMS@5aeMEnAJ~6)c$~A9u89ed4|+W4BOdm6(Wkti z@Cynh5od~+^{+Aa?1p(~dXY@IPE^LQ{!mGOpk&JvffD3PucQS}ks?yXDTDQAHkD0h zv)CNAlC5HYunlYz+roCRJ#0Ta$d0fR>=Zl4F0#w)8oSNzvis~Yd&*w2x9mOp%(A(e z=jR1^8D5>&=Jj|3?#7#NPu`sS@D{ukZ^JwAa30Bf@L_y7?`0UMzEJiVnix%{8Kxz= zeSbT{@bv7_FsoRXRmGDc8P|SMSBq@o{LxFqxgc=l|S!E7@{D9 zv5%5u>y|2fe8da!QoI%yl|#yLC0#kKoK-H0H_A2ThH^)|RkD;9;+^uzpo)JC1r0?E z#l(9YOT`woLzH!q24hcSZ(~1Wlrh>k%$Q&tfxjQ6$vECPNvg(a#+k-B#(Bo2 z##P3(#?8iL$!z`h3tul5mK1CnR-fT>TilM4K>A^hMPv2 ztfq;ksix_snWi~1pY7IuETB+Z=bfUAt{=j$M~E z*j-su)?&}tD_L9Ck#$+N=pgHTYcpB@KW$b8eI_E%XB~OCY~biK^@Y8|?EPhLuRdww zo};}=|EIn5A0Vv@JM??puz`Ua(+E|v&v1O$TFWc7RqHq!3+YqI{7pgCFmt)HLqhf_(oy(T9 z-`Hxlj_E&)OlCXT-khIDo@A%ld3K3i$@`%smO_rN25!PnYTWUYNniYA(w_(N5Z;c5 z@dQ4?zD)Fo#m{pW2pN=Ax}tlocqlEEV5PS*P>E4ul|;o_NSUPksw`BNDJzvfm2KjM z&2&QaRE)NMco9|A@@i$Zx-4Kje?mlB%d%;>L9AfEvo&lz+sHPvt!y{j$I{qgc8sO7 zv+M%PU{~2Kc85J;S?mRS!~S8P*jH}kDzD2M;g;aX19&iRi$W#xk-0fBEzQjeZcA72 zd#?gY6QzX`r1Vk-DA7uslAw%HCZ=pWi8qDrzp(#n&T=#6$XA&C(fadm)wZ=cC8{YQ RDKV!+1*eqRkHpDB{{viQ?vVfh diff --git a/res/lang/de.json b/res/lang/de.json index a8fc62c..f733fde 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -731,6 +731,7 @@ "market_cap_short": "Kap.", "market_chart_loading": "Preisverlauf wird geladen", "market_col_name": "Name", + "market_col_trend": "Trend", "market_col_value": "Wert", "market_iv_1d": "1T", "market_iv_1h": "1S", diff --git a/res/lang/es.json b/res/lang/es.json index 125b81f..46ab427 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -731,6 +731,7 @@ "market_cap_short": "Cap.", "market_chart_loading": "Cargando historial de precios", "market_col_name": "Nombre", + "market_col_trend": "Tendencia", "market_col_value": "Valor", "market_iv_1d": "1D", "market_iv_1h": "1H", diff --git a/res/lang/fr.json b/res/lang/fr.json index da1ef1c..404acc1 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -731,6 +731,7 @@ "market_cap_short": "Cap.", "market_chart_loading": "Chargement de l'historique des prix", "market_col_name": "Nom", + "market_col_trend": "Tendance", "market_col_value": "Valeur", "market_iv_1d": "1J", "market_iv_1h": "1H", diff --git a/res/lang/ja.json b/res/lang/ja.json index 4511182..9e21727 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -731,6 +731,7 @@ "market_cap_short": "時価総額", "market_chart_loading": "価格履歴を読み込み中", "market_col_name": "名前", + "market_col_trend": "トレンド", "market_col_value": "価値", "market_iv_1d": "1日", "market_iv_1h": "1時間", diff --git a/res/lang/ko.json b/res/lang/ko.json index f2255d9..ed28413 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -731,6 +731,7 @@ "market_cap_short": "시총", "market_chart_loading": "가격 기록 불러오는 중", "market_col_name": "이름", + "market_col_trend": "추세", "market_col_value": "가치", "market_iv_1d": "1일", "market_iv_1h": "1시간", diff --git a/res/lang/pt.json b/res/lang/pt.json index 0807177..944b7d1 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -731,6 +731,7 @@ "market_cap_short": "Cap.", "market_chart_loading": "Carregando histórico de preços", "market_col_name": "Nome", + "market_col_trend": "Tendência", "market_col_value": "Valor", "market_iv_1d": "1D", "market_iv_1h": "1H", diff --git a/res/lang/ru.json b/res/lang/ru.json index c9bf6f4..7bf0a7d 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -731,6 +731,7 @@ "market_cap_short": "Кап.", "market_chart_loading": "Загрузка истории цен", "market_col_name": "Название", + "market_col_trend": "Тренд", "market_col_value": "Стоимость", "market_iv_1d": "1Д", "market_iv_1h": "1Ч", diff --git a/res/lang/zh.json b/res/lang/zh.json index dff8c55..3f5d0f2 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -731,6 +731,7 @@ "market_cap_short": "市值", "market_chart_loading": "正在加载价格历史", "market_col_name": "名称", + "market_col_trend": "趋势", "market_col_value": "价值", "market_iv_1d": "1天", "market_iv_1h": "1时", diff --git a/src/config/settings.h b/src/config/settings.h index b0ef931..8cdd124 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -91,7 +91,8 @@ public: bool showValue = true; // show the converted/fiat value on the card bool show24h = false; // show the 24h % change (live-market bases only) bool showSparkline = false; // show a price-trend sparkline (live-market bases only) - int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history) + int sparklineInterval = 4; // 0=minute 1=hour 2=day 3=week 4=month (default month: a real curve + // from the daily series, vs the young in-session minute buffer) // Per-wallet visibility: "" (shown in every wallet — legacy/global) or a wallet-identity // hash (shown only when that wallet is active). New entries are tagged with the current // wallet so a portfolio built for wallet A doesn't clutter wallet B. diff --git a/src/data/portfolio.h b/src/data/portfolio.h index afd799b..300fc3c 100644 --- a/src/data/portfolio.h +++ b/src/data/portfolio.h @@ -31,19 +31,6 @@ inline double SumPortfolioBalance(const std::vector& entryAddresses return sum; } -// Shielded/transparent breakdown of an entry's balance (for the per-group Z/T split bar). Pure. -struct PortfolioSplit { double shielded = 0.0; double transparent = 0.0; }; -inline PortfolioSplit SumPortfolioSplit(const std::vector& entryAddresses, - const std::vector& walletAddresses) -{ - PortfolioSplit s; - if (entryAddresses.empty()) return s; - std::unordered_set want(entryAddresses.begin(), entryAddresses.end()); - for (const auto& a : walletAddresses) - if (want.count(a.address)) { if (a.isShielded()) s.shielded += a.balance; else s.transparent += a.balance; } - return s; -} - // Whether `address` is part of the entry's address group. inline bool PortfolioEntryContains(const std::vector& entryAddresses, const std::string& address) diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 535388d..844efdd 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -48,7 +48,6 @@ struct MarketViewState { int chartStyle = 1; // 0 = line, 1 = candlestick (only applies when per-exchange OHLC exists) }; static MarketViewState s_mkt; -static bool s_show_market_settings = false; // Market general-options modal (gear button, like chat) // The effective exchange registry: the live CoinGecko list when available, else the // compiled-in fallback. @@ -134,7 +133,7 @@ struct PfEditState { bool showValue = true; bool show24h = false; bool showSparkline = false; - int sparkInterval = 0; // 0=min 1=hour 2=day 3=week 4=month + int sparkInterval = 4; // 0=min 1=hour 2=day 3=week 4=month (default month) }; static PfEditState s_pfEdit; @@ -285,7 +284,7 @@ static void PortfolioBeginEdit(App* app, int index) s_pfEdit.showValue = true; s_pfEdit.show24h = false; s_pfEdit.showSparkline = false; - s_pfEdit.sparkInterval = 0; + s_pfEdit.sparkInterval = 4; // default month (a real curve from the daily series) } s_pfEdit.search[0] = '\0'; s_pfEdit.typeFilter = 0; @@ -1094,7 +1093,6 @@ bool PortfolioEditorActive() { return s_pfEdit.open; } static constexpr float kPfChgColW = 62.0f; static constexpr float kPfValColW = 108.0f; static constexpr float kPfDrgxColW = 118.0f; -static constexpr float kPfSparkColW = 72.0f; // Table trend column, reserved only when a group shows one struct PfTableCols { float labelR, drgxR, valR, chgR; }; static PfTableCols pfTableCols(float right, float dp) @@ -1110,7 +1108,12 @@ static PfTableCols pfTableCols(float right, float dp) // Portfolio row height + inter-row gap by style — one source shared by the row drawing // (mktDrawPortfolio) and the scroll-region height budget (RenderMarketTab) so they can't drift and clip. -static float pfRowHeight(int style, float dp) { return (style == 0 ? 40.0f : style == 1 ? 68.0f : 80.0f) * dp; } +// Row heights: Table 48dp (a dedicated trend column needs vertical room), Cards 68dp (two-zone), +// Spotlight 92dp (a dominant bottom chart band under the hero value). Uniform per style. +static float pfRowHeight(int style, float dp) { return (style == 0 ? 48.0f : style == 1 ? 68.0f : 92.0f) * dp; } +// Fraction of the Table label+trend zone given to the label; the rest is the centre TREND column. +// Shared by the row body and the header strip so the sparkline and its "TREND" label line up. +static constexpr float kPfTableTrendFrac = 0.38f; static float pfRowGapFor(int style, float dp) { return style == 0 ? 2.0f * dp : Layout::spacingSm(); } static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, @@ -1118,43 +1121,27 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, const MarketInfo& market, int style, float dp, ImFont* sub1, ImFont* capFont, bool hov, bool tableReserveSpark = false) { - // One pass over the wallet's addresses: the shielded/transparent split (used by the Cards Z/T bar) - // also yields the total, so we never scan twice. - const data::PortfolioSplit split = data::SumPortfolioSplit(e.addresses, state.addresses); - const double bal = split.shielded + split.transparent; + const double bal = data::SumPortfolioBalance(e.addresses, state.addresses); ImU32 accent = e.color ? (ImU32)e.color : 0; const bool zeroBal = (bal <= 0.0); // empty groups render dimmed (content only, not the container) - // ---- Per-style CONTAINER (the enabler): the three styles differ before any field is read. - // Table (0) = borderless ledger row (hairline + hover wash + a slim left identity tick); - // Cards (1) = glass card with a faint accent tint + a left accent spine; - // Spotlight (2) = a heavier tile (higher fill + a bold accent spine). ---- + // ---- Per-style CONTAINER: Table (0) = borderless ledger row (hairline + hover wash); Cards (1) = + // glass card with a faint identity tint; Spotlight (2) = a heavier glass tile. No accent line — + // the group's colour already reads from its icon and the sparkline, so it adds nothing here. ---- const float round = 10.0f * dp; - const int oa = (int)(std::max(0, std::min(100, e.outlineOpacity)) * 2.55f + 0.5f); if (style == 0) { if (hov) dl->AddRectFilled(rowMin, rowMax, WithAlpha(OnSurface(), 14), 0.0f); dl->AddLine(ImVec2(rowMin.x, rowMax.y - 0.5f), ImVec2(rowMax.x, rowMax.y - 0.5f), WithAlpha(OnSurface(), 24), 1.0f); - dl->AddRectFilled(ImVec2(rowMin.x, rowMin.y + 5.0f * dp), ImVec2(rowMin.x + 3.0f * dp, rowMax.y - 5.0f * dp), - accent ? accent : WithAlpha(OnSurface(), 55), 1.5f * dp); } else if (style == 1) { GlassPanelSpec g; g.rounding = round; g.fillAlpha = hov ? 42 : 30; g.borderAlpha = 55; DrawGlassPanel(dl, rowMin, rowMax, g); - if (accent) { - dl->AddRectFilled(rowMin, rowMax, WithAlpha(accent, hov ? 16 : 10), round); - dl->AddRectFilled(rowMin, ImVec2(rowMin.x + 3.0f * dp, rowMax.y), WithAlpha(accent, 205), - round, ImDrawFlags_RoundCornersLeft); - dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), round, 0, hov ? 1.8f : 1.2f); - } else if (hov) { - dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), round, 0, 1.2f); - } + if (accent) dl->AddRectFilled(rowMin, rowMax, WithAlpha(accent, hov ? 16 : 10), round); // faint identity wash + if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 80), round, 0, 1.2f); } else { GlassPanelSpec g; g.rounding = round; g.fillAlpha = hov ? 56 : 46; g.borderAlpha = 60; DrawGlassPanel(dl, rowMin, rowMax, g); - dl->AddRectFilled(rowMin, ImVec2(rowMin.x + 4.0f * dp, rowMax.y), - WithAlpha(accent ? accent : Primary(), 220), round, ImDrawFlags_RoundCornersLeft); - if (accent) dl->AddRect(rowMin, rowMax, WithAlpha(accent, hov ? std::min(255, oa + 45) : oa), round, 0, hov ? 1.8f : 1.2f); - else if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), round, 0, 1.2f); + if (hov) dl->AddRect(rowMin, rowMax, WithAlpha(OnSurface(), 70), round, 0, 1.2f); } auto tw = [](ImFont* f, const std::string& s){ return f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x; }; @@ -1194,12 +1181,10 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, const int zdVtx = zeroBal ? dl->VtxBuffer.Size : -1; if (style == 0) { - // ---- TABLE: icon + label (left); tight numeric columns DRGX | value | 24h pill (right), and a - // small trend sparkline in a reserved far-right column when any group opts into one. The - // column is reserved for ALL rows (tableReserveSpark) so they stay aligned with the header. - const float sparkW = tableReserveSpark ? kPfSparkColW * dp : 0.0f; - const float colsRight = right - (sparkW > 0.0f ? sparkW + colGap : 0.0f); - const PfTableCols cols = pfTableCols(colsRight, dp); + // ---- TABLE: icon + label (left); a dedicated centre TREND column (aligned + header-labelled); + // right-aligned numeric columns DRGX | value | 24h pill. The trend column is present for the + // whole list when any group opts in (tableReserveSpark); each row fills it when it has one. + const PfTableCols cols = pfTableCols(right, dp); const float chgR = cols.chgR, valR = cols.valR, drgxR = cols.drgxR, labelR = cols.labelR; const float valW = kPfValColW*dp, drgxW = kPfDrgxColW*dp; // fit() widths (shared with pfTableCols) float iconSz = capFont->LegacySize, x = left; @@ -1217,72 +1202,81 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, dl->AddText(capFont, capFont->LegacySize, ImVec2(pmn.x + 6.0f*dp, midY - capFont->LegacySize*0.5f), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); } - if (hasSpark && sparkW > 0.0f) - pfDrawSparkline(dl, ImVec2(right - sparkW, rowMin.y + padY), ImVec2(right, rowMax.y - padY), spark, sparkCol, dp); - const float lblMaxW = std::max(24.0f*dp, labelR - x); + // Dedicated centre TREND column: when the list has any sparkline (tableReserveSpark), cap the + // label to a fixed boundary so the column + its header label align across every row; each row + // fills it only when that group opts in. Rows without any list-wide trend keep the full label. + // Trend-column origin from the fixed LEFT (icon-independent) so it lines up with the "TREND" + // header and across rows whether or not a group has an icon (x may be shifted past the icon). + const float zoneW = std::max(24.0f*dp, labelR - left); + const float trendX0 = left + zoneW * kPfTableTrendFrac; + const float lblMaxW = tableReserveSpark ? std::max(24.0f*dp, trendX0 - colGap - x) : std::max(24.0f*dp, labelR - x); dl->AddText(sub1, sub1->LegacySize, ImVec2(x, midY - sub1->LegacySize*0.5f), OnSurface(), fit(e.label,sub1,lblMaxW).c_str()); + if (hasSpark && tableReserveSpark && labelR - trendX0 > 40.0f * dp) + pfDrawSparklineFilled(dl, ImVec2(trendX0, rowMin.y + padY), ImVec2(labelR, rowMax.y - padY), spark, sparkCol, dp); } else if (style == 1) { - // ---- CARDS: muted label + a leading (shadowed) value on top; DRGX + 24h beneath; and a - // per-group shielded/transparent split bar along the bottom edge as the signature. - const float zbarH = 3.0f*dp; - float iconSz = capFont->LegacySize, x = left; - const float topY = rowMin.y + padY + 1.0f*dp; - const float botY = rowMax.y - padY - zbarH - 3.0f*dp - capFont->LegacySize; - // Optional trend sparkline strip on the right (above the Z/T bar); text right-aligns to its left. - const float cSparkW = hasSpark ? std::min(110.0f * dp, (right - left) * 0.34f) : 0.0f; - const float cSparkL = right - cSparkW; - const float cRight = hasSpark ? (cSparkL - colGap) : right; + // ---- CARDS: a balanced snapshot — a LEFT-aligned info stack (icon + label / value+delta chip / + // DRGX) grouped on the left, with a full-height sparkline filling the rest of the width. + const float zoneTop = rowMin.y + padY; + const float zoneBot = rowMax.y - padY; + // Icon centred in the info zone; the label/value/DRGX stack sits to its right. + const float iconSz = capFont->LegacySize; + float textX = left; if (!e.icon.empty()) { - material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+sub1->LegacySize*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz); - x += iconSz + Layout::spacingSm(); + material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(left + iconSz*0.5f, (zoneTop + zoneBot)*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz); + textX = left + iconSz + Layout::spacingSm(); } - // value (top-right, leads via a soft shadow) - std::string vTop = wantValue ? (hasValue?valStr:kDash) : std::string(); - float vW = wantValue ? tw(sub1, vTop) : 0.0f; - if (wantValue) DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cRight - vW, topY), hasValue?OnSurface():OnSurfaceDisabled(), vTop.c_str()); - // label (top-left, muted so the value leads) - float labelR = wantValue ? (cRight - vW - colGap) : cRight; - dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY + (sub1->LegacySize - capFont->LegacySize)*0.5f), OnSurfaceMedium(), fit(e.label,capFont,std::max(24.0f*dp, labelR - x)).c_str()); - // DRGX (bottom-left) + 24h (bottom-right) - if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(left, botY), OnSurfaceMedium(), drgxStr.c_str()); - if (wantChange) { std::string s = hasChange?chgStr:kDash; dl->AddText(capFont, capFont->LegacySize, ImVec2(cRight - tw(capFont,s), botY), hasChange?chgCol:OnSurfaceDisabled(), s.c_str()); } - if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(cSparkL, topY), ImVec2(right, rowMax.y - padY - zbarH - 2.0f*dp), spark, sparkCol, dp); - // shielded/transparent split bar (Cards-only signature; reuses the split computed above) - { - const data::PortfolioSplit& sp = split; - const double tot = sp.shielded + sp.transparent; - const float barY = rowMax.y - padY - zbarH; - const bool barLight = IsLightTheme(); - dl->AddRectFilled(ImVec2(left, barY), ImVec2(right, barY + zbarH), - barLight ? IM_COL32(0,0,0,20) : IM_COL32(255,255,255,12), zbarH*0.5f); - if (tot > 0) { - float sr = (float)(sp.shielded / tot); sr = sr<0?0.0f:(sr>1?1.0f:sr); - float sw = (right - left) * sr; - const int fa = barLight ? 205 : 175; - if (sw > 0.5f) dl->AddRectFilled(ImVec2(left, barY), ImVec2(left+sw, barY+zbarH), WithAlpha(Success(), fa), - (right-left-sw > 0.5f) ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, zbarH*0.5f); - if (right-left-sw > 0.5f) dl->AddRectFilled(ImVec2(left+sw, barY), ImVec2(right, barY+zbarH), WithAlpha(Warning(), fa), - sw > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, zbarH*0.5f); + // Info zone driven by the ACTUAL content width so a short group hands the extra room to the chart; + // bounded [28%, 52%] so the info never cramps and the chart always keeps a decent band. + float infoR = right, cSparkL = right; + if (hasSpark) { + const std::string vTxt0 = wantValue ? (hasValue?valStr:kDash) : std::string(); + const float chipW = (wantChange && hasChange) ? (Layout::spacingSm() + tw(capFont, chgStr) + 12.0f*dp) : 0.0f; + const float valLineW = wantValue ? (tw(sub1, vTxt0) + chipW) : 0.0f; + const float contentW = std::max(std::max(tw(capFont, e.label), valLineW), e.showDrgx ? tw(capFont, drgxStr) : 0.0f); + const float span = right - left; + const float infoW = std::min(std::max((textX - left) + contentW + colGap * 2.0f, span * 0.28f), span * 0.52f); + cSparkL = left + infoW; + infoR = cSparkL - colGap; + } + const float gap = Layout::spacingXs(); + const float stackH = capFont->LegacySize + gap + sub1->LegacySize + gap + capFont->LegacySize; + float cy = zoneTop + std::max(0.0f, (zoneBot - zoneTop - stackH) * 0.5f); + // label (muted) + dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, cy), OnSurfaceMedium(), fit(e.label, capFont, std::max(24.0f*dp, infoR - textX)).c_str()); + cy += capFont->LegacySize + gap; + // value (leads via a soft shadow) + delta chip trailing it + if (wantValue) { + const std::string vTxt = hasValue ? valStr : kDash; + DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(textX, cy), hasValue?OnSurface():OnSurfaceDisabled(), vTxt.c_str()); + if (wantChange && hasChange) { + const float pw = tw(capFont, chgStr) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp; + const float chipX = textX + tw(sub1, vTxt) + Layout::spacingSm(); + const float chipY = cy + (sub1->LegacySize - ph)*0.5f; + if (chipX + pw <= infoR) { + dl->AddRectFilled(ImVec2(chipX, chipY), ImVec2(chipX+pw, chipY+ph), WithAlpha(chgCol, 34), ph*0.5f); + dl->AddText(capFont, capFont->LegacySize, ImVec2(chipX+6.0f*dp, chipY + (ph-capFont->LegacySize)*0.5f), chgCol, chgStr.c_str()); + } } } + cy += sub1->LegacySize + gap; + // DRGX (muted) + if (e.showDrgx) dl->AddText(capFont, capFont->LegacySize, ImVec2(textX, cy), OnSurfaceMedium(), fit(drgxStr, capFont, std::max(24.0f*dp, infoR - textX)).c_str()); + if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(cSparkL, zoneTop), ImVec2(right, zoneBot), spark, sparkCol, dp); } else { - // ---- SPOTLIGHT: muted name + DRGX on top, then a LARGE value coloured by 24h direction with a - // delta chip, and a filled sparkline on the right. A third font tier no other style uses. + // ---- SPOTLIGHT: "big number over a chart" hero tile — a top zone (muted name + DRGX, then a + // LARGE value coloured by 24h with a delta chip) over a dominant full-width sparkline band + // filling the bottom. The value spans the full width now (no competing right strip). const float heroSz = sub1->LegacySize * 1.5f; // ImGui 1.92 rebakes at this size (crisp, not upscaled) - const float sparkW = (right - left) * 0.34f; - const float sparkL = right - sparkW; - const float rightZoneL = hasSpark ? (sparkL - colGap) : right; - const float blockH = capFont->LegacySize + Layout::spacingXs() + heroSz; - const float topY = midY - blockH * 0.5f; + const float topY = rowMin.y + padY; float iconSz = capFont->LegacySize, x = left; if (!e.icon.empty()) { - material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+iconSz*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz); + material::project_icons::drawByName(dl, e.icon.c_str(), ImVec2(x+iconSz*0.5f, topY+capFont->LegacySize*0.5f), accent?accent:OnSurfaceMedium(), Type().iconSmall(), iconSz); x += iconSz + Layout::spacingSm(); } // name (top-left, muted) + DRGX (top-right, muted) const float drgxW2 = e.showDrgx ? tw(capFont, drgxStr) : 0.0f; - if (e.showDrgx) rtext(capFont, rightZoneL, topY + capFont->LegacySize*0.5f, OnSurfaceMedium(), drgxStr); - const float nameMaxW = std::max(24.0f*dp, (rightZoneL - (e.showDrgx ? drgxW2 + colGap : 0.0f)) - x); + if (e.showDrgx) rtext(capFont, right, topY + capFont->LegacySize*0.5f, OnSurfaceMedium(), drgxStr); + const float nameMaxW = std::max(24.0f*dp, (right - (e.showDrgx ? drgxW2 + colGap : 0.0f)) - x); dl->AddText(capFont, capFont->LegacySize, ImVec2(x, topY), OnSurfaceMedium(), fit(e.label,capFont,nameMaxW).c_str()); // hero value — coloured by 24h direction (neutral when no live change) const float valY = topY + capFont->LegacySize + Layout::spacingXs(); @@ -1290,17 +1284,22 @@ static void pfDrawRow(ImDrawList* dl, ImVec2 rowMin, ImVec2 rowMax, const ImU32 heroCol = !hasValue ? OnSurfaceDisabled() : (hasChange ? chgCol : OnSurface()); DrawTextShadow(dl, sub1, heroSz, ImVec2(left, valY), heroCol, hero.c_str()); const float heroW = sub1->CalcTextSizeA(heroSz, FLT_MAX, 0, hero.c_str()).x; - // delta chip trailing the hero value (only if it fits before the sparkline zone) + // delta chip trailing the hero value if (wantChange && hasChange) { const float pw = tw(capFont, chgStr) + 12.0f*dp, ph = capFont->LegacySize + 5.0f*dp; const float chipX = left + heroW + Layout::spacingSm(); const float chipY = valY + (heroSz - ph)*0.5f; - if (chipX + pw <= rightZoneL) { + if (chipX + pw <= right) { dl->AddRectFilled(ImVec2(chipX, chipY), ImVec2(chipX+pw, chipY+ph), WithAlpha(chgCol, 34), ph*0.5f); dl->AddText(capFont, capFont->LegacySize, ImVec2(chipX+6.0f*dp, chipY + (ph-capFont->LegacySize)*0.5f), chgCol, chgStr.c_str()); } } - if (hasSpark) pfDrawSparklineFilled(dl, ImVec2(sparkL, rowMin.y + padY), ImVec2(right, rowMax.y - padY), spark, sparkCol, dp); + // full-width sparkline band filling the bottom of the tile, below the hero value + if (hasSpark) { + const float bandTop = valY + heroSz + Layout::spacingXs(); + if (rowMax.y - padY - bandTop > 12.0f * dp) + pfDrawSparklineFilled(dl, ImVec2(left, bandTop), ImVec2(right, rowMax.y - padY), spark, sparkCol, dp); + } } if (zdVtx >= 0) { // fade the just-emitted body vertices for an empty group @@ -1504,6 +1503,23 @@ static void mktDrawPriceHero(const MktCtx& cx) float tradeBtnX = cardMax.x - pad - tradeBtnW; float tradeBtnY = cardMin.y + Layout::spacingSm(); + // Chart line/candle segmented control, immediately left of the trade button — only when the + // selected range has per-exchange candles (Live / aggregate is line-only, nothing to toggle). + if (cx.chartCandles && cx.chartCandles->size() >= 2) { + const char* styleIcons[2] = { ICON_MD_SHOW_CHART, ICON_MD_CANDLESTICK_CHART }; + const float csW = 2.0f * tradeBtnH; + const ImVec2 csOrigin(tradeBtnX - csW - Layout::spacingSm(), tradeBtnY); + const ImVec2 csSaved = ImGui::GetCursorScreenPos(); + const int csCur = s_mkt.chartStyle; + const int csClk = SegmentedControl(dl, csOrigin, csW, tradeBtnH, styleIcons, 2, csCur, + Type().iconSmall(), "##chartStyleSeg", dp); + if (csClk >= 0 && csClk != csCur) { + s_mkt.chartStyle = csClk; + if (app->settings()) { app->settings()->setChartStyle(csClk); app->settings()->save(); } + } + ImGui::SetCursorScreenPos(csSaved); + } + ImVec2 tMin(tradeBtnX, tradeBtnY), tMax(tradeBtnX + tradeBtnW, tradeBtnY + tradeBtnH); bool tradeHov = material::IsRectHovered(tMin, tMax); @@ -1626,30 +1642,7 @@ static void mktDrawPriceChart(const MktCtx& cx) bx += bw + Layout::spacingXs(); } - // Line/candle toggle — only when the selected range has per-exchange candles (the aggregate / - // Live view is line-only, so the toggle is hidden there). - if (cx.chartCandles && cx.chartCandles->size() >= 2) { - bx += Layout::spacingSm(); - ImFont* icoF = material::Typography::instance().iconSmall(); - const bool isCandle = (s_mkt.chartStyle == 1); - const char* styleIcon = isCandle ? ICON_MD_CANDLESTICK_CHART : ICON_MD_SHOW_CHART; - ImVec2 tmn(bx, rowTop), tmx(bx + pillH, rowTop + pillH); - bool thov = material::IsRectHovered(tmn, tmx); - if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f * mktDp); - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } - ImVec2 tiSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, styleIcon); - dl->AddText(icoF, icoF->LegacySize, - ImVec2(tmn.x + (pillH - tiSz.x) * 0.5f, tmn.y + (pillH - tiSz.y) * 0.5f), - thov ? OnSurface() : OnSurfaceMedium(), styleIcon); - ImGui::SetCursorScreenPos(tmn); - if (ImGui::InvisibleButton("##ChartStyle", ImVec2(pillH, pillH))) { - s_mkt.chartStyle = isCandle ? 0 : 1; - if (app->settings()) { app->settings()->setChartStyle(s_mkt.chartStyle); app->settings()->save(); } - } - if (ImGui::IsItemHovered()) - material::Tooltip("%s", TR(isCandle ? "market_style_line" : "market_style_candle")); - bx += pillH; - } + // (Line/candle chart-style toggle now lives top-right of the price hero, left of the trade button.) // Refresh button (far right). float rEdge = chartMax.x - chartPad; @@ -1975,23 +1968,29 @@ static void mktDrawPortfolio(const MktCtx& cx) } Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio")); - // Settings gear + "Manage…" button, right-aligned on the header row (gear left of Manage). The gear - // opens the general market-options modal; Manage opens the portfolio editor. + // Portfolio-style segmented control (Table / Cards / Spotlight) + "Manage…" button, right-aligned on + // the header row (style picker left of Manage). Manage opens the portfolio editor. { const char* ml = TR("portfolio_manage"); float mBtnW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, ml).x + Layout::spacingMd() * 2; - const float gearW = ImGui::GetFrameHeight(); // square, matching the Manage button's height - const float gGap = Layout::spacingXs(); + const char* styleItems[3] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"), + TR("portfolio_style_featured") }; + const float segH = ImGui::GetFrameHeight(); + const float segW = 210.0f * mktDp; + const float sGap = Layout::spacingSm(); ImGui::SameLine(); - RightAlignX(mBtnW + gGap + gearW); // reserve the whole group before drawing left-to-right - IconButtonStyle gst; - gst.tooltip = TR("market_settings_tip"); - gst.hoverBg = WithAlpha(OnSurface(), 30); - gst.restBg = WithAlpha(OnSurface(), 20); - gst.bgRounding = 7.0f * mktDp; - if (IconButton("##marketSettings", ICON_MD_SETTINGS, Type().iconSmall(), ImVec2(gearW, gearW), gst)) - s_show_market_settings = true; - ImGui::SameLine(0.0f, gGap); + RightAlignX(segW + sGap + mBtnW); // right-align the [style picker | Manage] group + const ImVec2 segOrigin = ImGui::GetCursorScreenPos(); + const int curStyle = app->settings() ? app->settings()->getPortfolioStyle() : 0; + const int segClk = SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, segW, segH, + styleItems, 3, curStyle, Type().caption(), "##pfStyleSeg", mktDp); + if (segClk >= 0 && segClk != curStyle && app->settings()) { + app->settings()->setPortfolioStyle(segClk); + app->settings()->save(); + } + ImGui::SetCursorScreenPos(segOrigin); + ImGui::Dummy(ImVec2(segW, segH)); // reserve + restore layout flow + ImGui::SameLine(0.0f, sGap); if (material::TactileButton(ml, ImVec2(mBtnW, 0))) { // Open the editor on the first group visible to this wallet (or the empty state if none) — // never a raw index 0 that might belong to a different wallet. @@ -2101,8 +2100,8 @@ static void mktDrawPortfolio(const MktCtx& cx) } int style = app->settings() ? app->settings()->getPortfolioStyle() : 0; - // Reserve the Table trend column for ALL rows + the header when ANY visible group opts into a - // sparkline, so the columns stay aligned regardless of which individual rows draw one. + // The Table's dedicated TREND column (+ its header label) is present for the whole list when any + // visible group opts into a sparkline, so the column aligns and doesn't flicker per row. bool anySpark = false; for (int i : vis) { const auto& e = allEntries[i]; @@ -2136,8 +2135,7 @@ static void mktDrawPortfolio(const MktCtx& cx) const ImVec2 hp = ImGui::GetCursorScreenPos(); const float hLeft = hp.x + Layout::spacingMd(); const float hRight = hp.x + rowW - Layout::spacingMd(); - const float hColsRight = hRight - (anySpark ? kPfSparkColW * mktDp + Layout::spacingMd() : 0.0f); - const PfTableCols hc = pfTableCols(hColsRight, mktDp); + const PfTableCols hc = pfTableCols(hRight, mktDp); ImFont* ovF = Type().overline(); const float ty = hp.y + (headerH - ovF->LegacySize) * 0.5f; const ImU32 hcol = OnSurfaceDisabled(); @@ -2146,6 +2144,8 @@ static void mktDrawPortfolio(const MktCtx& cx) rdl->AddText(ovF, ovF->LegacySize, ImVec2(xr - w, ty), hcol, s); }; rdl->AddText(ovF, ovF->LegacySize, ImVec2(hLeft, ty), hcol, TR("market_col_name")); + if (anySpark) // TREND column header, left-aligned at the same x the row sparklines start + rdl->AddText(ovF, ovF->LegacySize, ImVec2(hLeft + (hc.labelR - hLeft) * kPfTableTrendFrac, ty), hcol, TR("market_col_trend")); hdr(hc.drgxR, DRAGONX_TICKER); hdr(hc.valR, TR("market_col_value")); hdr(hc.chgR, TR("market_24h")); @@ -2176,87 +2176,6 @@ static void mktDrawPortfolio(const MktCtx& cx) ImGui::Dummy(ImVec2(0, gap)); } -// General market-tab options modal (opened by the PORTFOLIO-header gear). Mirrors the chat/contacts -// settings modals: a BlurFloat overlay of right-aligned segmented controls + a text-sized Done button. -// All controls write EXISTING settings (+ sync the live s_mkt mirror for chart range/style so the chart -// updates the same frame). Rendered outside the scroll child so it overlays the page. -static void RenderMarketSettings(App* app) -{ - if (!s_show_market_settings || !app->settings()) return; - auto* st = app->settings(); - const float dp = Layout::dpiScale(); - - OverlayDialogSpec ov; - ov.title = TR("market_settings_title"); - ov.p_open = &s_show_market_settings; - ov.style = OverlayStyle::BlurFloat; - ov.cardWidth = 480.0f; - ov.idSuffix = "marketsettings"; - if (BeginOverlayDialog(ov)) { - const float ctrlW = 280.0f * dp; // roomy enough for the multi-word segment labels - auto rowStart = [&](const char* label) { - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(label); - ImGui::SameLine(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, ImGui::GetContentRegionAvail().x - ctrlW)); - }; - // A right-aligned segmented control on its own row; returns the clicked index (-1 if none). - auto seg = [&](const char* label, const char* const* items, int count, int cur, const char* id) -> int { - rowStart(label); - const float segH = ImGui::GetFrameHeight(); - const ImVec2 o = ImGui::GetCursorScreenPos(); - int clk = SegmentedControl(ImGui::GetWindowDrawList(), o, ctrlW, segH, - items, count, cur, Type().caption(), id, dp); - ImGui::SetCursorScreenPos(o); - ImGui::Dummy(ImVec2(ctrlW, segH)); // restore + reserve the control's rect for layout flow - return clk; - }; - - // Portfolio layout — surfaces the row style that was previously only Left/Right-arrow cyclable, - // and (unlike the arrow cycle) persists it. - { - const char* items[] = { TR("portfolio_style_compact"), TR("portfolio_style_detailed"), - TR("portfolio_style_featured") }; - const int cur = st->getPortfolioStyle(); - const int clk = seg(TR("portfolio_style_label"), items, 3, cur, "##mktPfStyle"); - if (clk >= 0 && clk != cur) { st->setPortfolioStyle(clk); st->save(); } - } - // (Chart RANGE is deliberately not duplicated here — the Live/1H/1D/1W/1M toggle is always - // visible on the chart itself, and a 5-segment control can't hold long "Live" translations.) - // Chart style (candlesticks only render where per-exchange OHLC exists; the setting is still stored). - { - ImGui::Dummy(ImVec2(0.0f, 4.0f * dp)); - const char* items[] = { TR("market_style_line_label"), TR("market_style_candle_label") }; - const int cur = st->getChartStyle(); - const int clk = seg(TR("market_opt_chart_style"), items, 2, cur, "##mktChartStyle"); - if (clk >= 0 && clk != cur) { s_mkt.chartStyle = clk; st->setChartStyle(clk); st->save(); } - } - // Fetch live prices (master toggle, mirrors the general Settings page). - { - ImGui::Dummy(ImVec2(0.0f, 6.0f * dp)); - bool v = st->getFetchPrices(); - if (ImGui::Checkbox(TR("fetch_prices"), &v)) { st->setFetchPrices(v); st->save(); } - } - - // Done — sized to its text (+ a little shoulder room) and centered, not full width. - ImGui::Dummy(ImVec2(0.0f, 8.0f * dp)); - { - const char* doneLbl = TR("chat_settings_done"); - ImGui::PushFont(Type().button()); - const float bw = ImGui::CalcTextSize(doneLbl).x + ImGui::GetStyle().FramePadding.x * 2.0f + 28.0f * dp; - ImGui::PopFont(); - ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - bw) * 0.5f)); - ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 205))); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(Primary())); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 235))); - if (material::TactileButton(doneLbl, ImVec2(bw, 0))) - s_show_market_settings = false; - ImGui::PopStyleColor(3); - } - EndOverlayDialog(); - } -} - void RenderMarketTab(App* app) { auto& S = schema::UI(); @@ -2295,7 +2214,7 @@ void RenderMarketTab(App* app) // Left/Right arrows: cycle portfolio row styles (compact / detailed / featured), mirroring the // Overview tab's layout switch. Skip while typing, when Ctrl is held (theme cycle), or while the // portfolio editor / market-settings modal is open. - if (app->settings() && !s_pfEdit.open && !s_show_market_settings && + if (app->settings() && !s_pfEdit.open && !ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) { bool prev = ImGui::IsKeyPressed(ImGuiKey_LeftArrow); bool next = ImGui::IsKeyPressed(ImGuiKey_RightArrow); @@ -2431,7 +2350,6 @@ void RenderMarketTab(App* app) // Portfolio editor modal (rendered outside the scroll child so it overlays the page). It captures // + blurs the live content drawn above, so it must render after the tab body. RenderPortfolioEditor(app); - RenderMarketSettings(app); } } // namespace ui diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 17c73f7..9881b3e 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1568,6 +1568,7 @@ void I18n::loadBuiltinEnglish() strings_["market_24h"] = "24h"; strings_["market_col_name"] = "Name"; strings_["market_col_value"] = "Value"; + strings_["market_col_trend"] = "Trend"; strings_["market_24h_volume"] = "24H VOLUME"; strings_["market_6h"] = "6h"; strings_["market_iv_live"] = "Live";