From 47513b9764beeceb0c2f5e46213ea915531d06bd Mon Sep 17 00:00:00 2001 From: Sakyvo Date: Wed, 29 Jul 2026 14:08:18 +0800 Subject: [PATCH] feat: add Grok identity and tab reveal preference --- extra/logo/README.md | 20 +++ extra/logo/ai_grok_dark.png | Bin 0 -> 31074 bytes extra/logo/ai_grok_light.png | Bin 0 -> 25282 bytes nebula_app/src/display/chrome.rs | 94 ++++++++++--- nebula_app/src/display/mod.rs | 207 ++++++++++++++++++++++++++--- nebula_app/src/display/settings.rs | 110 ++++++++++++++- nebula_app/src/input/chrome.rs | 14 ++ nebula_app/src/input/mouse.rs | 2 + nebula_app/src/window_context.rs | 2 +- 9 files changed, 412 insertions(+), 37 deletions(-) create mode 100644 extra/logo/README.md create mode 100644 extra/logo/ai_grok_dark.png create mode 100644 extra/logo/ai_grok_light.png diff --git a/extra/logo/README.md b/extra/logo/README.md new file mode 100644 index 0000000..23b1bdb --- /dev/null +++ b/extra/logo/README.md @@ -0,0 +1,20 @@ +# Third-Party Logo Assets + +## Grok + +`ai_grok_dark.png` and `ai_grok_light.png` are unmodified copies of +`Grok_Logomark_Dark.png` and `Grok_Logomark_Light.png` from the official xAI +asset archive: + +- Brand guidelines: https://x.ai/legal/brand-guidelines +- Asset archive: https://data.x.ai/logos/xAI_Grok_Assets.zip +- Archive SHA-256: `F41A93923A85047B4B5A9571B7EC73339F562C3E58ACD096E25584AB0AE2A1FB` +- Dark PNG SHA-256: `37DDBCB6E2A7F2E4B3BE78A7D41296A3BC7EDF6926362434EFC00DF5A56A3586` +- Light PNG SHA-256: `359056EE8983CFA0BA7E72795078C7C0DDF6C5D7A1870401AB960ED4F9DF9E53` + +xAI owns the trademark, intellectual property, and branding rights in xAI, +Grok, and their logos. The xAI Brand Guidelines are usage terms, not an +open-source asset license. These files are used only to identify a running +`grok` or `grok-cli` process and do not imply xAI endorsement, approval, or +sponsorship. The application selects the supplied dark or light file for +contrast and scales it only when rendering; the committed files are unchanged. diff --git a/extra/logo/ai_grok_dark.png b/extra/logo/ai_grok_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8dd11d2e177cddf25e010f6de33bd35b031a2c63 GIT binary patch literal 31074 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 literal 0 HcmV?d00001 diff --git a/extra/logo/ai_grok_light.png b/extra/logo/ai_grok_light.png new file mode 100644 index 0000000000000000000000000000000000000000..adece9a435753cd978539a9d1d0301919359906f GIT binary patch literal 25282 zcmYIwc_7r=`~Mk?eW~nHLyNT7B1D;y+eR{NNMukUSxS*9-*1v3bpCk-?ow>fd;<-hKfe}vMArZ7_&R-pOrRC@P{Y{xeu#t$riP0$? zzxc*|(Leh(GUtj{Z|06pW<4HR7jUORXYbHhfJsbWVL(%kG|a>D@9F7DW5X1vCR+u& zBn}^bF0!~$-LsGEMgsgo3Y9!^jPUYt(T@V(=1H;~e6Sqt1h}aZHv7HaPQbk@1*J82 z9e8*ke4t-1RWNP=2E$R}4A0;LJ4A(nlX^m?h#NI2!DaZ+?0{Wqe0p~q84At1)A-B< zv%@5~J zr=J+C6tOI;e0@3n#VpIBrT(z2GMpj^?-H@QzJmcRbH+s9qEc=WEEnmwp8m7h2d#~T z%sc-rxbg(;4{gZ-@qGF`Xh?av36>^Wx2TATe{qCQbWlrI=*yiQsBR0d{W=}WsE@Cd z;a^@hioW%slpHnNKVp+So;%x;`}hA^-kbYBf>$k0{BMsduZH(?!|SFh_))VHXs#9Z zD8|lC)}^ubf0I>6{Gqz*h7M32Fe4IyW7|LkIz_!Ijd%RvxTc;g?%NwN2MRgE@cm_0 zFuEKyFej0~xqgf1;j7?-Au4({J5ALs%l`yCFZ*mcdv+PFEpK_`R*}%>-PSC_x55Xi zy7_0*X=_bE3c0fz#W{8ec6~%tpJK90&@?3yABUUE|BuFl)hd5z?6v@@m=Dz);URh4 zw`W#rXs1jQ@PXMqj`*mJ{WoUYA;tfz-*1}ur+#wPoImxq)nI4-^tX|V|6l)ADi~R! zeNq>upo#Sqs|mH=TbMANdLxA8O61W}7fcOmjQ=@PMjy8qux}~CFSO}7O)#`eK-X~J zDw0ujf3LC7cXR1;710lkcV#@!e%wG?Bn2HbxkUO?JgP?YgJY|vh~I3)*XxVgg#;7h zMODW{3}Zws*AY(iM%8 z$3!OGo?*}buf#zXSMr|ZGc|?bN>-vaMGYBzTt5Z=9cp1mQAI@^n~0#G(!b)XVlv5ciuZs>la`-D>L zqi913c_yiDgv}cn8B&!-Z(M-=b)i27a2{}+Cea=*tRm2r#ZG-Nlog*GVW(T7Z1>Y} zqEg;mbBc>w4xC@`;LK;eHxkp#4gUsz*?3fkOm`Q$c_F_&>Adz1_FPdDRl{FDgMGpm z;ya@KzxaMNRF^!~MA?7)1c92rp`1QW?Z;2TY_26s}4l%7pe}~z!4f-NMp5Nh&l)9QBMlE@>ts6a8w(L7V(3{TQYUfg@>{HV19c%(5&y~mUYC= zf$W|lC+0CAD|W|YHO*5s9=Cx`$7vLvg6%{e*;Ekh z4RfvvYVP7%3n!obI3rWfI&bujafy(Kg0sIuBsI~pj@0s&#zOe?jIgtOeD!trdP<~nZ4G%dyUruL-b4w#(O-lKZ92Yul$7bKOnLO8{2D!|m@3bB_<-48xTC`TPWYSf z?)&115hpWKlWeFW&N^%-ASVrL3N1`w@WG`9hQm1-<{YD-+^jCW| zaHhG{c-zm^kcXtkpD~eGV7_A4<5j|*vhO>LS3=()vv{o2xP;W)Tiy^$yVVk$c`{~suhWNK$+qWw$D_sQi zIUn}>&A#zHxR7*XgCTmol{;O~fva%z6Zp7pUlbh_#=tafElj!6n?=gI{JNQ2DE+#2 zTrN>FslSWQK08%39_%Hm?l?lGUIylFa6xT*yI|wUBT8`_VdKn>_>FLT#0v@3yLBOV zbOthp`OGgfMhr1T@kubpJ-wUy*fNaavEGR5cR$S~2=6Tl{C1Oy(@$p-RyJ%AkR8bt+}K6HyWJ}cWQR-G5fz}- zfA#rTSk@?rW!o?2k)KU_&LJYkId`k};9KOpO_{utXk`smP&nOw+LV`6~BBX^(K4x-9fcUi20 z!0{OSeQ-O4L_I56P9LmZD|7BXT*;`EMBB}*9kFTCrVzE{Mfz1ZaJ@H}!0{e-Ag>Ru zGvaO#NNG6OScTob`%JjNT4I&{_BbB7?({BWP;zp4ol3^Vbv1J-wpt2!O!JHpTo(+@ zu1DAjAwliv?Yr%D;e3Y$gjG*J&ocS8sJ7^n5=%d3AV)t+)%aOLIh}L@Kc6X%XdB9t z=s{y#YiyaWP&}D-?e4YFncKb++EUdIc%t694CsCKL2C53-(;C|66<)(K)3j9$!!mQ z^V<`@;wyf_O|&<%G4zrR^FS~bYsS(~y(|>Q zO8I@!TTgOd@b3-hIoNdb*MDq@u#Jkzo&&v;1}BUr0ui;ibM*FdiI6PN9MyyLjn#0& zEW0*Ca86emArvKEhZ{9rjI#bXLT%;79oz$S#(3cOs%XYCLX_(c0a+F6zbB456cc#V zNjG*Nf(_2^m;Nc9lXq`+ITv6#>wlcUqt(oAk4U)|cQ${NI;qLO$U8~9D_3xK99Dc8 zn(UMRg)K{OATe^sylhaMuCV7~?B;yM6ZpypFuGRx^Hc0h&o_!Z5@RUP+k(UiB&;kf z;|EW$cfy_c#^m)|`bCelWoV#tSLFUo>gWpB>#AT%9G!ZGnPer2ALxUX1|&|#0p{+8gnktK+> zH|D2g);T`@#jbE)y^|EGc|BVDDIH%GPe+w`(5Yb#Mz&VPT0sw+tz_rbjqIB`c*^~q z1jRq8ZbY}_NmAU^uCTYa%IV#G>`ibs9bX-9Y|kO1rLFNn4DT+wQLoSZ{&ACESh%gS z3SvBzFUHvKbM^!)BWhx2bC~#%xJB|j-14NdS@|3aOSAmma(1=_0gYL0wayvb=Xsfr ztGnQ9HUVK0#TYXCv81HwleUa2Xs_#re&-Ys>u~w^e?t}`x3NXPIBygWfvH&0K)?8z zfOfAec|bq=l|_b)be=g(=~x$!4;?R~M_~)mb z*NUOt?^tBye9wVfTp%8$wGAw`t33L}S{TqrV{~F2VtIH(?td8`v#<<*{))8~xcOb7 zXW@uk( zu5K8?Z?_^ATsvY#G$@}rq$g~zF!eYsi9fTnkRM{<#!7 z(yFg86fkA6s=3%iEzxm$A5!r?X+oEVrJ5h_U0Z4WxHrF_t;3@Sx8U=8q)}ec=*~~4 z$%R621G7BU^`_MA1np_#xN3+5a;*>fMjo097DIg}Og^*a&glr-KculDfc|$6Qir*{ zond~HQmp;zaWmUJ?l`OQ@J|b;OxtW0AkHH9-%^yWZKFmv_;lYpe72%2;$$a`U=fwz z7P4$&>AZM6%0w8v6n6fL<&YBP-K){p4(Yg=ehQx+?2#u6?N`;xt#uM`uVHs4q*!_$ zH-35&dHA;iJon#rF`9R|+}gq!IULo)e(aFlw7HBv8IQ;L;hu8~5xTZ|K#5U#`~Bbw z_9a_TDRDD?ci%-?iXpfM$5Xk}WLc!2R(k6CJWOdknBLvAk$^Prc+t`B4gB02eDb-8 z(#O#F!itg$g}m2fbgNERFEA|rd+J^KB4e5=sfbtx<{yc^177YQu3 z37FM(3s66(sG@75i#}On5j}Ke@RHz0-ud@hgEoLO6uqqE@Aik6I+6Y73d6TOrZx2F zk!hy@*==_DG-Xd&vB^n)!OVTCPL?AGcJ3`c`APIldpWHEt`X0-xqPPa6z?`(GRC!eZ_TJ zcj@nQbn+Csler}JT;W_y{kPf7z)eElF_7MO_XLG`a}x5$@>`Ei25e@(H5CM;z7Y?# zr70H=aoS8+`)KYji^dF3#LZ06#^UjJ!mQ>CVkjG8DCD<6rE+ba`*(-s7kl@Pe0p=n zxg_Bprz?oiekI)Z9=RQ6s>@)O-=~dLBZjIn#?x8*!!v&7)2BoGYT4ZLotjJ!E1SHF zMnr;#n2WOTpHL1MsI z?_~JE!(=(3_7Q#Qb!SibN*~u1gv#;Q(vx?_Ep=27ZBIO1P`Pm*-cvIqPu^wU&Q5C% zBMP<0!liD%&uL)G6_ti;>z<(bPxgK6A*3VplI-9;ejIReTD6XIRse zyG)xeS!J?K=9R{$;nw^If0gez-KqafLq`zm zTSh;+u=gNkqKWECS?w0>Kd#LGy8w5s4BY1Sz5W&o#&$q5?Cx+~hw>(=+np-4@hz#5$nK%!tN;&nuYyF$=N)l#zC4Z0P)bxWk z=ed*HR>Vw@DhB`Nj3?kX&SDqr1xEV^ldWZE$pj8Y{0`Tm7M{O6yuWo(+s8E?gjn)@ zW$X!GZ>Bnp_|8QIvF7f7YX(l2@AMfJ%bFV1%ljBk;+qb9+;K6pw+D~mER{ty46llw z3I3voHtN1toPLdqCp!sp&k-2WpuSc>SWvj=n?(J6Ip1l*WAE~GLrHY(I$Mfrz+=jW z@hVLieeQ0d(s6KuM5 zPJ^||L%C8CYi$|bv<-0})WQnsJ$T!Woocf5=LWu%-&KCQiZ~KAPBv1SX_Z)&dC7xM zRzSBF3oSC0iW1;bK5|2VucK*B%6lndx#A2x8Qig!zL;4*4P8+!!&g#VhGp`VBO+3D5a>r?A z8qjsdBXW89?%@qOQg{=A^BBe&-YV!Z2gUHZke5+?e7jG0V|y&39gN2tdc}j_)XHfk zV1eTAn-Y65^kk_p_o_F!@R%H0wWQ<*Gd`WZt#eQ0Lbx0Imk4v6Ng;9zP#%N#P8h%` z8g3v*G_|Wz&`RjlLLwAGOsk6TeQ4XuE^xKztAyg&seE>149DO&;#4Pu;%SVd$Majq zhQt{S_WkFjbcMMdvjZC6A3bKH*n??SyvxC_@Rjm{%E;n&=w*Wvp?br5nfck`83dl0=~knk&*orj+tNml(HElZyL|3J{}`Rtqbp4jP4d%>X{^nOV$1{O zMV67qqVTmI>v)j}7nvPS$Yu~sm7qAr(SYDQ( z*SD=q#2qI$D3 z5z#m}QSa}MF=N7dKf`F(6ZmxPJhG@9oVhJVeVzP!hse1*`CMy!f4zu+p1avAo2!%v z+6Vk*xW1$2gr+K^ZuR8?rX+H=74*ERo!XG&V{`~{cm-G}L~vY$G4iM=makM@Iu}_o z8tLPg?L@o835%yI)=Ld+C7?lnzZtq_NJ+R?Eg4zN!A`GTjyN;^Gd+3{&F%SMqq%Pu zNOd2QL~U`6^q?z=GbM_sD1p!Y8`udg7eGtNh(fZ`biWwN0q@oUf{}yU`^hf7KH=Zk za-z0Mt01!4W*g}rT<(xIH0ISzLWM96meE1~0rptXYEjHz9|9Z$J%v#Gpo6ZB zLd1C_{flC#ZBfbVOLZ0Z8FyTk3j;bOIyRM&sf&oHsI+DZUrk#kioP@EOXHMBDyZX- zB!1Ku1jQNJtcEu4^QP(AAOU$&FxVY2(=GboiAv&g;b#;qTMM|>B#-U0_|y3*EQ`i( z?~}n%o4r8l@Ix?`G=zowheRu1EEIpf3o!)!XWqy(>L=oz|4}TbB8pw|+qdRzf>7!@ z)FD|$u8}38nklw3#5rmQ@l0T-G`^droVSdKoIgINKWRt=`r$fYt&v69Ut4K`Q_W{^ z8*sCsD{#d*0K!Z2v$uZxO5K(LbnDOrea&6XDJc$^S&mF#d-v_Tek+92@tI3?d4Cx#sVnP$s6 zL0+$8pT7cMKjyW9Ybq`$s(Jd)bo-0wLEHzV2y&N~?@AH+|B-1fS0Y^0Jb`m#o$oy4 z=Z--P1txK4Hf~men?anR_GKp6g?ZP7fKsJ#mN;WZAJrs#P2gK=R`g&5!8yK?b-9&B z&;vvX7`{$cLxINzXNN%HK9b0BtK8}6ak_Frw~s6isa24!=pOy2sjnFGeuH%a0Y#$39~%je#@?+(`4kjfi|O&eK{g`!}7NtWegSXZSA&r8+wa z5{n(k7FGv~;C^q=Qj>IwO@WyanB~FBA>u@I_=Q3bVc*q#xRV%TkI1r5W6TF~On>2l z5o##=7GFuKRiceTV64a>mK_zu2Aq9_qm|2q<@)9X3o2qc=U@SwnRl;2SSX&Z@ewDA z+V;txj}NRlw+`}3a7xO^7y5Q^6jV^w6`;z}5z-^b72L2*z7%tdDP7iT|NIa&HCcn7 zkOgTWGs5O!UaQG{IX|pm9p`v-#|TgY+1L0=*J_C(Qz`4Ck*(a63g#(=o-7uY|EMN( zxXGg3iw!h}Q*Rm-m}?Vf%U8kW0QBjB@P<4dBhg~w={G*=bwGZ9x2}QK@ZN{ZY=2FS zz6Ks`6eR8q6w66R6oX>|i<$?gatAQ+?DKr3XVc3&v~ma!Wc`2hqbzaHVpxzD$^(V5 z>iaIY&>4*o;sT@+?7B`GAB)mL*`VpqM;Hr*rQtwjX%*{41i!rk(NyEqMUnm?-$(^wV^H5J{cg4Kdcf^)sp@Q_k z7}?BAF*l;?hk!mdJYsc`&hU>*Y0yh>WX# zcQ&jiZcgmJpj?WsTQ*$O2V$wT;Qa@N$&|$+b7@0& zkBG4EYREoc;jqsHi8Z$jUj>@VphPuFf17qDSvh7ZTC<=dlW|%Ttuid3y~v+${+EYn z9~pe|d5hk)%a1nUdcEBD{iDAfsKrjb+;5i%JZW?=WM*586_3O^RfhjCbiyiy&v>anid;914k6nS8CVQNRw55%JmPJ zBX_;g`(;8WLx=+grSA*+>3&kHI7qVZxr{UsrG#tOzX!>D2f?SV%fjr&6ue*ljQkM( zDp0&^4n-|RO_!aR^5E8wuXrMXa$VHQd@|F3`|nozso|RLKwdm6Vbr zuhS9do;&W6y2}M8)Nd9rw*^gdNunu;*( z#xUK*PMxA&|1*;|1~$rHZkLb5-8y~(C-jLdHQKh9AIZ!y`ojK0yI9*>XPcZG$6r|v zNm1wW=<_@V+^}NTmppo0x9>u;e|0D@M74R=#y95OW+*F`Pd?i=%uZO2hyT{gy0R9n z%tV}lgnr!I7A|t}-5Vf^M}j4cSY=%i<5Yq*5+6vtR)<(8%kZDCz}PVqdl%s3XXGt+(U$sHcvl?RHvTF=1w7_2GF zw&#cji|KqV6A64;QxJ1lc9RON$C8ds6UBH*9mLR{am# zDZ_k2&*ji$bV-)&LJaO*m5l<@MpE(opaS6~Km7;CZ5>~ft^f*`_2B(AM6_`(w)skN zYk|;d^90SKn&A3~BzDO7HjbNSm0nqS8CB{ENvnpov;XNQKlOSVu9g#(vvE)mt&yFi zh`PVu-mxGOj)Zm@UOHvmMOP~~LBNB2&2#42;Dd!8#n_7b+t8Ug5cbtO!DB0RAJeKo zFh3h|GoeR6WhtK6f@F1sq#xduc<3(7H??P_p#Am2R$7<}y3tm%zd*r$;gyBeYp-tbtj@J;_~)QUrY$DNf7r|H^4J(=W2^*C2jwDeFs#&ajl#%M6Q7U z2@c-bY}Uoz=0TO~9~IT@^xtufy}Ll|dafKgv`pCU@XhPgt$kSB&Jv{xbU0tZc_V@G z=+TN4r)e@rY^MNU8;=+Fc{dH?Hu{Yr@*O@@@y%f_o1o3)&gIO|NMt&{4EGOFf7#WZ zj7Pe9*7IzqY+59qz_F`8j*_{CvYZR?xOG+Whs|!-6WCkt`PmTmxTcw-x&shQo2{3l zk-69qM8vA->bnnGO4bn9Xmv5;yMh>B~I)CWGJehdzffx}uETzkZa} zN5)G%mmKUDV{Dbd!-u{7Ce<}szne3xj>PQ&Lwa60;y6-Q8y#TC$ly`^KCo;nza8rJ z9ho#$*voTY@!>E6XGuI>(zVyj4QoC0jk+NFtIGyphhX0FeVOm((I9a5?N}AQYv}$9 z1^m=B>v-CKT?veXb(oSdfk!tIotEdq+{CcWlk#HjGO*NtWb7M>=)RU3H?!37+dD%c zUB2;w++h~V6O%<3f;7-ixd-kgXVf|1h2pwJ+k?cp26%-V=JH7HFm2`yG}>00y7>#l zVa!s?@VJf@F*kI-A`&m;7}brI6HIEV#ZRr;Dv5fKz^%-8#kE zYlUvaj;9~_J*qtCE zPa!k-+P)OuqOR$q8MLG??kbE7$Nixrbw$Go#y)A>v$yVA9KMw!kQZCkddRZHAx;Hp zTpQVNruKzQh$#bH-9zg7tRj4 zNE{I$YJq1saZQV{>c0rA=p54ECOwiv z*dif)+~x$&6Zve7q5k7ovxJD=gD@XJIvk4NQd>83AI3a9nTcenXXd{#^#^6@Gr5_1 z&j!uG!iVA(8{o;_MTd9lK8DQ%H|N4TdB*BTIpR;22`h?ghMjIsu@3zIioFAbCohKi z^v}ce>@MbpwU1q2IM&=|OKIyHldfG*fdQEnDA5S|o#pB-3>Dz#a)YJ67>cpvZcWbEsS zoXTuVVUNyOeGd70Z`{={KLRcu!@NAB&~bm43isk-0nIBA=K!d!6U0jkmPVP zPW8Qi0tbgl52Jw-wgJ=#Fb1ALx*yjg(syD08%fDWl+E7Fou#Wziqh$f5;x$Eh2VK^wqA+eTw0r^K7Cok?m9XjQMa$Uby6K_2) zaT|Q3-}!U~KIu zp@yM)23NPMV$XT&HfEdOHr1fE0BQH(v9+!gv4(wH1w(dK!A^zmFPWBT0GZ zJP&J!Kn!1r2@+s#YYOBgi%}wWe?EQq=@Ma23cY8BcOJCF*eN@0LI?HDoJt>CncemHp z_doKP>}#P_c>a)Y(GF5_Wh7fS380kaK?(=+izFUj`?f5&uHtAxrjh|pnaQ4<9bK?m zG`gz4rI0OSfQP8ZP**gaviM616?D-F)U%ra0rWQVdMv=O2REoMt_DNZ?nrE{P!p;8 znF)KoVwsY+N=usG>Nj1oHHF~p3)u8C0Z@~IFNTm8$fyD!7EtxWQa;_J3pZofjS{wP z|8I~{0yRsroSj0aDFA;&oi$kEDvwG2W7b`o3Y!&Cqy^~)-2!m}|GKV_`Pa;T|H;I?WB*YM7!ZEy zrZPJ7uC@@O8{FE7X+1x1xk2g~XUyh-^ngi>Shj#1;@E1*;GiAFYpfvEjV2l?fXN)G z2J}b(WRXxCfWg@3zpB!iV#t{@7x`@_Aya?2KnjGAUC2&Q1TgCM6(S9CVumb9o%hc2 z#%U;{1z%6Z0oJxkXcuw3ld}$VCZlAxHWr%quL1sPT0HM?5)5AW*itx=-bFf>`o_l` zCjMzMoT*8)gZ?-v<}ebUgYJ(=z*)vs~m^kTr( zE&{OQ#ec@~(}vrLd1|uASRBULU`_R^Ejy)*nzEJ%!+Y04cnIv{$_A;ARMasCHt#x= zgZ5AY=K`4mj09=9*v_8t?5{Y{c&Mui~=aV8CFeBoZ2zfpVyK=~}hkl}tZv)LLAF7n~*9+N~ zJgV4yCHEIHG|0c#s1X4RSyoODnxsEw$h)V!Da`h)?6xij45jvCCHvt^$oWYJ#DjvG z@GunZL|`3}MW7EL6|16+wVz3$cN|J%9f%Q~3dIJgrC%iStd)l{+R@zl8VdO--}gT2~MZdjz%t) z*8E&JLC7QZr~b1XnSruM96m!;Gorel6)UHoBE4SyQ>iG?k-Ccb^@TnP(I+y>9#Hds zZ&V0eX)Nr46jICtn)so1|JdGsi}!Oy^ksY|A_Y;UNG+bWb1hDGAh1kesUp3-kXZAe ztc;Ff^pseUV{=*@jGHmrT_Uxq7Spw3W1^%8sx1`1zok8J()wcy@sLPwj@kN~6U;L5 z{#;zA^N++{g9f`wLrz;bq*+kTj0Er*LT9rlE#ktM$S@nt^qeDPWV zS&7jrR7tMh+RW>(+O+{_Is8>Jk)e#al=k)?tP|GUCde^M4-vXP_-J4XKf>h?uTShG>43}!|%D9PijFJP9u74az%ty z))uIB;MVaDsWVf35$+_vzUSyOnu#)2!*VqF|PArw z{-0yP3Tf0oXxjc{M-nnV7rjZ};P{AlomtaAN5ov;iUIzYd&M-3(px)mpYQp`#_uYa zZU+hk)2J&nK3inHeo)^S(I$aR)8Lg!S-&Yv z+4rOB-nNzjnm0V>X!|&_yNXPGG8*$z!MbPhUl zLlJE<9oY0pBc+E4py~G@`!N-_{?y0Rtb$=1r7|Ln<5g&XnV!MjcYpMqVM^wq>np zWgTbA-waICWn2Jzwm_kSRzBDeY~u0f%B#r=q*|p#T!aW(bF_c5zo~?J$K3>G`s)$$ zp6SEt#Un6BL2oAOc+!YD7zR5Oem!nUsDxqm!#fz!9cnQD&E`Ve-YJnEx6M|a0{m+G z@>X;Va=J1v!JY^hmf& z|Fa(Pwj*l4sV|)Hsw6W;@GvTJCBVGj!JU50=U+TqHhVp1x8?orkfu!9WK8TKnK?mc zSAI&B2`V}e`^ViNarkU|*W%o_?cxu44iDU!x#B9@I*fvSUIXPKml&=7g@*}(^JKvz zj<;oOtSY|a_FKnh2 zYy`^g$q_J;P0+118w3!?cqkNayQ7d0X_-^J8lqmM>JDF$#I-`<8Qas#5HunTI!@C8 z!95#^%SBL7=s(-)~rt&4%r+&0lf;;rSHDC#DS$zuMa7K`KsxGJ?ez&OHQ94uK;uZ zChmF@NI5vD37GZgLcnrtGO1R!QatPx8k7M$^R8xpKC}J?(*;$fWqF0eTIki z#_joSKBMbIyoUdYoo%|yG=hwfT#8n|?VqPxh-XHiyo}ca-E{{(Z z|55fg+NxN3Lval-e^3%3nX77}(vt)i(=Lo~=*k-=DdUnO^_rop0WAKZLSgj2amTuXcc5trp6*v;_0!733!SE`fs4s7Un~=6MDjs*YjD8FfACZ9X zS?-B95uZRfX=gPdtKujp9^RM&X>Tl0C2c!3<`sYlX}US$*_B2)5(id5UyI)9vSd1A zg9hee@U<`+)_pa4Zi8+j-a}Yiyj0j@jz>4PGuJczxuLW>(@U5dy+54eQg7W$LO00kO?O$#BGrud0Zan)uE(40Pkd180~~K{-X6V zher)*EX1je)@>-~kQ7tOaHFGPU5Y5ISJz$XX{_C?uAK?6Y^36M8+VS8Nra1cM<6)f4s*U$oOQ!Sn)-sW0MO!2?H1vNK)VT zJDx_raKH~Tc4ko#tiSBr!_#D9GP|3+beA-EK$HI~$JR25RGN44tTC!uzD@7yzZ zw`~}QTby$Jb$@>Y(Vdsb*zll(uY23%$;IN<$tnwx;js&z0)%OaUxnfYx;FR+_VsO$g+Iz zv0rb;KJYx%Ddh=w(xrNR>m%rl_suiGu{?Sk*l#;0#h%m|y+XIfKjdv@9D=tlp6Vjp zgM9;WO(hR+Vr@M!LuffL!Irr*P9KK^^OXdT_l8-YkGgF+0&g{=r+q`%$G_s$tH%CBS7csXNzF9HEHrd+G&A6sW& zxR8jb$)l$}jYOK;{@PD$J1vIRoK&u_e$GJD%e)qLkFDG*hBc%*C3E9$b)9CYX(DRU z>*?U_PBYu-0$!p`hhjBi$u6gKuSKma=ShuJw$Po7td~QEf9os&q0UG z3oy34+h<}M0Atob!tPXBo&YhH7LDgFNWhNEBO152F3=d6=Ifssvfj4@?T^47@^V{c@a&c&ig#$te188`I%aIL^EVj<#`_{{{TJi{+^ zCo=363_5+iUIjWF^R4jlZ9^S{wJrMLbqk~nJ`@8)W)t|AhZQQ2jikYccemTC#Q}Nu zd8&QYQ~Ux5Y|C27n!B6RW9FjqhhuF21l(i=Oq98z0?51ZptN1(`I3kv>S2T#dPH{uV!%=pfeMFkyZe%B)) zq*L;Gge~KC;FIb#5T8gm?H)Cc*C};C6@BZ_dv~37NrS%HsTXoLz)F28aH{zy)jxei zA2H51;@4Pva>evO6jr`s9fWBqLREO?{j~2FW(x3+TgK5lPRY8x*vbtip#JVk_6wMK zC(Qgn&aKeVn#7hw+0;lW?5a&toO9P#ypAEW9M#MVMUACY3Uc-Z_~U|t>Ou0gXiRP30wf<6_doYOb5iQ zO$+w}IxrzXm%!4pn>#&0Qy3luC2$jzz{Vt$-3pHe6L?z@m1u`M_w40}9Y-|}M(53# z#uSlGeLbeDA1Xam=2KX}Pu+o+;b9+cj)^`L-;{o0i z%y+Q5$7HcZFZ^A)0ud#>HmdL7RLJ>{k+*N{dV3XCa(yjww_KRBON?<(UKvi;8k0#T zJY%f^=`)P78OTF`3E6jkztrqtnqph7C3soUC8K{WDLJ|48^N{S_%~T;#x(Eb`eEw# z3b^_lrI4eR9(#h|Y5iFS><3ZKFCKpm_^DlRa@@K#kjoAUM4M}u=&gvS(HsFpEx zXRxYGf)l1fK(4WT_Vy&wPI+kEDCO=n7amK=H`Ut6HtG#wOi2~USEh^+EOx;l$I6Sl zEnV;X5{Pgo9E{=og6=n}`AKU5rs+A|L{xdZhGAB zxiv`*^!S|ID;}c5V>bW7_Bl}@YveYune*#gM$f-hS`0e~h(V6^`Si~gvilC@hPX5?KQ-#=LS4526@afB+(lOAiaIS7hMo<;vts{iesGd&w{U#I1a+V?a_Vaf z!6i3<35Llc_n$uDADI95qr)DCM;d zk=JZK#jdHc#?}h^B?%omBKUXR)ORn+Mo1aCjfo38(PQ^9uBw>^^?(W!6Zh+_1hZ`$`dceK4UVqcvKEiuW5_^u!o}Ywkdp#`$)ydshTM#2;ni@ z{k-X6BwGRAXf{Wz|)K*H@`C z?BvTaNadT;$dhf53WMk+2oKA~W8ee^=-4Q&Pt*9qGV*l)SM$I6MN1>c-g@m|^i3NB z=zH5P(FHt$wSCd}o5L5@In~$@V3zgv0$Tp_ELQ%!ul~>oMEF3624Ev}bV2IO=az z;GaRO#L}uBA@f#&97i_+oG-#eXC+w#=ScZN)OR5j^Kp>ug^)THbHVbh5>V}Gxmb83 z6YyLa;NGcTmGn2?xy5&*fn9IDYM6)1Gtr% zf0(gXkLSw)gb@wGP6iH0&IF1nj+7me2!Q8(2BzXH?V;N9<6D6JHVL+L(gs0K$YdKn z&A5I{LFc3W`7F=q!CNGy4Do=HR!pzy>Xc^6TR5(pBc`9tyz~a#|{pu#|gQVYurha41muYGCc4=&dL2R zPidS0&HtD#>G0FX#cSd!kroV-r0=Sj%agN1{yW{a2sMKUH};W0LgAf+Qg6(i_J)Wi zd7mVSr{5HD9J5^=OgVB7lyuXxyM&Zy+NRyFHril zvOOc9hr}rP;en6Ba~<7y`oodx7Kt~Rh;sf#sr$DFsr|be!qB*h{G_!5_bBK1a1H2p zkt?G%qf)KQc4xxNf<=YXnahUY@khnzjg)kH2)sY>Pa-g$o$Tt8d~^9)dfm*;R=EI`=1ZE$?A;{3b(9BB5^{LA$#SNt z(u0sEyY>H?x$;M-xA%V-V`miE*IUVw?Y7uzMro6ZHaA5^8!h4zqG2Y&Ek)h3bQ7b6 zx(Ov&GgA^tjksMZ8e~bt#9)}2_xtk<_xnG5{oAE;Q^C$Z0Awazt`2AVSP^w8Xph<&71+P+ z0R9W#D%_On81v2Z+!;G?byBXHN0&VqlCBDvZ%kuR5>|gi{ka%cbHQh-a@2(pIJwnC z<}J$rmo=OJM`NMqC{g)f<)t{g4kan49k?vo53U%y0 zITabB%2rgl85R}s?ss|Y+QZb&m-L|p+UHf#-|3omscQwS2O!jr99As0cI9yfgk@5F zL$ybU7vX04WFQy$bAz72U7f@f@wIGvJ|)K&-PZXeN^Knvi#8hl zWdJ|#*emF@{kbpXCRv%|zh>E1Jx<+2+Jm|?^nbmR-6{K`m^q!Xd;Pb~1HcKx;waAA z4!$Bv-n_r;>d;?z>vCQx;I<&I{=F<~F;tYUXQ$B{Q7C_q<*_C5Be9m8oQw+01E_$BU5x+EveESw@}z z^+sGg4z3@aro5bXg9XjzwLRxzy&5Ztn()5fotV&))b*>)_9J%stk=(RhfnerzR{RL zilXi9Tt>RwM=VUBG2^XgkX&pG-8&%QM*3W@?bXyDw$CR=ln|^HrDNEeKv%9WDC^eN z8!{Eug452u#BRFZ>xXYRE$;jn_ByZmqM~uT8Vwn;g?xxs#B?_Rk}{Ip~= zdc6*P+w?h+O%keMxyG{?_opeX^`G~Y>Dy*2;ur7toX76It`D`Ylo_)kRLbbui7tXa zaWZ=}fH=8OpId zq>h_B5^)FMN(-@Z>BFmBheFBRV68Bn3}+%%E-3B!B1+6j&>tDkFD89`9C3E7Ir6Md z^Ak^5Jah4oRy|bg_p{JKnbu+#Qhq!x7Auw3-ctLCsF;Ub-h0JPv1G+Onl2eH=MRad z@4q^G&b64OKeDh!MwI-VriJTJ0Y|?JX8FLwQ&SY1f5(ei)?F#qDwEEt&N;NvoiuZ0 zA1o#ovlo>H<6m~;B65vNKNpK#M#H||V6mqAM+xCDv-9aBv$i*TGQh6Dz3w;iavgn= zAJibL9nT{zcrWF0#j9Kh7?Or(-5uca$$p{leXbFXrCrBAiGlmXBY{4-LV5DdfDl|t z`ZH1q&!9%K6Eb_(7$GsP;U7H4`Xfwd2%e1UBaWem=6&avF~$lGPvO4NQHBb5hp8OS zr)^?2aaJO#PK-S6xdPk|4zI7mW&1-cf18%_yi9JgjQf6F%H`<4#X=iVm{q(OnO%-r z-z*Oq4=%34^ZG&;KR05m4fo$aZ#ig;79Wk8qOg{azbR2FKV(MhTS}``+<&7l93&+RuDlJkj=CPMnPWV$*GbA4st2Z6bXp@pWt?KT&@E66b zIzOqMS?ec5M;2WTzbt<@4{2t^Z9ggIuG?2>Ya1|;_&m0vJS`qKMNFTVZ+p-64_5QB&cSkBY3QvMS2hnZ=?qbJCUDb8;_$2Or8B_*wR)eV zcYy)wd+HAJ6RS5Hu{Oqp3MjuW&sWKi6&fTx5TU?SlREZ(unO!_+&}FzVfwu#NikY| z3tx`;yCaVkx@%s0#zM5kG@(d$9}dUKgCzn&v2;v%52<{ifrQXP6sZ~h+NOEokFW_{ zS)o`zoa-Qk%D{oMBl$SG-k5aHJ@$$so>qA(%`Y|uI&{j8DQN$TSWW^XsBgkUe7dk> zU`@DBpB(!_@3y-96m#jM5E;hy-R(0uasyd1A9UVL59pZ!-(t|2tJ&&R7&}*j_gqnm zvDC1kG_5s*QJJyR7Qkc~Gn``PG)lS%V(I%`|IFn0F*!chTN88txT+)-gIt^|`Kz{# zR#>AAJ@V+b_PYaZ#loCg{EB7Rr)!vg_REu|z>sKBw0r-@dlHKx`?-Zva;N0lUsICk zs8OL^3JL~ytt#s}GBWLwM+XHf&e0XG;08BTkW0eYiC>N{pzd%&4x2s&t=?u7eEUR2 zC*Huzns^V%#4)G7>tD~a^#QyP>{@OiuAZoZ9W=DVo}Ju&^S%t@&)yOY#3rh>6Hs#O zLJiIjKAW}OpapGn5$!H^a^ry3=}qHVJV60xcR(FBwP|$7VA-AXCh;WWEB{SEccTkR z4|_DkC9<0!LqP4e@7~vj5*~z=pFfF^W>*2F*nx;f?(lMtO<#d?b+mrT98dAC*P)>O_Y(geQGU{=a?Fb@M!&!F3}p@UKKO^f|c$B z>i>_9ZbqpGBah1OA?QehiF3t3WYEu1fSM%_bfe+NN z>n}r=8gt}AmLkRtn&FsMRSD*cB(l4`?mKbD%JTjXYVW9;g9FqXy}jwMmL$xgeSn9s538c4?sCmL%rn@@J@ zm>WP^H&4jow$xuWbB%pY-hDWKk*i79qSX{RbeuSqr58o!6Mz;G~-yANh(%A$9l6U60<+qJ-2Voj9_)6ycGa?SsB*5 zbuS#P2a+koG|ZoML}#u!f`||8VwD-UX;sr>%ARsWX`SGFnpyJ!maAOy{nf8aP zw-Kmd4)!Ww=F&OU4DYMm0i!|DPo>ydTPEeawn|a&D(cK+_`UEb?ReznDz}XsSE7RQ z4n3y0n4tnMMMKuz)FqFy;GV<}f-8y)+OmNs2P_fPgv4C>Yyu&224T0qb@+1 zTV9oZ1r$xM!-Avg@%ubd+p?uXK{cuv%s+9xd>!S7}==*Q53vismM zGL?s#i-}9H0k3M>Tk!8YV&`LWTEAZg1x4j#+s9GZ4jM=|?FIXG`%PgE$cQ|yoNbkR zm^8NFO{YLAL>}&B(B(EWp^7OJk?XuB1!VCbi4tn{M1f6lV3ijF#aQmD+Mf*CqHk{K zTE5bNQ0ho7YR-Jp%n8C%5A2`T%^vuB!gFPtyfm>QaGV}|M2$^@hX2%@V&$a{*cGkH z3>d+0u+JubCXBJ!L8u9y^DNgJ6!tHW>rrz!y~1~N5#!jPB06$8K6XTtR`k}0>J6B* zJpjnrWb<=zp|*_aElc2C(~1yEUATojE1pm}uFqnVr+c+ds{*oc`O$BR4k^BNRCOdQ z`AqV(B{D9kB=cACgA1!_#@)pY#fE!QyJgRQ<%=x)g@sbzTkA5qfc=l@sfg0{c2%-q zcd$s*H2ry~=8C;cXfl30*N6$sO$)$je6b35&1b%7{8>6`1RzRCwgoqvv87mF8%dsnt{NtC3YB`k@JU1&=hm zFox3iqWAaVe&Pw~d)~2A16vvPgM$S`^a-#eSFRTc&s^O<$su$Z0_Wmptz51xg|Pvu zInWRG(r0`RX{wB?9aXPND(;A^i+5fCEj_#cqnt z{FGr;RO-gPTA@eP1Nx#ArD9VMcl4@EVr5~c!q0@Lqgm=G@_3f{_QzR{l`cb5IiXeb z?7p!}a_Do6V!|VUNrn-3XZ?xnNqkY>gsGKbww%jt>jg4T{h7w$6@i0gwIuc;4WxKQ zvjDc`{rKqzPcAzsFc#=kKB%#h*S6TC5L!NZpsY40p2a4y??BGWn)wovu`NUE_wK4z zMb?Gh&rU0A9#YMSoEi@Zkhei$JJFtYW$3;`_TBZbgmBo`ZY5+SOQ)!Y_)T|yu5WV| zn(_&7`!R@bf&CAVXWvkHj-?Y(->7RJcVt*eh&0@#s3u3ZPlwGoTpHwqwy%hUHiR9` zbBP_>ayFT3sr=-}f#2|7hUr-8)Kau!JKE#2cNN*qh*ftA9)eOwR&pNH<4Kf$at}tMm2^KXX^g_==di87Ue*8Zu+!sll-vY(_Ajhflp7 zrF?uXM}4b-jsU6?j7jH@R&P%J>ndV8x?TZ}C*W)H!-0afN;I{%#c!s%{TalHgvqRc zDMNHJ53sb|MvMxT(d^_~E>CsHHh8IMnx26@db$=yVp zF@97ZFwj8yq5)K8M%gRw{Vm9#{&gIl!4OW#9 z<02Qw4H5$8!f68Z^6{NwY?8JrU~Vu+dU<%&Y;P0X{Kb9Kr(^%)Id8NJ{f~j#8wxLx zbo1I_$P5NSNgZtL&9nmsuLX#r!)|BA1LMC#DyZt`QhRCo|JUGHZ|97E+kOE|14HwY zHb(cDV^Wt8WheOxU2iJlP|6d`b#rX%jmr)Q=0>(^VInASA@SOhgh{!9D*u?I1_A-( z%{g9f9Bc;A_~z&0AJhKtT1sYTV~cq?9vzzruLVd`Opg=38L#aCfJW@ah8PO*|SEY z$1j`9nQPY>94BWp*PKbj|J#sEn)yqd78?od-?NK29r_1_RaL?V*8>=epXyInJ=|Ah zErRKASqvbRh**20mT_Z*FxE3?iyiuZys(X4Y%Ha)$>oJpyMwm=34LW%SvgDp*>B%| z6ux#fWGouDT~Ria_4lgWu7!gvDMrp{@=v;C9*t86mtj_~AcV&5+2Pxt>PTk2;r1(0 z&f~WdGIAF~y|1iJi}yv+Q5u56MXoLt}w`?!1Y3TcWu-UH8H?)V{uyD= zWhW_0YpkrpCxztM@UeeJUrK(KrwS9r+ynt@uAzdZHdq-&E<&;{y}Q!2a1cQtv>QV8 z5I|!VZ<2vkWj0i#g^y0?%=YA=ELNW^ph(u|^1i4V0o`)$HzPXMh0mAl(&k4;`JCw*zi}NU-^i z?&logzR`+FZop42Cf_bu|K?O(VQ`OR>pHExDP(#4q(7XSuGlB-Iq=~R$uxdz9`3+r zo5GsjiBlyb;|D~H;Iq3VC+`u<1imoHrDfQJ<7{L3x>)_>w4~_&$3N~@$_>2eQ*+Ac QEJ^p=)^A%^xRx6CKVa;B(EtDd literal 0 HcmV?d00001 diff --git a/nebula_app/src/display/chrome.rs b/nebula_app/src/display/chrome.rs index b724cfc..e3d6698 100644 --- a/nebula_app/src/display/chrome.rs +++ b/nebula_app/src/display/chrome.rs @@ -857,6 +857,7 @@ pub(super) fn draw_chrome(d: &mut Display) { .iter() .map(|tab| crate::motion::Spring::new(tab.1).with_response(0.14)) .collect(); + d.nebula_tab_was_visible = tab_layout.tabs.iter().map(|tab| tab.2 > 0.0).collect(); } let mut tab_anim_active = false; @@ -865,6 +866,7 @@ pub(super) fn draw_chrome(d: &mut Display) { // their eased position so they don't fly in when they reappear. if tab_w <= 0.0 { d.nebula_tab_anim[index].snap_to(row_y); + d.nebula_tab_was_visible[index] = false; continue; } let target_y = d.tab_drag_draw_y(index, row_y, &tab_layout); @@ -872,16 +874,26 @@ pub(super) fn draw_chrome(d: &mut Display) { // laggy); only the rows making way ease toward their slots. let dragging_this = d.nebula_tab_drag.as_ref().is_some_and(|d| d.active && d.source == index); - let tab_y = if dragging_this { - d.nebula_tab_anim[index].snap_to(target_y); - target_y - } else { - let spring = &mut d.nebula_tab_anim[index]; - spring.set_target(target_y, crate::motion::MotionPolicy::Full); - spring.step(motion_frame); - tab_anim_active |= spring.is_active(); - spring.value() + let action = tab_reveal_motion_action( + d.nebula_tab_reveal_motion, + d.nebula_tab_was_visible[index], + true, + dragging_this, + ); + let tab_y = match action { + TabRevealMotionAction::Snap => { + d.nebula_tab_anim[index].snap_to(target_y); + target_y + }, + TabRevealMotionAction::Spring => { + let spring = &mut d.nebula_tab_anim[index]; + spring.set_target(target_y, crate::motion::MotionPolicy::Full); + spring.step(motion_frame); + tab_anim_active |= spring.is_active(); + spring.value() + }, }; + d.nebula_tab_was_visible[index] = true; let tab_hovered = matches!( d.nebula_chrome_hover, ChromeHit::Tab(i) | ChromeHit::TabClose(i) if i == index @@ -1407,22 +1419,31 @@ pub(super) fn draw_chrome(d: &mut Display) { let color = if index == d.nebula_active_tab || tab_hovered { TXT_ON_ACCENT } else { TXT }; let cy = row_text_cy(draw_row_y, tab_h); - // Real AI brand logo (claude/codex): a textured quad in the - // icon slot, sized to the glyph ink height so it reads like + // Real AI brand logo: a textured quad in the icon slot, sized + // to the glyph ink height so it reads like // an icon, not a sticker. Staged here, drawn after ALL chrome // text (see nebula_chrome_logo_draws). Other programs keep // their Nerd Font glyph inside the label text. let mut text_x = tab_x + s(14.0) + hover_lift_x; let mut reserved = s(60.0); if let Some(logo) = d.nebula_tab_logos.get(index).copied().flatten() { - let icon_s = (cell_h * 0.72).round(); - if let Some((id, rgba, px)) = d.ai_logo_pixels(logo, color) { + let standard_icon_s = (cell_h * 0.72).round(); + let icon_s = if logo == super::AiLogo::Grok { + (cell_h * 0.92).round() + } else { + standard_icon_s + }; + if let Some((id, rgba, px)) = d.ai_logo_pixels(logo, color, icon_s as u32) { + // Grok is intentionally larger for legibility. Keep the + // standard icon slot's centre instead of growing only to + // the right from its left edge. + let icon_x = text_x - (icon_s - standard_icon_s) / 2.0; let icon_y = (draw_row_y + (tab_h - icon_s) / 2.0).round(); d.nebula_chrome_logo_draws.push(( id, rgba, px, - (text_x, icon_y, icon_s, icon_s), + (icon_x, icon_y, icon_s, icon_s), )); } text_x += icon_s + s(6.0); @@ -1881,13 +1902,36 @@ pub(super) fn draw_chrome(d: &mut Display) { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TabRevealMotionAction { + Snap, + Spring, +} + +fn tab_reveal_motion_action( + preference: super::settings::TabRevealMotion, + was_visible: bool, + is_visible: bool, + dragging: bool, +) -> TabRevealMotionAction { + if dragging + || (is_visible && !was_visible && preference == super::settings::TabRevealMotion::Instant) + { + TabRevealMotionAction::Snap + } else { + TabRevealMotionAction::Spring + } +} + #[cfg(test)] mod resize_edge_tests { use super::{ ChromeHit, advance_spinner_phase, chrome_control_centers, contains_rect, in_chrome_bar, - resize_edge, spinner_dot_center, window_control_hit_rect, window_control_visual_rect, + resize_edge, spinner_dot_center, tab_reveal_motion_action, window_control_hit_rect, + window_control_visual_rect, }; use crate::display::SizeInfo; + use crate::display::settings::TabRevealMotion; #[test] fn maximized_or_fullscreen_windows_do_not_expose_drag_resize_edges() { @@ -1952,4 +1996,24 @@ mod resize_edge_tests { assert!((half_turn - 0.5).abs() < f32::EPSILON); assert!((wrapped - 0.01).abs() < 0.000_001); } + + #[test] + fn instant_reveal_snaps_only_when_a_hidden_tab_becomes_visible() { + assert_eq!( + tab_reveal_motion_action(TabRevealMotion::Instant, false, true, false), + super::TabRevealMotionAction::Snap + ); + assert_eq!( + tab_reveal_motion_action(TabRevealMotion::Slide, false, true, false), + super::TabRevealMotionAction::Spring + ); + assert_eq!( + tab_reveal_motion_action(TabRevealMotion::Instant, true, true, false), + super::TabRevealMotionAction::Spring + ); + assert_eq!( + tab_reveal_motion_action(TabRevealMotion::Instant, true, true, true), + super::TabRevealMotionAction::Snap + ); + } } diff --git a/nebula_app/src/display/mod.rs b/nebula_app/src/display/mod.rs index 5b2ff84..f56ba44 100644 --- a/nebula_app/src/display/mod.rs +++ b/nebula_app/src/display/mod.rs @@ -512,10 +512,14 @@ pub enum AiLogo { OpenCode, /// Pi's block-built `pi` mark from pi.dev. Pi, + /// xAI's Grok mark. + Grok, } -/// Official logo assets (Wikimedia SVG renders, 64 px, alpha-transparent). +/// Embedded brand logo assets, kept at their source pixel dimensions. const AI_LOGO_CLAUDE_PNG: &[u8] = include_bytes!("../../../extra/logo/ai_claude.png"); +const AI_LOGO_GROK_DARK_PNG: &[u8] = include_bytes!("../../../extra/logo/ai_grok_dark.png"); +const AI_LOGO_GROK_LIGHT_PNG: &[u8] = include_bytes!("../../../extra/logo/ai_grok_light.png"); const AI_LOGO_OPENAI_PNG: &[u8] = include_bytes!("../../../extra/logo/ai_openai.png"); /// opencode's mark, rasterized from their `favicon.svg`. RGB carries luma /// (frame=255, block=90), alpha the shape; tinted `ink × luma/255` at runtime. @@ -526,6 +530,58 @@ const AI_LOGO_PI_PNG: &[u8] = include_bytes!("../../../extra/logo/ai_pi.png"); /// (which starts at 1), so the two id spaces can share the renderer cache. const AI_LOGO_ID_BASE: u64 = 1 << 62; +fn prepare_ai_logo_texture( + rgba: &[u8], + width: u32, + height: u32, + target_size: u32, +) -> (Vec, u32, u32) { + use image::imageops::FilterType; + + let target_size = target_size.max(1); + let source = image::RgbaImage::from_raw(width, height, rgba.to_vec()) + .expect("decoded logo dimensions must match its RGBA buffer"); + let resized = image::imageops::resize(&source, target_size, target_size, FilterType::Lanczos3); + let pixels = resized.as_raw(); + let (mass, weighted_x, weighted_y) = pixels.chunks_exact(4).enumerate().fold( + (0.0, 0.0, 0.0), + |(mass, weighted_x, weighted_y), (index, pixel)| { + let alpha = f64::from(pixel[3]); + let x = (index as u32 % target_size) as f64 + 0.5; + let y = (index as u32 / target_size) as f64 + 0.5; + (mass + alpha, weighted_x + x * alpha, weighted_y + y * alpha) + }, + ); + if mass == 0.0 { + return (resized.into_raw(), target_size, target_size); + } + + let center = f64::from(target_size) / 2.0; + let shift_x = (center - weighted_x / mass).round() as i32; + let shift_y = (center - weighted_y / mass).round() as i32; + if shift_x == 0 && shift_y == 0 { + return (resized.into_raw(), target_size, target_size); + } + + let mut centered = vec![0; pixels.len()]; + for y in 0..target_size as i32 { + for x in 0..target_size as i32 { + let dest_x = x + shift_x; + let dest_y = y + shift_y; + if !(0..target_size as i32).contains(&dest_x) + || !(0..target_size as i32).contains(&dest_y) + { + continue; + } + let source_index = ((y as u32 * target_size + x as u32) * 4) as usize; + let dest_index = ((dest_y as u32 * target_size + dest_x as u32) * 4) as usize; + centered[dest_index..dest_index + 4] + .copy_from_slice(&pixels[source_index..source_index + 4]); + } + } + (centered, target_size, target_size) +} + /// Real-logo mapping for AI clients; everything else falls back to the /// [`program_icon`] glyph. Gated on PNG support: without it there is no /// texture to draw and the glyph must stay. @@ -538,6 +594,7 @@ pub(crate) fn ai_logo(program: &str) -> Option { "codex" => Some(AiLogo::OpenAi), "opencode" => Some(AiLogo::OpenCode), "pi" => Some(AiLogo::Pi), + "grok" | "grok-cli" => Some(AiLogo::Grok), _ => None, } } @@ -1033,6 +1090,7 @@ pub struct Display { /// Per-displayed-tab animated draw-x, eased toward the laid-out / drag /// target each frame so tab reorder "make way" slides instead of snapping. nebula_tab_anim: Vec, + nebula_tab_was_visible: Vec, /// Active scrollbar drag: the pointer's y-offset inside the thumb captured /// at press time, so the thumb tracks the pointer without jumping. pub nebula_scrollbar_drag: Option, @@ -1115,12 +1173,15 @@ pub struct Display { nebula_tab_bells: Vec, /// Per-tab "command is running" flags driving the sidebar spinners. nebula_tab_running: Vec, - /// Per-tab real AI brand logo (claude/codex), textured over the icon slot. + /// Per-tab real AI brand logo, textured over the icon slot. nebula_tab_logos: Vec>, - /// Decoded (+theme-tinted) logo pixels with stable renderer texture ids, - /// keyed by (logo, ink). Decode and tint run once per key. - nebula_ai_logo_cache: - std::collections::HashMap<(AiLogo, [u8; 3]), (u64, std::sync::Arc>, (u32, u32))>, + /// Decoded (and, where appropriate, theme-tinted) logo pixels with stable renderer texture ids, + /// keyed by (logo, ink, target physical size). Decode, tint and high-quality + /// downsampling run once per key. + nebula_ai_logo_cache: std::collections::HashMap< + (AiLogo, [u8; 3], u32), + (u64, std::sync::Arc>, (u32, u32)), + >, /// Decoded shell icons (full-color PNGs) with stable texture ids, keyed by /// shell id (pwsh/cmd/nu/wsl:Ubuntu). Decode runs once per id. nebula_shell_icon_cache: @@ -1196,6 +1257,7 @@ pub struct Display { pub nebula_cursor_blink: bool, /// 交互: WT-style copyOnSelect. Off = right-click copies / pastes instead. pub nebula_copy_on_select: bool, + pub nebula_tab_reveal_motion: settings::TabRevealMotion, pub nebula_font_family: String, nebula_font_families: Vec, nebula_font_notice: Option, @@ -1589,6 +1651,7 @@ impl Display { directory_history: crate::directory_history::global(), nebula_commands: nebula_commands_handle(), nebula_tab_anim: Vec::new(), + nebula_tab_was_visible: vec![true], nebula_scrollbar_drag: None, nebula_split_reveal: None, nebula_confirm: required_font_install, @@ -1631,6 +1694,7 @@ impl Display { nebula_cursor_shape: settings_init.cursor_shape, nebula_cursor_blink: settings_init.cursor_blink, nebula_copy_on_select: settings_init.copy_on_select, + nebula_tab_reveal_motion: settings_init.tab_reveal, nebula_font_family: settings_init.font_family, nebula_font_families, nebula_font_notice: None, @@ -2280,20 +2344,27 @@ impl Display { &mut self, logo: AiLogo, ink: Rgb, + target_size: u32, ) -> Option<(u64, std::sync::Arc>, (u32, u32))> { - // Claude's mark ships in Anthropic coral and is used as-is (only its - // alpha matters), so its cache key ignores the ink. The OpenAI mark - // is black-on-alpha and follows the chrome ink to stay visible on - // every theme. + // Claude and Grok keep their source colors. Grok ships official dark + // and light marks, selected to match the chrome ink without tinting. + let grok_uses_light_mark = + u32::from(ink.r) * 299 + u32::from(ink.g) * 587 + u32::from(ink.b) * 114 >= 128_000; let key = match logo { - AiLogo::Claude => (logo, [0, 0, 0]), - AiLogo::OpenAi | AiLogo::OpenCode | AiLogo::Pi => (logo, [ink.r, ink.g, ink.b]), + AiLogo::Claude => (logo, [0, 0, 0], target_size), + AiLogo::Grok if grok_uses_light_mark => (logo, [255, 255, 255], target_size), + AiLogo::Grok => (logo, [0, 0, 0], target_size), + AiLogo::OpenAi | AiLogo::OpenCode | AiLogo::Pi => { + (logo, [ink.r, ink.g, ink.b], target_size) + }, }; if let Some(cached) = self.nebula_ai_logo_cache.get(&key) { return Some(cached.clone()); } let bytes: &[u8] = match logo { AiLogo::Claude => AI_LOGO_CLAUDE_PNG, + AiLogo::Grok if grok_uses_light_mark => AI_LOGO_GROK_LIGHT_PNG, + AiLogo::Grok => AI_LOGO_GROK_DARK_PNG, AiLogo::OpenAi => AI_LOGO_OPENAI_PNG, AiLogo::OpenCode => AI_LOGO_OPENCODE_PNG, AiLogo::Pi => AI_LOGO_PI_PNG, @@ -2323,8 +2394,13 @@ impl Display { px[2] = (ink.b as u16 * luma / 255) as u8; } }, - AiLogo::Claude => {}, + AiLogo::Claude | AiLogo::Grok => {}, } + let (rgba, width, height) = if logo == AiLogo::Grok { + prepare_ai_logo_texture(&rgba, width, height, target_size) + } else { + (rgba, width, height) + }; let id = AI_LOGO_ID_BASE + self.nebula_ai_logo_cache.len() as u64; let entry = (id, std::sync::Arc::new(rgba), (width, height)); self.nebula_ai_logo_cache.insert(key, entry.clone()); @@ -2669,6 +2745,7 @@ impl Display { cursor_shape: self.nebula_cursor_shape, cursor_blink: self.nebula_cursor_blink, copy_on_select: self.nebula_copy_on_select, + tab_reveal: self.nebula_tab_reveal_motion, preview_bg: self.preview_terminal_bg(), preview_fg: { let bg = self.preview_terminal_bg(); @@ -2885,6 +2962,15 @@ impl Display { self.pending_update.dirty = true; } + pub fn set_tab_reveal_option(&mut self, index: usize) { + if let Some(motion) = settings::TAB_REVEAL_OPTIONS.get(index) { + self.nebula_tab_reveal_motion = *motion; + self.persist_nebula_settings(); + } + self.nebula_settings_dropdown = None; + self.pending_update.dirty = true; + } + /// Returns true when the default cursor style changed (the caller then /// pushes the new default into every live terminal). pub fn set_cursor_shape_option(&mut self, index: usize) -> bool { @@ -3990,6 +4076,7 @@ impl Display { cursor_shape: self.nebula_cursor_shape, cursor_blink: self.nebula_cursor_blink, copy_on_select: self.nebula_copy_on_select, + tab_reveal: self.nebula_tab_reveal_motion, theme: self.nebula_theme_preference, follow_system_theme: self.nebula_follow_system_theme, pinned_hosts: self.nebula_pinned_hosts.clone(), @@ -4060,6 +4147,7 @@ impl Display { self.nebula_fetch_enabled = settings.fetch; self.nebula_powerline_enabled = settings.powerline; self.nebula_keep_session = settings.keep_session; + self.nebula_tab_reveal_motion = settings.tab_reveal; self.nebula_window_opacity = settings.opacity; self.nebula_background = if settings.follow_system_theme { Some(active_theme.palette().term_bg) @@ -6903,15 +6991,102 @@ mod nebula_ux_tests { use winit::window::Theme as WinitTheme; use super::{ - NebulaConfirm, SizeInfo, alt_screen_vertical_padding_bands, nebula_command_hint, - remove_ssh_host_from_lists, replays_untrusted_terminal_output, restore_ssh_host_to_lists, - system_theme_snapshot, + AI_LOGO_GROK_DARK_PNG, AI_LOGO_GROK_LIGHT_PNG, AiLogo, NebulaConfirm, SizeInfo, ai_logo, + alt_screen_vertical_padding_bands, extract_program, nebula_command_hint, + prepare_ai_logo_texture, program_icon, remove_ssh_host_from_lists, + replays_untrusted_terminal_output, restore_ssh_host_to_lists, system_theme_snapshot, }; fn strings(values: &[&str]) -> Vec { values.iter().map(|value| (*value).to_owned()).collect() } + fn logo_for_command(command: &str) -> Option { + extract_program(command).as_deref().and_then(ai_logo) + } + + #[test] + fn running_program_identity_normalizes_grok_without_changing_fallbacks() { + for command in [ + "grok", + "grok-cli", + "GROK.CMD", + "C:\\Tools\\GROK-CLI.EXE --help", + "/usr/local/bin/grok", + ] { + assert_eq!(logo_for_command(command), Some(AiLogo::Grok), "{command}"); + } + + assert_eq!(logo_for_command("codex"), Some(AiLogo::OpenAi)); + assert_eq!(logo_for_command("C:\\Tools\\CLAUDE.EXE --resume"), Some(AiLogo::Claude)); + assert_eq!(program_icon("cargo"), "\u{e7a8}"); + assert_eq!(logo_for_command("unlisted-program"), None); + assert_eq!(program_icon("unlisted-program"), "\u{f04b}"); + assert_eq!(extract_program(""), None); + } + + #[cfg(all(feature = "png", not(target_os = "macos")))] + #[test] + fn official_grok_logos_are_embedded_unchanged_and_decodable() { + use sha2::{Digest, Sha256}; + + let assets = [ + ( + AI_LOGO_GROK_DARK_PNG, + [ + 0x37, 0xdd, 0xbc, 0xb6, 0xe2, 0xa7, 0xf2, 0xe4, 0xb3, 0xbe, 0x78, 0xa7, 0xd4, + 0x12, 0x96, 0xa3, 0xbc, 0x7e, 0xdf, 0x69, 0x26, 0x36, 0x24, 0x34, 0xef, 0xc0, + 0x0d, 0xf5, 0xa5, 0x6a, 0x35, 0x86, + ], + ), + ( + AI_LOGO_GROK_LIGHT_PNG, + [ + 0x35, 0x90, 0x56, 0xee, 0x89, 0x83, 0xcf, 0xa0, 0xba, 0x7e, 0x72, 0x79, 0x50, + 0x78, 0xc7, 0xc0, 0xdd, 0xf6, 0xc5, 0xd7, 0xa1, 0x87, 0x04, 0x01, 0xab, 0x96, + 0x0e, 0xd4, 0xf9, 0xdf, 0x9e, 0x53, + ], + ), + ]; + + for (png, expected_sha256) in assets { + assert_eq!(Sha256::digest(png).as_slice(), expected_sha256); + let (width, height, rgba) = crate::renderer::image::decode_png_bytes(png).unwrap(); + assert_eq!((width, height), (1024, 1024)); + assert!(rgba.chunks_exact(4).any(|pixel| pixel[3] != 0)); + } + } + + #[cfg(all(feature = "png", not(target_os = "macos")))] + #[test] + fn official_grok_logos_prepare_sharp_optically_centered_physical_textures() { + for png in [AI_LOGO_GROK_DARK_PNG, AI_LOGO_GROK_LIGHT_PNG] { + let (width, height, rgba) = crate::renderer::image::decode_png_bytes(png).unwrap(); + let (rgba, width, height) = prepare_ai_logo_texture(&rgba, width, height, 18); + + assert_eq!((width, height, rgba.len()), (18, 18, 18 * 18 * 4)); + let alpha_levels = rgba + .chunks_exact(4) + .map(|pixel| pixel[3]) + .filter(|alpha| *alpha > 0) + .collect::>(); + assert!(alpha_levels.len() >= 8); + + let (mass, weighted_x, weighted_y) = rgba.chunks_exact(4).enumerate().fold( + (0.0, 0.0, 0.0), + |(mass, weighted_x, weighted_y), (index, pixel)| { + let alpha = f64::from(pixel[3]); + let x = (index as u32 % width) as f64 + 0.5; + let y = (index as u32 / width) as f64 + 0.5; + (mass + alpha, weighted_x + x * alpha, weighted_y + y * alpha) + }, + ); + let center = f64::from(width) / 2.0; + assert!((weighted_x / mass - center).abs() <= 0.5); + assert!((weighted_y / mass - center).abs() <= 0.5); + } + } + #[test] fn log_replay_commands_do_not_receive_terminal_query_answers() { for command in [ diff --git a/nebula_app/src/display/settings.rs b/nebula_app/src/display/settings.rs index ef4b1c1..ce28645 100644 --- a/nebula_app/src/display/settings.rs +++ b/nebula_app/src/display/settings.rs @@ -105,6 +105,7 @@ pub enum SettingsDropdown { Language, Accept, CursorShape, + TabReveal, /// 背景色:色板网格 + 16 进制输入的专用浮层(不是通用行列表)。 BackgroundColor, } @@ -151,10 +152,44 @@ pub(super) const LANGUAGE_OPTIONS: [LanguagePreference; 3] = pub(super) const ACCEPT_OPTIONS: [AcceptKey; 3] = [AcceptKey::Both, AcceptKey::Tab, AcceptKey::Right]; +pub(super) const TAB_REVEAL_OPTIONS: [TabRevealMotion; 2] = + [TabRevealMotion::Slide, TabRevealMotion::Instant]; + /// Order mirrors the Windows Terminal appearance page the user referenced. pub(super) const CURSOR_SHAPE_OPTIONS: [CursorShape; 4] = [CursorShape::Beam, CursorShape::Underline, CursorShape::Block, CursorShape::HollowBlock]; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) enum TabRevealMotion { + #[default] + Slide, + Instant, +} + +impl TabRevealMotion { + fn parse(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "slide" => Some(Self::Slide), + "instant" => Some(Self::Instant), + _ => None, + } + } + + fn settings_value(self) -> &'static str { + match self { + Self::Slide => "slide", + Self::Instant => "instant", + } + } +} + +fn tab_reveal_label(motion: TabRevealMotion, language: UiLanguage) -> &'static str { + match motion { + TabRevealMotion::Slide => language.pick("滑动", "Slide"), + TabRevealMotion::Instant => language.pick("立即", "Instant"), + } +} + pub(super) fn cursor_shape_label(shape: CursorShape, language: UiLanguage) -> &'static str { match shape { CursorShape::Beam => language.pick("条形(│)", "Bar (│)"), @@ -210,6 +245,8 @@ pub enum SettingsHit { CursorBlinkToggle, /// 交互: copy-on-select toggle row. CopyOnSelectToggle, + TabRevealDropdown, + TabRevealOption(usize), /// Language combobox trigger (options resolve to [`SettingsHit::Language`]). LanguageDropdown, /// Expanded dropdown option rows for the cycle-style settings. @@ -266,6 +303,7 @@ pub(super) struct NebulaRuntimeSettings { /// 交互: selecting text copies it to the clipboard immediately (WT's /// copyOnSelect). Off = right-click copies instead. pub(super) copy_on_select: bool, + pub(super) tab_reveal: TabRevealMotion, pub(super) fetch: bool, pub(super) powerline: bool, /// Window close keeps the PTYs alive in the resident process (detach / @@ -314,6 +352,7 @@ pub(super) fn nebula_settings_load(config: &UiConfig) -> NebulaRuntimeSettings { cursor_shape: CursorShape::Beam, cursor_blink: true, copy_on_select: true, + tab_reveal: TabRevealMotion::Slide, // Off by default: the welcome screen pipes a whole script through the // fresh shell and repaints on resize — real startup-latency cost on // the critical path (user ruling: startup speed outranks the art). @@ -386,6 +425,9 @@ pub(super) fn nebula_settings_load(config: &UiConfig) -> NebulaRuntimeSettings { }, Some(("cursor_blink", v)) => settings.cursor_blink = parse_bool(v, true), Some(("copy_on_select", v)) => settings.copy_on_select = parse_bool(v, true), + Some(("tab_reveal", v)) => { + settings.tab_reveal = TabRevealMotion::parse(v).unwrap_or_default(); + }, Some(("startup_directory", v)) => { let path = std::path::PathBuf::from(v.trim()); if path.is_dir() { @@ -559,7 +601,7 @@ pub(super) fn nebula_settings_write(settings: &NebulaRuntimeSettings) { let _ = std::fs::write( path, format!( - "language={}\ntheme={theme}\nfollow_system_theme={}\nghost={}\naccept={accept}\nshell={shell}\nstartup_directory={startup_directory}\nfont_family={}\nfont_size={font_size}\ncursor_shape={}\ncursor_blink={}\ncopy_on_select={}\nfetch={}\npowerline={}\nkeep_session={}\nopacity={:.2}\nbackground={background}\nbackground_image={background_image}\nbackground_image_opacity={:.2}\nbackground_image_fit={}\nbackground_image_alignment={}\nbackground_image_cover_chrome={}\npinned_hosts={pinned_hosts}\nsaved_hosts={saved_hosts}\nhidden_hosts={hidden_hosts}\n", + "language={}\ntheme={theme}\nfollow_system_theme={}\nghost={}\naccept={accept}\nshell={shell}\nstartup_directory={startup_directory}\nfont_family={}\nfont_size={font_size}\ncursor_shape={}\ncursor_blink={}\ncopy_on_select={}\ntab_reveal={}\nfetch={}\npowerline={}\nkeep_session={}\nopacity={:.2}\nbackground={background}\nbackground_image={background_image}\nbackground_image_opacity={:.2}\nbackground_image_fit={}\nbackground_image_alignment={}\nbackground_image_cover_chrome={}\npinned_hosts={pinned_hosts}\nsaved_hosts={saved_hosts}\nhidden_hosts={hidden_hosts}\n", settings.language.as_str(), settings.follow_system_theme as u8, settings.ghost as u8, @@ -567,6 +609,7 @@ pub(super) fn nebula_settings_write(settings: &NebulaRuntimeSettings) { cursor_shape_settings_value(settings.cursor_shape), settings.cursor_blink as u8, settings.copy_on_select as u8, + settings.tab_reveal.settings_value(), settings.fetch as u8, settings.powerline as u8, settings.keep_session as u8, @@ -623,6 +666,7 @@ struct SettingsGeometry { background_image_opacity_slider: (f32, f32, f32, f32), /// 交互: copy-on-select toggle row. copy_on_select: (f32, f32, f32, f32), + tab_reveal: (f32, f32, f32, f32), reset: (f32, f32, f32, f32), /// Top edge of the scrollable content viewport (just below the fixed /// header band); everything above it never scrolls. @@ -781,10 +825,11 @@ fn settings_geometry( }; let profiles_h = s(profiles_end + 32.0 - 72.0); - // 交互: a single copy-on-select toggle row for now. + // 交互: clipboard behaviour and tab-section reveal motion. let interaction_y0 = 146.0; - let interaction_h = s(interaction_y0 + ROW_H + 32.0 - 72.0); + let interaction_h = s(interaction_y0 + ROW_H * 2.0 + 32.0 - 72.0); let copy_on_select = (row_x, at(interaction_y0), row_w, row_h); + let tab_reveal = (row_x, at(interaction_y0 + ROW_H), row_w, row_h); // Keymap: one contiguous group of read-only shortcut rows. let keymap_y0 = 146.0; @@ -849,6 +894,7 @@ fn settings_geometry( hidden_host_row0: (row_x, at(hidden_y0), row_w, row_h), hidden_host_count, copy_on_select, + tab_reveal, reset: (popup_x + popup_w - s(170.0), popup_y + s(24.0), s(150.0), s(42.0)), content_top, appearance_h, @@ -962,6 +1008,9 @@ fn dropdown_anchor( (Section::Profiles, SettingsDropdown::Accept) => { Some((anchor(geometry.accept), ACCEPT_OPTIONS.len())) }, + (Section::Interaction, SettingsDropdown::TabReveal) => { + Some((anchor(geometry.tab_reveal), TAB_REVEAL_OPTIONS.len())) + }, (Section::Appearance, SettingsDropdown::BackgroundFit) => { Some((anchor(geometry.background_image_fit), BACKGROUND_FIT_OPTIONS.len())) }, @@ -1049,6 +1098,7 @@ pub fn settings_hit( SettingsDropdown::BackgroundAlignment => SettingsHit::AlignOption(index), SettingsDropdown::Language => SettingsHit::Language(LANGUAGE_OPTIONS[index]), SettingsDropdown::Accept => SettingsHit::AcceptOption(index), + SettingsDropdown::TabReveal => SettingsHit::TabRevealOption(index), SettingsDropdown::CursorShape => SettingsHit::CursorShapeOption(index), // 背景色浮层在上方特判处理,走不到通用行列表。 SettingsDropdown::BackgroundColor => SettingsHit::Panel, @@ -1168,6 +1218,9 @@ pub fn settings_hit( if contains_rect(geometry.copy_on_select, x, y) { return SettingsHit::CopyOnSelectToggle; } + if contains_rect(geometry.tab_reveal, x, y) { + return SettingsHit::TabRevealDropdown; + } }, NebulaSettingsSection::Keymap => {}, NebulaSettingsSection::Advanced => { @@ -1225,6 +1278,7 @@ pub(super) struct SettingsView { pub(super) cursor_shape: CursorShape, pub(super) cursor_blink: bool, pub(super) copy_on_select: bool, + pub(super) tab_reveal: TabRevealMotion, /// Live-preview colors: the ACTUAL terminal background/foreground the /// grid would use right now (custom background wins over the theme). pub(super) preview_bg: Rgb, @@ -1268,6 +1322,9 @@ fn dropdown_selected_index(view: &SettingsView, dropdown: SettingsDropdown) -> O LANGUAGE_OPTIONS.iter().position(|preference| *preference == view.language_preference) }, SettingsDropdown::Accept => ACCEPT_OPTIONS.iter().position(|key| *key == view.accept), + SettingsDropdown::TabReveal => { + TAB_REVEAL_OPTIONS.iter().position(|motion| *motion == view.tab_reveal) + }, SettingsDropdown::CursorShape => { CURSOR_SHAPE_OPTIONS.iter().position(|shape| *shape == view.cursor_shape) }, @@ -1287,6 +1344,7 @@ fn dropdown_hover_index(hover: SettingsHit, dropdown: SettingsDropdown) -> Optio LANGUAGE_OPTIONS.iter().position(|option| *option == preference) }, (SettingsDropdown::Accept, SettingsHit::AcceptOption(index)) => Some(index), + (SettingsDropdown::TabReveal, SettingsHit::TabRevealOption(index)) => Some(index), (SettingsDropdown::CursorShape, SettingsHit::CursorShapeOption(index)) => Some(index), _ => None, } @@ -1757,13 +1815,21 @@ pub(super) fn push_quads( } }, NebulaSettingsSection::Interaction => { - group_frame(quads, geometry.copy_on_select, 1); + group_frame(quads, geometry.copy_on_select, 2); row_hover( quads, geometry.copy_on_select, view.hover == SettingsHit::CopyOnSelectToggle, ); toggle(quads, &mut staged, geometry.copy_on_select, view.copy_on_select); + row_hover(quads, geometry.tab_reveal, view.hover == SettingsHit::TabRevealDropdown); + combobox( + quads, + &mut staged, + geometry.tab_reveal, + view.hover == SettingsHit::TabRevealDropdown, + view.dropdown == Some(SettingsDropdown::TabReveal), + ); }, NebulaSettingsSection::Keymap => { group_frame(quads, geometry.keymap_row0, KEYMAP_ROWS.len()); @@ -2014,6 +2080,9 @@ pub(super) fn draw_popup_text( language_label(LANGUAGE_OPTIONS[index], language).to_owned() }, SettingsDropdown::Accept => accept_label(ACCEPT_OPTIONS[index], language).to_owned(), + SettingsDropdown::TabReveal => { + tab_reveal_label(TAB_REVEAL_OPTIONS[index], language).to_owned() + }, SettingsDropdown::CursorShape => { cursor_shape_label(CURSOR_SHAPE_OPTIONS[index], language).to_owned() }, @@ -2874,6 +2943,26 @@ pub(super) fn draw_text( sk.ink, ); } + if visible(geometry.tab_reveal.1, geometry.tab_reveal.3) { + row_label( + r, + gc, + size, + scale, + &sk, + geometry.tab_reveal, + language.pick("标签展开", "Tab reveal"), + "", + sk.ink, + ); + combobox_value( + r, + gc, + geometry.tab_reveal, + tab_reveal_label(view.tab_reveal, language), + sk.accent, + ); + } }, NebulaSettingsSection::Keymap => { let (kx, ky, kw, kh) = geometry.keymap_row0; @@ -2948,7 +3037,7 @@ pub(super) fn draw_text( #[cfg(test)] mod opacity_tests { - use super::opacity_from_pointer; + use super::{TabRevealMotion, opacity_from_pointer}; #[test] fn slider_pointer_maps_to_clamped_fraction() { @@ -2959,4 +3048,15 @@ mod opacity_tests { assert_eq!(opacity_from_pointer(300.0, slider), 1.0); assert_eq!(opacity_from_pointer(350.0, slider), 1.0); } + + #[test] + fn tab_reveal_motion_defaults_compatibly_and_round_trips() { + assert_eq!(TabRevealMotion::default(), TabRevealMotion::Slide); + assert_eq!(TabRevealMotion::parse("slide"), Some(TabRevealMotion::Slide)); + assert_eq!(TabRevealMotion::parse("INSTANT"), Some(TabRevealMotion::Instant)); + assert_eq!(TabRevealMotion::parse("unknown").unwrap_or_default(), TabRevealMotion::Slide); + for value in [TabRevealMotion::Slide, TabRevealMotion::Instant] { + assert_eq!(TabRevealMotion::parse(value.settings_value()), Some(value)); + } + } } diff --git a/nebula_app/src/input/chrome.rs b/nebula_app/src/input/chrome.rs index 4ce8e41..eb34887 100644 --- a/nebula_app/src/input/chrome.rs +++ b/nebula_app/src/input/chrome.rs @@ -726,6 +726,18 @@ impl> Processor { self.ctx.mark_dirty(); return; }, + crate::display::SettingsHit::TabRevealDropdown => { + self.ctx + .display() + .toggle_settings_dropdown(crate::display::SettingsDropdown::TabReveal); + self.ctx.mark_dirty(); + return; + }, + crate::display::SettingsHit::TabRevealOption(index) => { + self.ctx.display().set_tab_reveal_option(index); + self.ctx.mark_dirty(); + return; + }, crate::display::SettingsHit::FontSizeUp => { let ui_scale = self.ctx.display().window.scale_factor as f32; self.ctx.change_font_size(ui_scale); @@ -1122,6 +1134,8 @@ fn settings_dropdown_keeps_open(hit: crate::display::SettingsHit) -> bool { | Hit::Language(_) | Hit::AcceptCycle | Hit::AcceptOption(_) + | Hit::TabRevealDropdown + | Hit::TabRevealOption(_) | Hit::CursorShapeDropdown | Hit::CursorShapeOption(_) | Hit::BackgroundColor diff --git a/nebula_app/src/input/mouse.rs b/nebula_app/src/input/mouse.rs index 0229d1d..e8a3ae7 100644 --- a/nebula_app/src/input/mouse.rs +++ b/nebula_app/src/input/mouse.rs @@ -291,6 +291,8 @@ impl> Processor { | crate::display::SettingsHit::CursorShapeOption(_) | crate::display::SettingsHit::CursorBlinkToggle | crate::display::SettingsHit::CopyOnSelectToggle + | crate::display::SettingsHit::TabRevealDropdown + | crate::display::SettingsHit::TabRevealOption(_) | crate::display::SettingsHit::FontSizeUp | crate::display::SettingsHit::FontSizeDown | crate::display::SettingsHit::BackgroundImageCoverChrome diff --git a/nebula_app/src/window_context.rs b/nebula_app/src/window_context.rs index 83f9e6e..b4f216b 100644 --- a/nebula_app/src/window_context.rs +++ b/nebula_app/src/window_context.rs @@ -2042,7 +2042,7 @@ impl WindowContext { }; // Program icon (Nerd Font) in front of the label while a command // runs — the sidebar shows WHAT each tab is busy with. AI clients - // with a real brand logo (claude/codex) skip the glyph: the + // with a real brand logo skip the glyph: the // display layer textures the actual mark into the icon slot. let logo = state.and_then(|s| s.running_program.as_deref()).and_then(crate::display::ai_logo);