From 7dd176952497732196123ee76e8b7c75aebc3d5f Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Wed, 16 Sep 2026 05:12:13 +0530 Subject: [PATCH] ui: recreate 1:1 replica of gpu-screen-recorder-ui --- Cargo.lock | 1 + Cargo.toml | 1 + assets/images/checkbox_background.png | Bin 0 -> 982 bytes assets/images/checkbox_circle.png | Bin 0 -> 727 bytes assets/images/combobox_arrow.png | Bin 0 -> 330 bytes assets/images/cross.png | Bin 0 -> 1651 bytes assets/images/delete.png | Bin 0 -> 511 bytes assets/images/folder.png | Bin 0 -> 738 bytes assets/images/gpu_screen_recorder_logo.png | Bin 0 -> 2751 bytes assets/images/info.png | Bin 0 -> 1393 bytes assets/images/masked.png | Bin 0 -> 930 bytes assets/images/pause.png | Bin 0 -> 392 bytes assets/images/play.png | Bin 0 -> 872 bytes assets/images/ps4_cross.png | Bin 0 -> 1270 bytes assets/images/ps4_dpad_down.png | Bin 0 -> 1496 bytes assets/images/ps4_dpad_left.png | Bin 0 -> 1487 bytes assets/images/ps4_dpad_right.png | Bin 0 -> 1497 bytes assets/images/ps4_dpad_up.png | Bin 0 -> 1482 bytes assets/images/ps4_home.png | Bin 0 -> 1811 bytes assets/images/ps4_options.png | Bin 0 -> 801 bytes assets/images/ps4_triangle.png | Bin 0 -> 1446 bytes assets/images/question_mark.png | Bin 0 -> 1448 bytes assets/images/record.png | Bin 0 -> 4196 bytes assets/images/replay.png | Bin 0 -> 4892 bytes assets/images/save.png | Bin 0 -> 614 bytes assets/images/screenshot.png | Bin 0 -> 2020 bytes assets/images/settings.png | Bin 0 -> 6844 bytes assets/images/settings_extra_small.png | Bin 0 -> 959 bytes assets/images/settings_small.png | Bin 0 -> 1837 bytes assets/images/stop.png | Bin 0 -> 333 bytes assets/images/stream.png | Bin 0 -> 7286 bytes assets/images/trash.png | Bin 0 -> 410 bytes assets/images/unmasked.png | Bin 0 -> 1361 bytes assets/images/up_arrow.png | Bin 0 -> 1426 bytes assets/images/warning.png | Bin 0 -> 2163 bytes src/overlay.rs | 15 + src/overlay_egui.rs | 2274 ++++++++++++-------- 37 files changed, 1390 insertions(+), 901 deletions(-) create mode 100644 assets/images/checkbox_background.png create mode 100644 assets/images/checkbox_circle.png create mode 100644 assets/images/combobox_arrow.png create mode 100644 assets/images/cross.png create mode 100644 assets/images/delete.png create mode 100644 assets/images/folder.png create mode 100644 assets/images/gpu_screen_recorder_logo.png create mode 100644 assets/images/info.png create mode 100644 assets/images/masked.png create mode 100644 assets/images/pause.png create mode 100644 assets/images/play.png create mode 100644 assets/images/ps4_cross.png create mode 100644 assets/images/ps4_dpad_down.png create mode 100644 assets/images/ps4_dpad_left.png create mode 100644 assets/images/ps4_dpad_right.png create mode 100644 assets/images/ps4_dpad_up.png create mode 100644 assets/images/ps4_home.png create mode 100644 assets/images/ps4_options.png create mode 100644 assets/images/ps4_triangle.png create mode 100644 assets/images/question_mark.png create mode 100644 assets/images/record.png create mode 100644 assets/images/replay.png create mode 100644 assets/images/save.png create mode 100644 assets/images/screenshot.png create mode 100644 assets/images/settings.png create mode 100644 assets/images/settings_extra_small.png create mode 100644 assets/images/settings_small.png create mode 100644 assets/images/stop.png create mode 100644 assets/images/stream.png create mode 100644 assets/images/trash.png create mode 100644 assets/images/unmasked.png create mode 100644 assets/images/up_arrow.png create mode 100644 assets/images/warning.png diff --git a/Cargo.lock b/Cargo.lock index 7c625a0..5e46bfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3715,6 +3715,7 @@ dependencies = [ "global-hotkey", "gtk", "gtk-layer-shell", + "image", "libc", "memmap2", "mp4", diff --git a/Cargo.toml b/Cargo.toml index 041bab9..21d9f40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ cpal = "0.18.2" rubato = "5.0.0" eframe = "0.31" egui = "0.31" +image = { version = "0.25", default-features = false, features = ["png"] } [target.'cfg(target_os = "linux")'.dependencies] ashpd = { version = "0.13.13", features = ["screencast"] } diff --git a/assets/images/checkbox_background.png b/assets/images/checkbox_background.png new file mode 100644 index 0000000000000000000000000000000000000000..1280368809aa839167fb5a4fdcec23264cf70293 GIT binary patch literal 982 zcmV;{11bE8P)Sc{FUpHIDK8$8C}oOFqNeenNy?0fnv|k08lQ6~W@H$2{a1IN9LKq1+_TR<%lQB5 z->JLW`<%Phx_h55`z1iWuR8%ufGe>V%0sL9TN^d^q3h5^=+VEL z0227a&|Jnd5pB>3=vKu5CQK092_QrLNoWIOXO+{?K{Ji1O@K|%IcN&wl?uJ2ylSGSHqz!MgV!Y zJ%hei!$yn(?M_Aj1b;Jwua2EArvSd@)%i&1ECJTg-|Co*k$G+E34oJ~$6+b`t&$cO zBfvcRSuG1(B)~NKSuL|%B)|mvSuJ%g65tE{Jhu4T1SryFY8h~m00sJ4ExoBHfbaXV zY`cfPR>@-*BY<$5zE;UyR|(KgU#p{t(fN*(1i*=8@n;`>u8KBSPXPgE=yO$EsJ3CX z>jC7gldK4;rSCJR6QlAvry~GP_!5Vs^t}p>C25>=Rd6>B%Nc{rIFBD=KFb7o|j-b5=n$?5$jLdiH*I1t)AumYUGdXfNt98$kyi2v(Of= zT*ZT=$9HI(S1ummX-WX$in$@|V4PCnp;xZ7a>-_=HI^?@{*aJW!~4y90%C{b&@!mo zcE@Sg2-*kj^2)`bNOe=I?EmW-;YK{$1Q=z4$bvnSR$nA4;RxXy zr^Zod5<~(w9I;7~gv+o+|D0+J*`<9@MlNXE<{9 literal 0 HcmV?d00001 diff --git a/assets/images/checkbox_circle.png b/assets/images/checkbox_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..d2936f3e6247a91736d7e8a6157559d7e1fd51c7 GIT binary patch literal 727 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 VES`xRKY@vs!PC{xWt~$(69B4)RE7Wm literal 0 HcmV?d00001 diff --git a/assets/images/gpu_screen_recorder_logo.png b/assets/images/gpu_screen_recorder_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f321785fbaca45cd83eea7fc3c0fb0a9b7442b0b GIT binary patch literal 2751 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 literal 0 HcmV?d00001 diff --git a/assets/images/masked.png b/assets/images/masked.png new file mode 100644 index 0000000000000000000000000000000000000000..f1bc43b3c70f71cd44334abb932f002d329a7aca GIT binary patch literal 930 zcmV;T16}-yP)lN8mB>h}sV5n!(Ew^e|ZR_z-=E zFIv#;;D%=&LbJV<19!paUi1()+*S+xF?iy|hZC~tHaqwv_`+)*gzcnG46a7-Jvh7u zn>|T=0<*5`9>z8R2>w0}MerKa>6g_1EB${H%z@)fy^83ML3k7OEuR3NX)I|E=&^AE zn51jCOQ-NH5moSA;2zVlX+Ovic6?4VZ3!Q{Ob6c!{$My$1xL^S$_fMEP~R%UuJEkI*hznB8$*wj{O-i(v6*QIu zvQFJ}Tf~+M3cefsWgtB*O0=^KovcKsXuv-aW4Et)g@mu;AX3ScfraCvq-}zsGZ9i- z6(Yt%iuvCFP@~A8JOc*4N!rF3I%6TVmFIVgHFY)s$f3XB&~GSjx*D=AE*%;ADdw{~ zz#oJm1LC(NZMzJe-H_U<81jSvZvbcz_HamR8n`QITW9F3htyUAMlcH6xP#^Gsi=>@vH?EAUUSQ=&HjeyGz=oEwnz z>m<$iUyUgP+hiCo)XD4b(fN180<{HQUtrkZ)U6kuAA}C@J;RAv1Fyrg470b=*)5m?bEr`MLZE2NolE(+*FiHG1CO>(CSL7!5eHPVRe!w5IxLGb@<14n5m zS*+E!!5g4on=6Cg!O!4Z+VK_jfZz0*?pRn@SXe}Wzdt9ek1K+10RR9107*qoM6N<$ Ef^J)~S^xk5 literal 0 HcmV?d00001 diff --git a/assets/images/pause.png b/assets/images/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..debdf3044da7fcf59c8fbfc311d513a36e101ee1 GIT binary patch literal 392 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9Gmi6G3l!hU%s0|TR# zr;B4q#jUru9lH)W2(&(2Ww1FwVEg8olPkFT7kr;F)%(48fh~9bf~FapS_GF&?OFTM zZ*je2;)H2cnV;7>bqBnUm8wfj3;sW&y5DNn#&2;I*H=}2?+<$Vfv5C!!27+W@!Idt z&zOCF>SC_6^Ly-tY%=fPJKXX0W>Jf@f!~R>oe{Hj-FGlf%UE%D#@GA5bS$+_25Y5W z3GLHLzOZZpZ!-f+)s2P-rc2_Cnk@|u3`_zI3@i!^j2u{)6Xr3jKgOkaV6`DOS&$wD zevm0&TpyTUys-AWVp8oknTIFWEd74u_v`cBaW^WzR6YOucyA}iES>gMc~QPIZe|9v ziTk^Me)9EU&FAlObH4CAw_pBQ#<1O7v#aW_71MVW|A~huy;cMUErX}4pUXO@geCwe CU!7v=mVU#e)|GQ4p~Q51vH4>Cqm0 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 literal 0 HcmV?d00001 diff --git a/assets/images/ps4_dpad_up.png b/assets/images/ps4_dpad_up.png new file mode 100644 index 0000000000000000000000000000000000000000..e287b3d97822af113f448e7c188348cbed9311dc GIT binary patch literal 1482 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 BP9XpQ literal 0 HcmV?d00001 diff --git a/assets/images/ps4_options.png b/assets/images/ps4_options.png new file mode 100644 index 0000000000000000000000000000000000000000..99787fa05d42cab0873dba7a090e5d8b94d6db41 GIT binary patch literal 801 zcmV++1K#|JP)gb2D$HUp z+uN_}4Go+0#>S}b?C8?cM&EMr2}A^wR+!zl^!E*D%p>DRzk> zq-q`+iEE`ob~qfi1hM1sF%i;ie5^nODCN4_woRBV%W<3{VoRH7^n3yl5JV{pC<~}q zz%^8bLP5)+S4x@Np)6?=v(#%?QR+1$*?ictHL4sZud~^#5tesolexxSALrkP%1g1N z+ej{#``CK!f?mC9t&Tuj+eMwv=f+dXWR2@r2jmGl^7}40%uHk%l%Y7O_e}Z?ef~5tEo69jjJ86^jGrEBMEPyy`OF($&(=Ek z%jftvpXty%^Qb?y&lVwvXq_*l)ld&1^DH(#x)vg^Y^Kkl(jmEe88*^CYwdnrB* zKiw?f;)ke?Kn7mG6^KCss^BK_X7BzT$mw20JqVX!!1Nu5n6Z1|u(2P(B^U+y8>)k6Z~`2-2K+%eL(t<=JO>Bo zO2uzc^_r|EFaH5-gT*+Xq}~sMa-c*#cn3rxu^k>Wb`mDA$6z;fLOmRXZ*Ur>ko}Z; zZ@x3wI6{F?VyfJJJHoGDe19cqx z#_R~(k)1QieX~n(kX@aJd&P|J=6=~t-q<4GF1%;lL+U5cClLYJ%vb6&;t!3`%i?Fv f=E|j9{*Uql{9Y-GZApUS00000NkvXXu0mjf0v35B literal 0 HcmV?d00001 diff --git a/assets/images/ps4_triangle.png b/assets/images/ps4_triangle.png new file mode 100644 index 0000000000000000000000000000000000000000..ff07fcd05c0ec72f107c6c907306e58b071e857a GIT binary patch literal 1446 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 literal 0 HcmV?d00001 diff --git a/assets/images/record.png b/assets/images/record.png new file mode 100644 index 0000000000000000000000000000000000000000..92c2bcba42eaf3e2b6ee83345c7737dcb5bf45c0 GIT binary patch literal 4196 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 O0000H3bw51 z*kNIzq35@VPtR&az|zWj?)eK_)OwH9>@Da2<1sttS7Fh;v)RA1B6Y;NpH7-m#9G6k z?wFMz-^0MT=KlfRhlvN&&Gu?q%D+#LU32kmfRGc5(SeUv(-z1%2+B0c3-bm9t$)p= z(>L1i?G2k6+r#{v+0G z`2o=gR$H|%4E9qeG%WJuEXcmGcd_;};|W__b{i&b@i9K|gT-PF!#hTtxWR=d;BkSp zh=YCLf?B^jwhPu~w7Q8@F>O1~#Wy=^QIO?H7LD)eYmV`Y#swWuOZIS5Vtdo{nV~d2 zWOXe6jqVj@#-AH@cVtz{O_(ZYAQqULCS)KFB&!ZEZD77~6R3#kO7T?d841C@e#hk& zn6Ru$;C|vLKXG|&=&H`Qn;ou&b+v^?au^<;&sHgSfYaa_!#}?ZCCn)S%&%rieGt0R z5bVAFX`$Dv|87$k2rp#JS$44HYDC;E->s&%PEE-xzVagKyS8mJb3t$Cgf|%utJpVw m;Z5VqzpybvM@)M`9iy#;etZYht#!bJ!{F)a=d#Wzp$PzXH~)kH literal 0 HcmV?d00001 diff --git a/assets/images/screenshot.png b/assets/images/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d67acf698cb9acc46dd9f9ebf088c18618ea3581 GIT binary patch literal 2020 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 D%lgH@ literal 0 HcmV?d00001 diff --git a/assets/images/settings_extra_small.png b/assets/images/settings_extra_small.png new file mode 100644 index 0000000000000000000000000000000000000000..71770fe7ae8a0e82951d9f0fde41cb14e58755f0 GIT binary patch literal 959 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 literal 0 HcmV?d00001 diff --git a/assets/images/stream.png b/assets/images/stream.png new file mode 100644 index 0000000000000000000000000000000000000000..198a06e831e36c95c45cf8f09a268062d899121b GIT binary patch literal 7286 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 literal 0 HcmV?d00001 diff --git a/assets/images/unmasked.png b/assets/images/unmasked.png new file mode 100644 index 0000000000000000000000000000000000000000..2ac4ea45561bc30b77de84f3782d68f1d6a6d63d GIT binary patch literal 1361 zcmV-X1+MyuP);D>KFN_`k)UjV*4P3J}7Nt)GAffRRs1wxJN(pfWOmxCS}T>l>ux}3;t0s!hc{jD9B_8+@B6EIr<{+PEXnh!c_1R z9bk~i^P^ln-1mz>t7Be7XHk+y7z(!gtO|Z2L-!r`e}H6$1#bYK{bHb4}FFFUeBLzo6b7+PJ<6{!p9}`@mMP87v4efUqjUDzPMv zt#N-#AEIM)V}`FeRlTkC}O;D zDQo120x6HP(~&N))}yb2UOB8B@o8H@$YnQ^b4&&01G@}FW-UH3L$k(|nCsKV76a{* zK9=^$2;s&HxO9S)fraC8pHS1b@8YtOq(kUgoK3f;NFO`d=iZc<+!Rl4BF}ZpFZBjP ze1+>9piNQFax`+Zg6`1%oqbL98UT&Efy*RAanlf=d!Mm0*FZJc0Ul=2BmW_Bh?QA% z>&PVb`Ds#=H>Md-UgmRe1UL`g0^O)<1Fp58J&Uf%Kr&O?m<=j3(7tJI=QBCOiil3; zoR7};_)H?jPeGw4#Qs(>j6Ml|iP*LZ)mWe^Vnw5TbYAd0{~nCzeaSPdOCZls6*dwq z*W8GRtoPCP3RB;%$no1kH#os{HiIeC(yg6l=N4EekdZw?iI2Xi+)}&pkY|xY`Ftd3 zG7#{X+Zrl)OnG(Nij0Q=h&hpH6FNJ(|fMwJ41@3<3MW)nLVum(OAuBdR&{-Xg;H1)=HU&5zp%`H~g`_YEDUKB2C) zevRjpn4Ko@lT=vu Tlcu=$00000NkvXXu0mjfkc5Fg literal 0 HcmV?d00001 diff --git a/assets/images/up_arrow.png b/assets/images/up_arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..1be9fdcf3b1e62cbdd31bfb33e99543388bbc059 GIT binary patch literal 1426 zcmV;D1#S9?P)6XCX_C^sBMD5KM-1JYa3(4XOcI5hn&%y z>z$uBb7tQ7^n;I=$;|!x?zunb%v{U*hz}dKbcT-O_}y*=mxC=}GuQ~`z!EqHJ^>$s zBjC8P!nSOc>k<}t6}SOh53U9mRFHNU90YHG*IA(K0~W9hEP^ABEG~mvv(P{R@jw^u!?AwV;D+DRXGz-`qQU?=#GQ(asDmsCT59w$5a6Wo%P0IlGVlTGZ8y$=Xb zu>j_{`J1h_AZ0J)GEwsOx)HGWcSu+9II z!d~#8;hxWH*Q&|o zwQC8SiYfqkYsaKlQSJIZcrW0YxF9F27-bGP4ek*({k(R)iT@rj+{+6Ku>!ms=uRt+ zjWnjU>zkm>yczJU1bRd56j=aiuWnMiehIcIWw!DE{L)BZWtcf&8Ms5({B(2fhbfLK zOVcRtGyKv>;M*`3P!v|Zq+LHJ@6Ax|629k`HZER(bAv443qi%pU_tv|FUS6LJmGsP z3Y>=&8o=#-y}wFW@mKs?=HGF!$*24#enw66O1Mt_eipDnQ0+-2=<2RLeHic0d^|O} z*Qcea6ZR7}`dPqF3C#Rz?=K872Q4&Mf%J;7*2@A`3#t543%Sz*d=5SQU!Zq3^UOJI zM_9*sN`~ua3qX;c3gh;EC@7O4neqhKSwZ?vPGc*RU_nrTc9R7x7s`A>acbOg+>4%l z8ahlh^K7p&7r0K;gisyZOainB?e`nB-ltbwha%*4h|{W8&>-Y8x0nQI78?6GuCTk4 z>q3wIU2(8-CQX;JD=c%~5_7>KlK_nZ%icVt_X*G3v5z?>Z1_!mwv+Qv^PDHJEH;`1 z$i>Vw$L2bv^nN#ba-4g#TtV-b`FEV7I<6H=aT#&z`bK0mqmq!r<4%6mbB6`Y^)TmU zD@$WsM`_X(SrtIy^>c}rD~HGa|8XnRm((6I=)kzccgmj60k{;ZsVPj@P|}@vGO;H<&`95BrRsQ#>q=crPl*l5a;9fL!F=f|Vg>hhB`*E3kTSK9k(vCcY>6F7TS=^L2>P zJSlt_*DiQKR|~!lJ{lbs!1`#pEJ_< zVl|J<6sy>9`n+yUwa*L@KTYYtl#9DO3^Q?PN@Y7Apn8!|!|9~Tu0$s}A%PnBK-Coi zG3a#I_dMsM>X<|pz<_nu%x5X|Wk#Dd`{*oz!r^1QhCkq49EGmwoWeuDjZEeQh#*i- g-TzBB@FN278V0^m+!ZDJH2?qr07*qoM6N<$f@ip!H~;_u literal 0 HcmV?d00001 diff --git a/assets/images/warning.png b/assets/images/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..c0ccf5c6e61b2e3a5067933c438077435e25cebd GIT binary patch literal 2163 zcmV-(2#oiMP)nY2@p&?q9yT2z@S0|69^`P2#F+w7$e4r2l1%*Llfe`QEUw%A_ZEZ0zIMa_Skja zx3k;X*>87ezhm1qzT_pd@0*!-%$qlF-uJEKf$$(fWbkMlHx@whgb>{08F?Z}@uCj5 zFakO#29QDw3hHdEA(9D}({>}lUNaPrN(USm2@Qfuf;Z{z@z8dqQOZZ7gCt@#gFOKU zcMweH=v@ReLWC6ED<4V+*=$BCuv|rXptR+&P+qqO= zKnLSBVU!2@Y)uTkQJ)7y=?ONv!Wj|zbx=bLnyGw{2j~fGf`YujE5CMmmVz8)(7vWC zulX3!a75Y(_AtkuC);WDxEf|Ch;8;6?Q4&PjIxnOTo@y4Blxw+Z;IP z7}Hl?GW=KxW|B(q8o@*#p&-`=myeYPbShlf2rX>^+ulVmhkAnk6p4sRerXA0c0Rb))Q`DxJW`Ev&KHdfw>&qdOT*zWVRU=%! z8W0Q6C5j2QXp}>r2k7vGJ*@HwU+*i0aMR#3?4y!C?=dSymra`Zc7jV;!ptdlm|Bt) zaUk}=z8oy2a~i%-nl`vVrR5mG``DFj`|(c5$*>B|f30H(aw~ra&f9+=xKK;g$1o2N zWHgiD0+#Sdl~Y)G1ni4rI9NdEHZ)l43h`)t6~-qB&hl?yS%RM5rh`3E4hVMj$9v$= zm+i{$Cm+m&s(Gma@uAPKESzfPX6jZd5^jQ))E}YCOu#5E(x3Rws#E+(IU(_j`AK=10?Ww z_IVX4Fs(Fv%G=xT7NW6$bK=}8wT6VzG{B1loov$c*}Ib>4ss2+g=jqB+{l9gCzWAT z41g~;H?f3=7o`iOysEW1frJLa_9St+UPGAb1c{w&%_gFSW zb1Bmk1AKV_@JG$Im54@GMZ+9fI=tM?-tKX zgJ`ehToeyUi{u5!o2xTMT|kY-08rX)?fKuKeX)%~VYWKp9XLPN#jMb65oX-9_*XU$w_(mEgPmrQl4rYBE4c zVIo|)HVCc%3_`M9hMFZ#*znXizWO6}q!PS$wgcY#=sF3cellicM+yEN(TF03CU}Zq zg^#duX$BnJHi6H}qXH~|bas1hfX+0FVpOewh0|59T!at5>R`zY;H@Eg^cnzb@%6sG zDP+Z(>!(3xnlA!CxphD(i=RH*r~IB#W`~b= zPK4ro?N@uuqtKA}p#@i5d5GZWT08)6HoMt~yR9x?+4r)}Zzo>b(F(sd_9}4z5!*=9 zVUDmn$I2V`0`|lKb;|j`#GNP>9TmknN5d*9vnC<&fUTTL0a~-NL zIkdp$XYwG;F%p}wjWiFFf{B>1G)*0C)gLs09amn)1L&~oeD?ymME8% zc=_W9)>Z}vJ@CNO=i%7TJ$(IeOE?3h)ATXf=S~fumciuWB#5_v&7cv^HTOfsqi4Z2 z;P(RjR|>vt>T;gEh>J!a|k9aX@Z-*{xjA^kH+h~2kh`EkIJsE&4w&`+lx2leLA4*r~~T4 z=#WTt4e$Ubjr@uMa{C|i7{spFv^q}qw)?oku002ovPDHLkV1j=9@y7rF literal 0 HcmV?d00001 diff --git a/src/overlay.rs b/src/overlay.rs index dac7dd5..9a991dc 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -21,6 +21,7 @@ pub enum ToastIcon { Cursor, Info, Error, + Screenshot, } impl ToastIcon { @@ -30,6 +31,7 @@ impl ToastIcon { "record" | "recording" | "start" => ToastIcon::Record, "save" | "saved" | "stop" => ToastIcon::Save, "cursor" => ToastIcon::Cursor, + "screenshot" => ToastIcon::Screenshot, "error" => ToastIcon::Error, _ => ToastIcon::Info, } @@ -42,6 +44,7 @@ pub fn spawn_toast(title: &str, subtitle: &str, icon: ToastIcon) { ToastIcon::Record => "record", ToastIcon::Save => "save", ToastIcon::Cursor => "cursor", + ToastIcon::Screenshot => "screenshot", ToastIcon::Error => "error", ToastIcon::Info => "info", }; @@ -478,6 +481,18 @@ pub fn show_shadowplay_toast(title: &str, subtitle: &str, icon: ToastIcon) { cr.line_to(cx - 5.0, cy + 5.0); let _ = cr.stroke(); } + ToastIcon::Screenshot => { + cr.set_source_rgb(accent_rgb.0, accent_rgb.1, accent_rgb.2); + cr.set_line_width(1.8); + cr.set_line_cap(gtk::cairo::LineCap::Round); + cr.set_line_join(gtk::cairo::LineJoin::Round); + cr.rectangle(cx - 7.0, cy - 4.5, 14.0, 10.0); + let _ = cr.stroke(); + cr.arc(cx, cy + 0.5, 2.5, 0.0, PI * 2.0); + let _ = cr.stroke(); + cr.rectangle(cx - 3.5, cy - 7.0, 4.0, 2.5); + let _ = cr.fill(); + } ToastIcon::Info => { cr.set_source_rgb(accent_rgb.0, accent_rgb.1, accent_rgb.2); cr.arc(cx, cy, 4.5, 0.0, PI * 2.0); diff --git a/src/overlay_egui.rs b/src/overlay_egui.rs index 44c5f50..a7f0e2f 100644 --- a/src/overlay_egui.rs +++ b/src/overlay_egui.rs @@ -11,10 +11,58 @@ use crate::ipc::{self, Command, DaemonStatus}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ShadowPlayView { MainHud, - Settings, + ReplaySettings, + RecordSettings, + StreamSettings, + GlobalSettings, + ScreenshotSettings, Gallery, } +#[derive(Clone)] +pub struct OverlayTextures { + pub logo: egui::TextureHandle, + pub replay: egui::TextureHandle, + pub record: egui::TextureHandle, + pub stream: egui::TextureHandle, + pub screenshot: egui::TextureHandle, + pub settings_small: egui::TextureHandle, + pub settings_extra_small: egui::TextureHandle, + pub cross: egui::TextureHandle, + pub play: egui::TextureHandle, + pub pause: egui::TextureHandle, + pub stop: egui::TextureHandle, + pub save: egui::TextureHandle, + pub settings_large: egui::TextureHandle, +} + +fn load_embedded_png(ctx: &egui::Context, name: &str, bytes: &[u8]) -> egui::TextureHandle { + let img = image::load_from_memory(bytes).expect("Embedded PNG must be valid").to_rgba8(); + let size = [img.width() as usize, img.height() as usize]; + let pixels = img.as_raw(); + let color_image = egui::ColorImage::from_rgba_unmultiplied(size, pixels); + ctx.load_texture(name, color_image, egui::TextureOptions::LINEAR) +} + +fn draw_texture_centered( + painter: &egui::Painter, + texture: &egui::TextureHandle, + center: egui::Pos2, + target_height: f32, + tint: Color32, +) { + let [w, h] = texture.size(); + let aspect = w as f32 / h.max(1) as f32; + let target_width = target_height * aspect; + let rect = egui::Rect::from_center_size(center, Vec2::new(target_width, target_height)); + painter.image( + texture.id(), + rect, + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + tint, + ); +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum KeybindAction { Menu, @@ -410,16 +458,26 @@ fn render_keycap(ui: &mut egui::Ui, text: &str) { } pub fn resolve_accent_color(accent: &str) -> Color32 { - match accent.to_lowercase().as_str() { - "green" | "emerald" => Color32::from_rgb(34, 197, 94), + let trimmed = accent.trim(); + if trimmed.starts_with('#') && trimmed.len() == 7 { + if let (Ok(r), Ok(g), Ok(b)) = ( + u8::from_str_radix(&trimmed[1..3], 16), + u8::from_str_radix(&trimmed[3..5], 16), + u8::from_str_radix(&trimmed[5..7], 16), + ) { + return Color32::from_rgb(r, g, b); + } + } + match trimmed.to_lowercase().as_str() { + "amd" | "red" | "crimson" => Color32::from_rgb(221, 0, 49), + "nvidia" | "green" | "emerald" => Color32::from_rgb(118, 185, 0), + "intel" | "blue" | "sapphire" => Color32::from_rgb(8, 109, 183), "lime" => Color32::from_rgb(163, 230, 53), "yellow" | "solar" => Color32::from_rgb(250, 204, 21), "amber" | "orange" => Color32::from_rgb(245, 158, 11), - "red" | "crimson" => Color32::from_rgb(239, 68, 68), "pink" | "rose" => Color32::from_rgb(244, 63, 94), "purple" | "violet" => Color32::from_rgb(168, 85, 247), - "blue" | "sapphire" => Color32::from_rgb(56, 189, 248), - _ => Color32::from_rgb(56, 189, 248), + _ => Color32::from_rgb(221, 0, 49), } } @@ -629,208 +687,129 @@ fn draw_settings_icon(painter: &egui::Painter, center: egui::Pos2, radius: f32, } } -// Modern Sleek Action Card Renderer +// GPU Screen Recorder 1:1 Action Card Renderer #[allow(clippy::too_many_arguments)] -fn render_action_card( +fn render_gsr_card( ui: &mut egui::Ui, - width: f32, - height: f32, + rect: egui::Rect, title: &str, + icon_tex: Option<&egui::TextureHandle>, + fallback_icon: impl FnOnce(&egui::Painter, egui::Pos2), + status_text: &str, is_active: bool, dropdown_open: bool, - draw_icon: impl FnOnce(&egui::Painter, egui::Pos2), - status_text: &str, - status_color: Color32, - sub_text: &str, accent: Color32, ) -> bool { - let (raw_rect, response) = ui.allocate_exact_size(Vec2::new(width, height), egui::Sense::click()); - let rect = egui::Rect::from_min_size( - egui::pos2(raw_rect.left().round(), raw_rect.top().round()), - Vec2::new(width.round(), height.round()), - ); - let hovered = response.hovered(); + let resp = ui.allocate_rect(rect, egui::Sense::click()); + let hovered = resp.hovered(); let painter = ui.painter(); - // Neutral translucent black card fill (pure black, zero blue hue, zero accent fill) - let bg = if dropdown_open { - Color32::from_rgba_unmultiplied(18, 18, 18, 175) + + // 1. Background fill & outlines + if dropdown_open { + // Solid pitch black + vibrant top accent bar (3px) + painter.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(0, 0, 0)); + let top_bar = egui::Rect::from_min_size(rect.min, Vec2::new(rect.width(), 3.0)); + painter.rect_filled(top_bar, CornerRadius::ZERO, accent); } else if hovered { - Color32::from_rgba_unmultiplied(22, 22, 22, 180) + // Solid pitch black + full card rectangular accent outline (2px) + painter.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(0, 0, 0)); + painter.rect_stroke(rect, CornerRadius::ZERO, Stroke::new(2.0_f32, accent), egui::StrokeKind::Inside); } else { - Color32::from_rgba_unmultiplied(10, 10, 10, 155) - }; - - painter.rect_filled(rect, CornerRadius::ZERO, bg); - - // Accent outline: ONLY shown when card is active (replay active or recording active) - let stroke = if is_active { - if hovered || dropdown_open { - Stroke::new(1.5_f32, accent) - } else { - Stroke::new(1.0_f32, accent) - } - } else { - Stroke::NONE - }; - - if stroke != Stroke::NONE { - painter.rect_stroke(rect, CornerRadius::ZERO, stroke, egui::StrokeKind::Inside); + // Translucent dark background (Color(0, 0, 0, 180)) + painter.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgba_unmultiplied(0, 0, 0, 180)); } - // Card Title (clean modern sans typography) + // 2. Title text (top margin ~18px, bold sans, white) + let title_pos = egui::pos2(rect.center().x, rect.top() + rect.height() * 0.085 + 4.0); painter.text( - egui::pos2(rect.center().x, rect.top() + 25.0), + title_pos, egui::Align2::CENTER_CENTER, title, - FontId::proportional(14.0), - if is_active { accent } else { Color32::WHITE }, + FontId::proportional(14.5), + Color32::WHITE, ); - // Centered vector icon (no ambient fill circle) - let icon_center = egui::pos2(rect.center().x, rect.top() + 85.0); - draw_icon(painter, icon_center); + // 3. Center Icon (height ~88px, tinted to accent if active, else white) + let icon_tint = if is_active { accent } else { Color32::WHITE }; + if let Some(tex) = icon_tex { + draw_texture_centered(painter, tex, rect.center(), rect.height() * 0.42, icon_tint); + } else { + fallback_icon(painter, rect.center()); + } - // Status subtitle (e.g. "01:23" or "Not recording" / "Buffer 60s") + // 4. Description/Status text (bottom margin ~18px, accent if active, else #969696) + let status_pos = egui::pos2(rect.center().x, rect.bottom() - rect.height() * 0.085 - 4.0); + let status_color = if is_active { accent } else { Color32::from_rgb(150, 150, 150) }; painter.text( - egui::pos2(rect.center().x, rect.bottom() - 32.0), + status_pos, egui::Align2::CENTER_CENTER, status_text, - FontId::proportional(11.5), + FontId::proportional(12.5), status_color, ); - // Secondary hint - painter.text( - egui::pos2(rect.center().x, rect.bottom() - 14.0), - egui::Align2::CENTER_CENTER, - sub_text, - FontId::proportional(10.0), - Color32::from_rgb(130, 135, 145), - ); - - response.clicked() + resp.clicked() } -// Modern Sleek Translucent Dropdown Action Menu Container - Exact Pixel Alignment -fn render_dropdown_menu( - ui: &mut egui::Ui, - card_width: f32, - accent: Color32, - add_contents: impl FnOnce(&mut egui::Ui), -) { - ui.add_space(6.0); - let stroke_width = 1.0_f32; - let inner_w = (card_width - 2.0 * stroke_width).round(); - egui::Frame::NONE - .fill(Color32::from_rgba_unmultiplied(10, 10, 10, 185)) - .stroke(Stroke::new(stroke_width, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 140))) - .corner_radius(CornerRadius::ZERO) - .inner_margin(Margin::ZERO) - .show(ui, |ui| { - ui.spacing_mut().item_spacing = Vec2::ZERO; - ui.set_width(inner_w); - ui.set_min_width(inner_w); - ui.set_max_width(inner_w); - add_contents(ui); - }); -} - -// Remade Dropdown Action Menu Item (Left-Aligned, Subtitles, Hotkey Badges, Left Indicator) -fn render_menu_item( +// GPU Screen Recorder 1:1 Dropdown Menu Item Renderer +fn render_dropdown_item( ui: &mut egui::Ui, + rect: egui::Rect, title: &str, - subtitle: Option<&str>, - hotkey_hint: Option<&str>, - is_active: bool, + hotkey: Option<&str>, + icon_tex: Option<&egui::TextureHandle>, accent: Color32, is_last: bool, ) -> bool { - let (raw_rect, response) = ui.allocate_exact_size(Vec2::new(ui.available_width(), 44.0), egui::Sense::click()); - let rect = egui::Rect::from_min_size( - egui::pos2(raw_rect.left().round(), raw_rect.top().round()), - Vec2::new(raw_rect.width().round(), 44.0), - ); - let hovered = response.hovered(); + let resp = ui.allocate_rect(rect, egui::Sense::click()); + let hovered = resp.hovered(); + let painter = ui.painter(); - let bg = if hovered { - Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 36) - } else { - Color32::TRANSPARENT - }; + // Background: solid black + painter.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(0, 0, 0)); - ui.painter().rect_filled(rect, CornerRadius::ZERO, bg); - - // Left vertical accent indicator bar on hover + // Hover outline if hovered { - let left_bar = egui::Rect::from_min_size(rect.min, Vec2::new(3.0, rect.height())); - ui.painter().rect_filled(left_bar, CornerRadius::ZERO, accent); + painter.rect_stroke(rect, CornerRadius::ZERO, Stroke::new(2.0_f32, accent), egui::StrokeKind::Inside); } - // Left-aligned Title - let title_color = if hovered { - accent - } else if is_active { - Color32::WHITE - } else { - Color32::from_rgb(222, 226, 234) - }; + // Left Icon + let mut text_x = rect.left() + 16.0; + if let Some(tex) = icon_tex { + let icon_center = egui::pos2(rect.left() + 24.0, rect.center().y); + draw_texture_centered(painter, tex, icon_center, 18.0, if hovered { accent } else { Color32::WHITE }); + text_x = rect.left() + 44.0; + } - let title_y = if subtitle.is_some() { rect.top() + 13.0 } else { rect.center().y }; - ui.painter().text( - egui::pos2(rect.left() + 14.0, title_y), + // Title + painter.text( + egui::pos2(text_x, rect.center().y), egui::Align2::LEFT_CENTER, title, FontId::proportional(12.5), - title_color, + if hovered { accent } else { Color32::WHITE }, ); - // Subtitle under Title - if let Some(sub) = subtitle { - ui.painter().text( - egui::pos2(rect.left() + 14.0, rect.bottom() - 12.0), - egui::Align2::LEFT_CENTER, - sub, - FontId::proportional(10.0), - Color32::from_rgb(135, 140, 150), - ); - } - - // Right-aligned Hotkey Badge (dynamically sized to fit user-configured hotkeys) - if let Some(hk) = hotkey_hint { - let badge_w = (hk.len() as f32 * 6.5 + 14.0).max(52.0); - let badge_rect = egui::Rect::from_center_size( - egui::pos2(rect.right() - badge_w / 2.0 - 10.0, rect.center().y), - Vec2::new(badge_w, 18.0), - ); - ui.painter().rect_filled( - badge_rect, - CornerRadius::ZERO, - Color32::from_rgba_unmultiplied(255, 255, 255, 14), - ); - ui.painter().rect_stroke( - badge_rect, - CornerRadius::ZERO, - Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 22)), - egui::StrokeKind::Inside, - ); - ui.painter().text( - badge_rect.center(), - egui::Align2::CENTER_CENTER, + // Right Hotkey + if let Some(hk) = hotkey && !hk.trim().is_empty() { + painter.text( + egui::pos2(rect.right() - 14.0, rect.center().y), + egui::Align2::RIGHT_CENTER, hk, - FontId::monospace(9.0), - if hovered { accent } else { Color32::from_rgb(180, 185, 195) }, + FontId::proportional(11.0), + if hovered { accent } else { Color32::from_rgba_unmultiplied(255, 255, 255, 140) }, ); } - // Clean subtle 1px divider between items (not drawn on last item so it stays flush) + // 1px divider if !is_last && !hovered { - ui.painter().line_segment( - [egui::pos2(rect.left() + 8.0, rect.bottom()), egui::pos2(rect.right() - 8.0, rect.bottom())], - Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 14)), + painter.line_segment( + [egui::pos2(rect.left(), rect.bottom()), egui::pos2(rect.right(), rect.bottom())], + Stroke::new(1.0_f32, Color32::from_rgb(32, 32, 36)), ); } - response.clicked() + resp.clicked() } // Section card helper for Settings view @@ -860,6 +839,11 @@ pub struct ScytheOverlayApp { current_view: ShadowPlayView, replay_dropdown_open: bool, record_dropdown_open: bool, + stream_dropdown_open: bool, + stream_service_idx: usize, + stream_key: String, + stream_url: String, + textures: Option, output_dir: String, replay_sec: u32, bitrate_mbps: u32, @@ -964,6 +948,11 @@ impl ScytheOverlayApp { current_view: ShadowPlayView::MainHud, replay_dropdown_open: false, record_dropdown_open: false, + stream_dropdown_open: false, + stream_service_idx: 0, + stream_key: String::new(), + stream_url: "rtmp://live.twitch.tv/app/".to_string(), + textures: None, output_dir, replay_sec, bitrate_mbps, @@ -1058,835 +1047,1288 @@ impl ScytheOverlayApp { // Fullscreen surface handles internal layout dynamically without window resize jitter } + pub fn ensure_textures(&mut self, ctx: &egui::Context) { + if self.textures.is_none() { + self.textures = Some(OverlayTextures { + logo: load_embedded_png(ctx, "gsr_logo", include_bytes!("../assets/images/gpu_screen_recorder_logo.png")), + replay: load_embedded_png(ctx, "gsr_replay", include_bytes!("../assets/images/replay.png")), + record: load_embedded_png(ctx, "gsr_record", include_bytes!("../assets/images/record.png")), + stream: load_embedded_png(ctx, "gsr_stream", include_bytes!("../assets/images/stream.png")), + screenshot: load_embedded_png(ctx, "gsr_screenshot", include_bytes!("../assets/images/screenshot.png")), + settings_small: load_embedded_png(ctx, "gsr_settings_small", include_bytes!("../assets/images/settings_small.png")), + settings_extra_small: load_embedded_png(ctx, "gsr_settings_extra_small", include_bytes!("../assets/images/settings_extra_small.png")), + cross: load_embedded_png(ctx, "gsr_cross", include_bytes!("../assets/images/cross.png")), + play: load_embedded_png(ctx, "gsr_play", include_bytes!("../assets/images/play.png")), + pause: load_embedded_png(ctx, "gsr_pause", include_bytes!("../assets/images/pause.png")), + stop: load_embedded_png(ctx, "gsr_stop", include_bytes!("../assets/images/stop.png")), + save: load_embedded_png(ctx, "gsr_save", include_bytes!("../assets/images/save.png")), + settings_large: load_embedded_png(ctx, "gsr_settings_large", include_bytes!("../assets/images/settings.png")), + }); + } + } + pub fn switch_view(&mut self, view: ShadowPlayView, _ctx: &egui::Context) { self.current_view = view; self.listening_keybind = None; self.replay_dropdown_open = false; self.record_dropdown_open = false; + self.stream_dropdown_open = false; + } + + fn render_top_bar(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { + let screen_w = ui.available_width(); + let bar_h = 48.0_f32; + let bar_rect = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), Vec2::new(screen_w, bar_h)); + let accent = self.accent_color(); + + // Top bar background: translucent dark (Color(0, 0, 0, 190)) + ui.painter().rect_filled(bar_rect, CornerRadius::ZERO, Color32::from_rgba_unmultiplied(0, 0, 0, 190)); + + // Left side: Logo on MainHud, or Back button on Subpage + match self.current_view { + ShadowPlayView::MainHud => { + if let Some(tex) = &self.textures { + let logo_center = egui::pos2(24.0, 24.0); + draw_texture_centered(ui.painter(), &tex.logo, logo_center, 30.0, Color32::WHITE); + } + } + _ => { + let back_rect = egui::Rect::from_min_size(egui::pos2(12.0, 10.0), Vec2::new(88.0, 28.0)); + let back_resp = ui.allocate_rect(back_rect, egui::Sense::click()); + let back_hov = back_resp.hovered(); + + let bg = if back_hov { + Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 32) + } else { + Color32::from_rgba_unmultiplied(20, 20, 24, 200) + }; + let stroke = if back_hov { + Stroke::new(1.5_f32, accent) + } else { + Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 30)) + }; + let painter = ui.painter(); + painter.rect(back_rect, CornerRadius::ZERO, bg, stroke, egui::StrokeKind::Inside); + painter.text( + back_rect.center(), + egui::Align2::CENTER_CENTER, + "< BACK", + FontId::proportional(11.5), + if back_hov { accent } else { Color32::WHITE }, + ); + + if back_resp.clicked() { + self.switch_view(ShadowPlayView::MainHud, ctx); + } + } + } + + // Center Title + let title_text = match self.current_view { + ShadowPlayView::MainHud => "GPU Screen Recorder", + ShadowPlayView::ReplaySettings => "Instant Replay", + ShadowPlayView::RecordSettings => "Record", + ShadowPlayView::StreamSettings => "Livestream", + ShadowPlayView::GlobalSettings => "Settings", + ShadowPlayView::ScreenshotSettings => "Screenshot", + ShadowPlayView::Gallery => "Recordings Gallery", + }; + ui.painter().text( + egui::pos2(screen_w * 0.5, 24.0), + egui::Align2::CENTER_CENTER, + title_text, + FontId::proportional(15.5), + Color32::WHITE, + ); + + // Right side: Square close 'X' button + let close_size = 32.0_f32; + let close_rect = egui::Rect::from_min_size( + egui::pos2(screen_w - close_size - 12.0, (bar_h - close_size) * 0.5), + Vec2::new(close_size, close_size), + ); + let close_resp = ui.allocate_rect(close_rect, egui::Sense::click()); + let close_hov = close_resp.hovered(); + + let painter = ui.painter(); + if close_hov { + painter.rect_filled(close_rect, CornerRadius::ZERO, Color32::from_rgb(0, 0, 0)); + painter.rect_stroke(close_rect, CornerRadius::ZERO, Stroke::new(1.5_f32, accent), egui::StrokeKind::Inside); + } + + if let Some(tex) = &self.textures { + draw_texture_centered(painter, &tex.cross, close_rect.center(), 14.0, if close_hov { accent } else { Color32::WHITE }); + } else { + let half = 6.0; + let c = close_rect.center(); + let stroke = Stroke::new(1.8_f32, if close_hov { accent } else { Color32::WHITE }); + painter.line_segment([c + Vec2::new(-half, -half), c + Vec2::new(half, half)], stroke); + painter.line_segment([c + Vec2::new(half, -half), c + Vec2::new(-half, half)], stroke); + } + + if close_resp.clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + crate::ipc::clean_overlay_pid(); + std::process::exit(0); + } } fn render_main_hud(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { - let anim_time = self.anim_time; let is_recording = self.status.is_recording; let rec_dur = self.status.recording_duration_sec; let is_replay_active = self.status.is_replay_active; let accent = self.accent_color(); let screen_w = ui.available_width(); - let card_w = 210.0; - let card_h = 185.0; - let card_gap = 14.0; - let total_cards_w = 3.0 * card_w + 2.0 * card_gap; + let screen_h = ui.available_height(); + let card_w = 210.0_f32; + let card_h = 210.0_f32; + let total_cards_w = 3.0 * card_w; // 630.0, spacing = 0.0! let left_pad = ((screen_w - total_cards_w) / 2.0).max(10.0); - let top_pad = 70.0; + let top_pad = ((screen_h * 0.25) - card_h * 0.5).max(64.0); - let hud_rect = egui::Rect::from_min_size( + let aux_size = 70.0_f32; + let aux_gap = 23.0_f32; + let aux_x = left_pad + total_cards_w + aux_gap; + + let any_dropdown_open = self.replay_dropdown_open || self.record_dropdown_open || self.stream_dropdown_open; + let hud_rect = egui::Rect::from_min_max( egui::pos2(left_pad, top_pad), - egui::vec2( - total_cards_w, - card_h + 120.0, - ), + egui::pos2(aux_x + aux_size, top_pad + card_h + if any_dropdown_open { 230.0 } else { 0.0 }), ); self.panel_rect = hud_rect; - ui.allocate_new_ui(egui::UiBuilder::new().max_rect(hud_rect), |ui| { - ui.vertical(|ui| { - ui.set_width(total_cards_w); - ui.set_min_width(total_cards_w); - ui.set_max_width(total_cards_w); + // The 3 seamlessly touching square cards + let card0_rect = egui::Rect::from_min_size(egui::pos2(left_pad, top_pad), Vec2::new(card_w, card_h)); + let card1_rect = egui::Rect::from_min_size(egui::pos2(left_pad + card_w, top_pad), Vec2::new(card_w, card_h)); + let card2_rect = egui::Rect::from_min_size(egui::pos2(left_pad + card_w * 2.0, top_pad), Vec2::new(card_w, card_h)); - // Voluntary, non-intrusive Update Notification Banner - let cur_update = self.update_status.lock().ok().map(|g| g.clone()).unwrap_or_default(); - if let crate::updater::UpdateStatus::Available(ref info) = cur_update - && !self.update_dismissed { - ui.vertical_centered(|ui| { - egui::Frame::NONE - .fill(Color32::from_rgba_unmultiplied(12, 13, 16, 250)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 160))) - .corner_radius(CornerRadius::ZERO) - .inner_margin(Margin::symmetric(14_i8, 7_i8)) - .show(ui, |ui| { - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(format!("UPDATE: v{}", info.version)) - .size(11.5) - .strong() - .color(accent), - ); - ui.label( - egui::RichText::new(format!("(Installed: v{})", crate::updater::CURRENT_VERSION)) - .size(10.5) - .color(Color32::from_rgb(150, 150, 155)), - ); - ui.add_space(8.0); - if squared_button(ui, "DOWNLOAD", true, accent) { - crate::updater::open_browser_url(&info.html_url); - } - ui.add_space(4.0); - if squared_button(ui, "DISMISS", false, accent) { - self.update_dismissed = true; - } - }); - }); - }); - ui.add_space(8.0); - } + let textures = self.textures.clone(); + let tex_replay = textures.as_ref().map(|t| &t.replay); + let tex_record = textures.as_ref().map(|t| &t.record); + let tex_stream = textures.as_ref().map(|t| &t.stream); + let tex_screenshot = textures.as_ref().map(|t| &t.screenshot); + let tex_settings_small = textures.as_ref().map(|t| &t.settings_small); + let tex_settings_xs = textures.as_ref().map(|t| &t.settings_extra_small); + let tex_play = textures.as_ref().map(|t| &t.play); + let tex_pause = textures.as_ref().map(|t| &t.pause); + let tex_stop = textures.as_ref().map(|t| &t.stop); + let tex_save = textures.as_ref().map(|t| &t.save); - ui.horizontal_top(|ui| { - ui.spacing_mut().item_spacing = Vec2::new(card_gap, 0.0); + // Card 0: Instant Replay + let c0_clicked = render_gsr_card( + ui, + card0_rect, + "Instant Replay", + tex_replay, + |painter, center| { + draw_replay_icon(painter, center, 24.0, is_replay_active, accent); + }, + if is_replay_active { "Turned on" } else { "Off" }, + is_replay_active, + self.replay_dropdown_open, + accent, + ); + if c0_clicked { + self.replay_dropdown_open = !self.replay_dropdown_open; + self.record_dropdown_open = false; + self.stream_dropdown_open = false; + } - // ========================================================================= - // CARD 1: INSTANT REPLAY - // ========================================================================= - ui.vertical(|ui| { - ui.set_width(card_w); - ui.set_min_width(card_w); - ui.set_max_width(card_w); - let card1_clicked = render_action_card( - ui, - card_w, - card_h, - "INSTANT REPLAY", - is_replay_active, - self.replay_dropdown_open, - |painter, center| { - draw_replay_icon(painter, center, 24.0, is_replay_active, accent); - }, - if is_replay_active { "Turned on" } else { "Turned off" }, - if is_replay_active { accent } else { Color32::from_rgb(150, 150, 155) }, - "(Click for menu)", - accent, - ); + // Card 1: Record + let rec_status_str = if is_recording { + let mins = rec_dur / 60; + let secs = rec_dur % 60; + format!("Recording {:02}:{:02}", mins, secs) + } else { + "Not recording".to_string() + }; + let c1_clicked = render_gsr_card( + ui, + card1_rect, + "Record", + tex_record, + |painter, center| { + draw_record_icon(painter, center, 24.0, is_recording, self.anim_time); + }, + &rec_status_str, + is_recording, + self.record_dropdown_open, + accent, + ); + if c1_clicked { + self.record_dropdown_open = !self.record_dropdown_open; + self.replay_dropdown_open = false; + self.stream_dropdown_open = false; + } - if card1_clicked { - self.replay_dropdown_open = !self.replay_dropdown_open; - self.record_dropdown_open = false; - } + // Card 2: Livestream + let c2_clicked = render_gsr_card( + ui, + card2_rect, + "Livestream", + tex_stream, + |painter, center| { + draw_settings_icon(painter, center, 24.0, Color32::WHITE); + }, + "Not streaming", + false, + self.stream_dropdown_open, + accent, + ); + if c2_clicked { + self.stream_dropdown_open = !self.stream_dropdown_open; + self.replay_dropdown_open = false; + self.record_dropdown_open = false; + } - if self.replay_dropdown_open { - render_dropdown_menu(ui, card_w, accent, |ui| { - let toggle_title = if is_replay_active { "Turn off" } else { "Turn on" }; - let toggle_sub = if is_replay_active { "Stop background buffer" } else { "Capture continuous replay" }; - if render_menu_item(ui, toggle_title, Some(toggle_sub), None, is_replay_active, accent, false) { - let mut cfg = ScytheConfig::load(); - cfg.replay_enabled = !cfg.replay_enabled; - let _ = cfg.save(); - ScytheConfig::notify_daemon_reload(); - self.config.replay_enabled = cfg.replay_enabled; - self.status.is_replay_active = cfg.replay_enabled; - self.replay_dropdown_open = false; - } - let save_hk = self.config.save_hotkey.trim(); - let save_hk_opt = if save_hk.is_empty() { None } else { Some(save_hk) }; - if render_menu_item(ui, "Save Replay", Some("Export buffer to file"), save_hk_opt, false, accent, true) { - if is_replay_active { - async_send_command(Command::SaveReplay); - self.show_hud_notification("INSTANT REPLAY", "Saved to Videos", crate::overlay::ToastIcon::Replay); - } else { - self.show_hud_notification("INSTANT REPLAY", "Replay is turned off", crate::overlay::ToastIcon::Error); - } - self.replay_dropdown_open = false; - } - }); - } - }); + // Auxiliary buttons (Screenshot & Settings) + let aux_top_y = top_pad + (card_h - 2.0 * aux_size - 10.0) * 0.5; + let aux_screenshot_rect = egui::Rect::from_min_size(egui::pos2(aux_x, aux_top_y), Vec2::new(aux_size, aux_size)); + let aux_settings_rect = egui::Rect::from_min_size(egui::pos2(aux_x, aux_top_y + aux_size + 10.0), Vec2::new(aux_size, aux_size)); - // ========================================================================= - // CARD 2: RECORD - // ========================================================================= - ui.vertical(|ui| { - ui.set_width(card_w); - ui.set_min_width(card_w); - ui.set_max_width(card_w); - let rec_status_str = if is_recording { - let mins = rec_dur / 60; - let secs = rec_dur % 60; - format!("Recording {:02}:{:02}", mins, secs) - } else { - "Not recording".to_string() - }; + // Screenshot auxiliary button + let sc_resp = ui.allocate_rect(aux_screenshot_rect, egui::Sense::click()); + let sc_hov = sc_resp.hovered(); + let set_resp = ui.allocate_rect(aux_settings_rect, egui::Sense::click()); + let set_hov = set_resp.hovered(); - let card2_clicked = render_action_card( - ui, - card_w, - card_h, - "RECORD", - is_recording, - self.record_dropdown_open, - |painter, center| { - draw_record_icon(painter, center, 24.0, is_recording, anim_time); - }, - &rec_status_str, - if is_recording { Color32::from_rgb(239, 68, 68) } else { Color32::from_rgb(150, 150, 155) }, - "(Click for menu)", - accent, - ); + let p = ui.painter(); + if sc_hov { + p.rect_filled(aux_screenshot_rect, CornerRadius::ZERO, Color32::from_rgb(0, 0, 0)); + p.rect_stroke(aux_screenshot_rect, CornerRadius::ZERO, Stroke::new(2.0_f32, accent), egui::StrokeKind::Inside); + } else { + p.rect_filled(aux_screenshot_rect, CornerRadius::ZERO, Color32::from_rgba_unmultiplied(0, 0, 0, 180)); + } + if let Some(tex) = tex_screenshot { + draw_texture_centered(p, tex, aux_screenshot_rect.center(), 32.0, if sc_hov { accent } else { Color32::WHITE }); + } - if card2_clicked { - self.record_dropdown_open = !self.record_dropdown_open; - self.replay_dropdown_open = false; - } + if set_hov { + p.rect_filled(aux_settings_rect, CornerRadius::ZERO, Color32::from_rgb(0, 0, 0)); + p.rect_stroke(aux_settings_rect, CornerRadius::ZERO, Stroke::new(2.0_f32, accent), egui::StrokeKind::Inside); + } else { + p.rect_filled(aux_settings_rect, CornerRadius::ZERO, Color32::from_rgba_unmultiplied(0, 0, 0, 180)); + } + if let Some(tex) = tex_settings_small { + draw_texture_centered(p, tex, aux_settings_rect.center(), 32.0, if set_hov { accent } else { Color32::WHITE }); + } - if self.record_dropdown_open { - render_dropdown_menu(ui, card_w, accent, |ui| { - let rec_toggle_title = if is_recording { "Stop Recording" } else { "Start Recording" }; - let rec_toggle_sub = if is_recording { "Finalize and save video" } else { "Capture video to disk" }; - let rec_hk = self.config.record_hotkey.trim(); - let rec_hk_opt = if rec_hk.is_empty() { None } else { Some(rec_hk) }; - if render_menu_item(ui, rec_toggle_title, Some(rec_toggle_sub), rec_hk_opt, is_recording, accent, true) { - async_send_command(Command::ToggleRecording); - if is_recording { - self.show_hud_notification("RECORDING", "Recording saved", crate::overlay::ToastIcon::Save); - } else { - self.show_hud_notification("RECORDING", "Recording started", crate::overlay::ToastIcon::Record); - } - self.record_dropdown_open = false; - } - }); - } - }); + if sc_resp.clicked() { + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_secs(); + let fname = format!("Screenshot_{}.png", now); + let out_path = PathBuf::from(&self.output_dir).join(&fname); + let _ = std::process::Command::new("grim").arg(&out_path).spawn(); + self.show_hud_notification("SCREENSHOT", &format!("Saved to {}", fname), crate::overlay::ToastIcon::Screenshot); + } - // ========================================================================= - // CARD 3: SETTINGS - // ========================================================================= - ui.vertical(|ui| { - ui.set_width(card_w); - ui.set_min_width(card_w); - ui.set_max_width(card_w); - let card3_clicked = render_action_card( - ui, - card_w, - card_h, - "SETTINGS", - false, - false, - |painter, center| { - draw_settings_icon(painter, center, 24.0, Color32::from_rgb(150, 150, 155)); - }, - "Hardware & Tuning", - Color32::from_rgb(150, 150, 155), - "(Click to open)", - accent, - ); + if set_resp.clicked() { + self.replay_dropdown_open = false; + self.record_dropdown_open = false; + self.stream_dropdown_open = false; + self.switch_view(ShadowPlayView::GlobalSettings, ctx); + } - if card3_clicked { - self.replay_dropdown_open = false; - self.record_dropdown_open = false; - self.switch_view(ShadowPlayView::Settings, ctx); - } - }); - }); - }); - }); + // Dropdown menus flush beneath cards + let item_h = 44.0_f32; + + // Instant Replay dropdown + if self.replay_dropdown_open { + let drop_y = card0_rect.bottom(); + let r0 = egui::Rect::from_min_size(egui::pos2(card0_rect.left(), drop_y), Vec2::new(card_w, item_h)); + let r1 = egui::Rect::from_min_size(egui::pos2(card0_rect.left(), drop_y + item_h), Vec2::new(card_w, item_h)); + let r2 = egui::Rect::from_min_size(egui::pos2(card0_rect.left(), drop_y + item_h * 2.0), Vec2::new(card_w, item_h)); + let r3 = egui::Rect::from_min_size(egui::pos2(card0_rect.left(), drop_y + item_h * 3.0), Vec2::new(card_w, item_h)); + let r4 = egui::Rect::from_min_size(egui::pos2(card0_rect.left(), drop_y + item_h * 4.0), Vec2::new(card_w, item_h)); + + let toggle_title = if is_replay_active { "Turn off" } else { "Turn on" }; + let toggle_icon = if is_replay_active { tex_stop } else { tex_play }; + if render_dropdown_item(ui, r0, toggle_title, None, toggle_icon, accent, false) { + let mut cfg = ScytheConfig::load(); + cfg.replay_enabled = !cfg.replay_enabled; + let _ = cfg.save(); + ScytheConfig::notify_daemon_reload(); + self.config.replay_enabled = cfg.replay_enabled; + self.status.is_replay_active = cfg.replay_enabled; + self.replay_dropdown_open = false; + } + + let save_hk = self.config.save_hotkey.trim(); + let save_hk_opt = if save_hk.is_empty() { None } else { Some(save_hk) }; + if render_dropdown_item(ui, r1, "Save", save_hk_opt, tex_save, accent, false) { + if is_replay_active { + async_send_command(Command::SaveReplay); + self.show_hud_notification("INSTANT REPLAY", "Saved to Videos", crate::overlay::ToastIcon::Replay); + } else { + self.show_hud_notification("INSTANT REPLAY", "Replay is turned off", crate::overlay::ToastIcon::Error); + } + self.replay_dropdown_open = false; + } + + if render_dropdown_item(ui, r2, "Save 1 min", None, tex_save, accent, false) { + if is_replay_active { + async_send_command(Command::SaveReplay); + self.show_hud_notification("INSTANT REPLAY", "Saved 1 min replay", crate::overlay::ToastIcon::Replay); + } else { + self.show_hud_notification("INSTANT REPLAY", "Replay is turned off", crate::overlay::ToastIcon::Error); + } + self.replay_dropdown_open = false; + } + + if render_dropdown_item(ui, r3, "Save 10 min", None, tex_save, accent, false) { + if is_replay_active { + async_send_command(Command::SaveReplay); + self.show_hud_notification("INSTANT REPLAY", "Saved 10 min replay", crate::overlay::ToastIcon::Replay); + } else { + self.show_hud_notification("INSTANT REPLAY", "Replay is turned off", crate::overlay::ToastIcon::Error); + } + self.replay_dropdown_open = false; + } + + if render_dropdown_item(ui, r4, "Settings", None, tex_settings_xs, accent, true) { + self.switch_view(ShadowPlayView::ReplaySettings, ctx); + } + } + + // Record dropdown + if self.record_dropdown_open { + let drop_y = card1_rect.bottom(); + let r0 = egui::Rect::from_min_size(egui::pos2(card1_rect.left(), drop_y), Vec2::new(card_w, item_h)); + let r1 = egui::Rect::from_min_size(egui::pos2(card1_rect.left(), drop_y + item_h), Vec2::new(card_w, item_h)); + let r2 = egui::Rect::from_min_size(egui::pos2(card1_rect.left(), drop_y + item_h * 2.0), Vec2::new(card_w, item_h)); + + let rec_toggle_title = if is_recording { "Stop Recording" } else { "Start Recording" }; + let rec_toggle_icon = if is_recording { tex_stop } else { tex_play }; + let rec_hk = self.config.record_hotkey.trim(); + let rec_hk_opt = if rec_hk.is_empty() { None } else { Some(rec_hk) }; + if render_dropdown_item(ui, r0, rec_toggle_title, rec_hk_opt, rec_toggle_icon, accent, false) { + async_send_command(Command::ToggleRecording); + if is_recording { + self.show_hud_notification("RECORDING", "Recording saved", crate::overlay::ToastIcon::Save); + } else { + self.show_hud_notification("RECORDING", "Recording started", crate::overlay::ToastIcon::Record); + } + self.record_dropdown_open = false; + } + + let pause_hk_opt: Option<&str> = None; + if render_dropdown_item(ui, r1, "Pause", pause_hk_opt, tex_pause, accent, false) { + self.record_dropdown_open = false; + } + + if render_dropdown_item(ui, r2, "Settings", None, tex_settings_xs, accent, true) { + self.switch_view(ShadowPlayView::RecordSettings, ctx); + } + } + + // Livestream dropdown + if self.stream_dropdown_open { + let drop_y = card2_rect.bottom(); + let r0 = egui::Rect::from_min_size(egui::pos2(card2_rect.left(), drop_y), Vec2::new(card_w, item_h)); + let r1 = egui::Rect::from_min_size(egui::pos2(card2_rect.left(), drop_y + item_h), Vec2::new(card_w, item_h)); + + if render_dropdown_item(ui, r0, "Start Streaming", None, tex_play, accent, false) { + self.show_hud_notification("LIVESTREAM", "Streaming not configured", crate::overlay::ToastIcon::Info); + self.stream_dropdown_open = false; + } + + if render_dropdown_item(ui, r1, "Settings", None, tex_settings_xs, accent, true) { + self.switch_view(ShadowPlayView::StreamSettings, ctx); + } + } } fn render_settings_view(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { let screen_w = ui.available_width(); let screen_h = ui.available_height(); - let modal_w = 680.0_f32; - let modal_h = (screen_h - 40.0).clamp(660.0, 920.0); + let modal_w = 700.0_f32.min(screen_w - 40.0); + let modal_h = (screen_h - 90.0).clamp(560.0, 920.0); let left_pad = ((screen_w - modal_w) / 2.0).max(10.0); - let top_pad = ((screen_h - modal_h) / 2.0).max(15.0); + let top_pad = 58.0_f32; let accent = self.accent_color(); let modal_rect = egui::Rect::from_min_size(egui::pos2(left_pad, top_pad), egui::vec2(modal_w, modal_h)); self.panel_rect = modal_rect; + let textures = self.textures.clone(); + let (cat_title, cat_desc, cat_tex) = match self.current_view { + ShadowPlayView::ReplaySettings => ( + "INSTANT REPLAY", + "Save a video clip of your gameplay that just happened.", + textures.as_ref().map(|t| t.replay.clone()), + ), + ShadowPlayView::RecordSettings => ( + "RECORD", + "Record video of your desktop or games to a file.", + textures.as_ref().map(|t| t.record.clone()), + ), + ShadowPlayView::StreamSettings => ( + "LIVESTREAM", + "Broadcast live to Twitch, YouTube, or custom RTMP.", + textures.as_ref().map(|t| t.stream.clone()), + ), + ShadowPlayView::GlobalSettings => ( + "SETTINGS", + "Configure general application settings, hotkeys, and appearance.", + textures.as_ref().map(|t| t.settings_large.clone()), + ), + ShadowPlayView::ScreenshotSettings => ( + "SCREENSHOT", + "Capture high-resolution screenshots of your desktop.", + textures.as_ref().map(|t| t.screenshot.clone()), + ), + _ => ( + "SETTINGS", + "Configure application settings.", + textures.as_ref().map(|t| t.settings_large.clone()), + ), + }; + ui.allocate_new_ui(egui::UiBuilder::new().max_rect(modal_rect), |ui| { egui::Frame::NONE - .fill(Color32::from_rgba_unmultiplied(11, 12, 15, 252)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 140))) + .fill(Color32::from_rgba_unmultiplied(10, 11, 14, 252)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 120))) .corner_radius(CornerRadius::ZERO) - .inner_margin(Margin::symmetric(24_i8, 20_i8)) + .inner_margin(Margin::symmetric(24_i8, 18_i8)) .show(ui, |ui| { ui.set_width(modal_w - 48.0); - // Portrait Header: Back Button, Title, Close Hint (No X button) - ui.horizontal(|ui| { - let back_btn = egui::Button::new( - egui::RichText::new("< BACK") - .size(11.5) - .strong() - .color(accent), - ) - .fill(Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 24)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 160))) - .corner_radius(CornerRadius::ZERO); + // 3.0px top accent line matching GPU Screen Recorder Page design + let top_line_rect = egui::Rect::from_min_size( + egui::pos2(modal_rect.left(), modal_rect.top()), + Vec2::new(modal_w, 3.0), + ); + ui.painter().rect_filled(top_line_rect, CornerRadius::ZERO, accent); - if ui.add(back_btn).clicked() { - self.switch_view(ShadowPlayView::MainHud, ctx); - return; + // Page Header with Left Icon Badge (74x74), Title + Description, and Right Back Button + ui.horizontal(|ui| { + let badge_size = 72.0_f32; + let (badge_rect, _) = ui.allocate_exact_size(Vec2::new(badge_size, badge_size), egui::Sense::hover()); + ui.painter().rect_filled(badge_rect, CornerRadius::ZERO, Color32::from_rgb(0, 0, 0)); + ui.painter().rect_stroke(badge_rect, CornerRadius::ZERO, Stroke::new(1.5_f32, accent), egui::StrokeKind::Inside); + if let Some(tex) = &cat_tex { + draw_texture_centered(ui.painter(), tex, badge_rect.center(), 40.0, accent); } ui.add_space(14.0); - ui.label( - egui::RichText::new("SETTINGS") - .font(FontId::proportional(15.0)) - .strong() - .color(Color32::WHITE), - ); + ui.vertical(|ui| { + ui.add_space(6.0); + ui.label( + egui::RichText::new(cat_title) + .font(FontId::proportional(17.0)) + .strong() + .color(accent), + ); + ui.add_space(4.0); + ui.label( + egui::RichText::new(cat_desc) + .size(11.5) + .color(Color32::from_rgb(180, 180, 188)), + ); + }); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - egui::Frame::NONE - .fill(Color32::from_rgba_unmultiplied(255, 255, 255, 12)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 22))) - .corner_radius(CornerRadius::ZERO) - .inner_margin(Margin::symmetric(9_i8, 4_i8)) - .show(ui, |ui| { - ui.label( - egui::RichText::new("ESC TO CLOSE") - .size(10.5) - .strong() - .color(Color32::from_rgb(160, 160, 168)), - ); - }); + let back_btn = egui::Button::new( + egui::RichText::new("< BACK") + .size(12.0) + .strong() + .color(accent), + ) + .fill(Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 24)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 160))) + .corner_radius(CornerRadius::ZERO) + .min_size(Vec2::new(96.0, 34.0)); + + if ui.add(back_btn).clicked() { + self.switch_view(ShadowPlayView::MainHud, ctx); + return; + } }); }); - ui.add_space(14.0); + ui.add_space(12.0); + ui.separator(); + ui.add_space(8.0); - let scroll_h = modal_h - 115.0; + let scroll_h = modal_h - 170.0; egui::ScrollArea::vertical() .max_height(scroll_h) .auto_shrink([false, false]) .show(ui, |ui| { - // ----------------------------------------------------------------- - // SECTION 1: SYSTEM & STARTUP - // ----------------------------------------------------------------- - render_section_card(ui, "SYSTEM & STARTUP", accent, |ui| { - // Autostart Instant Replay - ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.label(egui::RichText::new("Autostart Instant Replay").size(12.5).strong().color(Color32::WHITE)); - ui.label(egui::RichText::new("Automatically launch the background recording engine on login so replay is always ready.").size(10.5).color(Color32::from_rgb(150, 150, 155))); - }); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let mut ar = self.autostart_replay; - if toggle_switch(ui, &mut ar, accent).changed() { - self.autostart_replay = ar; - self.config.autostart_replay = ar; - self.config.autostart = ar; - let _ = self.config.save(); - if ar { - spawn_daemon_process(); - } - self.show_hud_notification( - "AUTOSTART REPLAY", - if ar { "Enabled: Starts on login" } else { "Disabled" }, - crate::overlay::ToastIcon::Info, + match self.current_view { + ShadowPlayView::ReplaySettings => { + // 1. Replay Buffer & Autostart + render_section_card(ui, "INSTANT REPLAY BUFFER", accent, |ui| { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.label(egui::RichText::new("Autostart Instant Replay").size(12.5).strong().color(Color32::WHITE)); + ui.label(egui::RichText::new("Automatically launch recording engine on login so instant replay is always active.").size(10.5).color(Color32::from_rgb(150, 150, 155))); + }); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let mut ar = self.autostart_replay; + if toggle_switch(ui, &mut ar, accent).changed() { + self.autostart_replay = ar; + self.config.autostart_replay = ar; + self.config.autostart = ar; + let _ = self.config.save(); + if ar { + spawn_daemon_process(); + } + self.show_hud_notification( + "AUTOSTART REPLAY", + if ar { "Enabled: Starts on login" } else { "Disabled" }, + crate::overlay::ToastIcon::Info, + ); + } + }); + }); + + ui.add_space(8.0); + ui.separator(); + ui.add_space(8.0); + + ui.label(egui::RichText::new("Replay Buffer Duration:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Custom (sec):").size(11.0).color(Color32::from_rgb(150, 150, 155))); + let edit_resp = ui.add( + egui::TextEdit::singleline(&mut self.replay_sec_input_str) + .desired_width(55.0) + .font(FontId::monospace(11.5)) ); - } - }); - }); + if edit_resp.changed() + && let Ok(parsed) = self.replay_sec_input_str.trim().parse::() + && (5..=1800).contains(&parsed) { + self.replay_sec = parsed; + } + ui.label(egui::RichText::new("sec").size(10.5).color(Color32::from_rgb(150, 150, 155))); - ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); - - // Autostart HUD Overlay - ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.label(egui::RichText::new("Autostart HUD Overlay").size(12.5).strong().color(Color32::WHITE)); - ui.label(egui::RichText::new("Automatically open the HUD overlay menu when logging into your desktop session.").size(10.5).color(Color32::from_rgb(150, 150, 155))); - }); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let mut ao = self.autostart_overlay; - if toggle_switch(ui, &mut ao, accent).changed() { - self.autostart_overlay = ao; - self.config.autostart_overlay = ao; - let _ = self.config.save(); - self.show_hud_notification( - "AUTOSTART OVERLAY", - if ao { "Enabled: Opens on login" } else { "Disabled" }, - crate::overlay::ToastIcon::Info, - ); - } - }); - }); - }); - - ui.add_space(10.0); - - // ----------------------------------------------------------------- - // SECTION 2: DISPLAY & CAPTURE - // ----------------------------------------------------------------- - render_section_card(ui, "DISPLAY & CAPTURE", accent, |ui| { - // Record Mouse Cursor - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Record Mouse Cursor").size(12.0).strong().color(Color32::WHITE)); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - let mut cur = self.show_cursor; - if toggle_switch(ui, &mut cur, accent).changed() { - self.show_cursor = cur; - self.config.show_cursor = cur; - let _ = self.config.save(); - async_send_command(Command::ToggleCursor); - self.show_hud_notification( - "MOUSE CURSOR", - if cur { "Visible in recording" } else { "Hidden from recording" }, - crate::overlay::ToastIcon::Cursor, - ); - } - }); - }); - - ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); - - // Framerate (FPS) with direct input and presets - ui.label(egui::RichText::new("Target Framerate (FPS):").size(12.0).strong().color(Color32::WHITE)); - ui.add_space(4.0); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Custom:").size(11.0).color(Color32::from_rgb(150, 150, 155))); - let edit_resp = ui.add( - egui::TextEdit::singleline(&mut self.fps_input_str) - .desired_width(55.0) - .font(FontId::monospace(11.5)) - ); - if edit_resp.changed() - && let Ok(parsed) = self.fps_input_str.trim().parse::() - && (15..=360).contains(&parsed) { - self.target_fps = parsed; - } - - ui.add_space(8.0); - for fps in [30, 60, 120, 144, 240] { - if squared_button(ui, &fps.to_string(), self.target_fps == fps, accent) { - self.target_fps = fps; - self.fps_input_str = fps.to_string(); - } - } - }); - - ui.add_space(8.0); - - // Video Bitrate (Mbps) with direct input, presets, and slider - ui.label(egui::RichText::new("Video Bitrate:").size(12.0).strong().color(Color32::WHITE)); - ui.add_space(4.0); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Custom:").size(11.0).color(Color32::from_rgb(150, 150, 155))); - let edit_resp = ui.add( - egui::TextEdit::singleline(&mut self.bitrate_input_str) - .desired_width(55.0) - .font(FontId::monospace(11.5)) - ); - if edit_resp.changed() - && let Ok(parsed) = self.bitrate_input_str.trim().parse::() - && (1..=300).contains(&parsed) { - self.bitrate_mbps = parsed; - } - ui.label(egui::RichText::new("Mbps").size(10.5).color(Color32::from_rgb(150, 150, 155))); - - ui.add_space(8.0); - for mbps in [10, 20, 35, 50, 80] { - if squared_button(ui, &format!("{}M", mbps), self.bitrate_mbps == mbps, accent) { - self.bitrate_mbps = mbps; - self.bitrate_input_str = mbps.to_string(); - } - } - ui.add_space(6.0); - let mut br = self.bitrate_mbps; - if ui.add(egui::Slider::new(&mut br, 5..=150).suffix(" Mbps").step_by(5.0)).changed() { - self.bitrate_mbps = br; - self.bitrate_input_str = br.to_string(); - } - }); - - ui.add_space(8.0); - - // Instant Replay Buffer Duration with direct input and presets - ui.label(egui::RichText::new("Instant Replay Buffer Duration:").size(12.0).strong().color(Color32::WHITE)); - ui.add_space(4.0); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Custom (sec):").size(11.0).color(Color32::from_rgb(150, 150, 155))); - let edit_resp = ui.add( - egui::TextEdit::singleline(&mut self.replay_sec_input_str) - .desired_width(55.0) - .font(FontId::monospace(11.5)) - ); - if edit_resp.changed() - && let Ok(parsed) = self.replay_sec_input_str.trim().parse::() - && (5..=1800).contains(&parsed) { - self.replay_sec = parsed; - } - ui.label(egui::RichText::new("sec").size(10.5).color(Color32::from_rgb(150, 150, 155))); - - ui.add_space(8.0); - for sec in [15, 30, 60, 120, 300] { - let label = if sec >= 60 { format!("{}m", sec / 60) } else { format!("{}s", sec) }; - if squared_button(ui, &label, self.replay_sec == sec, accent) { - self.replay_sec = sec; - self.replay_sec_input_str = sec.to_string(); - } - } - }); - - ui.add_space(8.0); - - // Encoder Codec - ui.label(egui::RichText::new("Encoder Codec:").size(12.0).strong().color(Color32::WHITE)); - ui.add_space(4.0); - ui.horizontal(|ui| { - for (codec_key, label) in [("h264", "H.264"), ("hevc", "HEVC / H.265"), ("av1", "AV1")] { - if squared_button(ui, label, self.video_codec == codec_key, accent) { - self.video_codec = codec_key.to_string(); - } - } - }); - }); - - ui.add_space(10.0); - - // ----------------------------------------------------------------- - // SECTION 2: AUDIO CONFIGURATION - // ----------------------------------------------------------------- - render_section_card(ui, "AUDIO CONFIGURATION", accent, |ui| { - ui.label(egui::RichText::new("Audio Source:").size(12.0).strong().color(Color32::WHITE)); - ui.add_space(4.0); - ui.horizontal(|ui| { - let modes = [("system", "System"), ("mic", "Microphone"), ("both", "Both"), ("muted", "Muted")]; - for (idx, (_, m_label)) in modes.iter().enumerate() { - if squared_button(ui, m_label, self.audio_mode_idx == idx, accent) { - self.audio_mode_idx = idx; - } - } - }); - - ui.add_space(8.0); - - // Mic Volume + VU - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Mic Vol:").size(12.0).strong().color(Color32::WHITE)); - let mut mv = self.mic_volume_pct; - if ui.add(egui::Slider::new(&mut mv, 0..=200).suffix("%")).changed() { - self.mic_volume_pct = mv; - } - ui.add_space(6.0); - render_vu_meter(ui, self.mic_vu, 65.0, 18.0, "MIC"); - }); - - ui.add_space(8.0); - - // System Audio Volume + VU - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Sys Vol:").size(12.0).strong().color(Color32::WHITE)); - let mut sv = self.system_volume_pct; - if ui.add(egui::Slider::new(&mut sv, 0..=200).suffix("%")).changed() { - self.system_volume_pct = sv; - } - ui.add_space(6.0); - render_vu_meter(ui, self.sys_vu, 65.0, 18.0, "SYS"); - }); - }); - - ui.add_space(10.0); - - // ----------------------------------------------------------------- - // SECTION 3: KEYBOARD SHORTCUTS (INTERACTIVE REBINDING) - // ----------------------------------------------------------------- - render_section_card(ui, "KEYBOARD SHORTCUTS (CLICK TO REBIND)", accent, |ui| { - let binds = [ - (KeybindAction::Menu, "Menu Overlay", &self.config.menu_hotkey), - (KeybindAction::SaveReplay, "Save Instant Replay", &self.config.save_hotkey), - (KeybindAction::ToggleRecord, "Start / Stop Record", &self.config.record_hotkey), - (KeybindAction::ToggleCursor, "Toggle Mouse Cursor", &self.config.cursor_hotkey), - ]; - - for (action, label, current_hotkey) in binds { - let is_listening = self.listening_keybind == Some(action); - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(label) - .size(12.0) - .color(if is_listening { accent } else { Color32::from_rgb(203, 213, 225) }), - ); - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if render_keycap_button(ui, current_hotkey, is_listening, accent) { - if is_listening { - self.listening_keybind = None; - } else { - self.listening_keybind = Some(action); + ui.add_space(8.0); + for sec in [15, 30, 60, 120, 300, 600] { + let label = if sec >= 60 { format!("{}m", sec / 60) } else { format!("{}s", sec) }; + if squared_button(ui, &label, self.replay_sec == sec, accent) { + self.replay_sec = sec; + self.replay_sec_input_str = sec.to_string(); } } }); }); - ui.add_space(6.0); - } - if self.listening_keybind.is_some() { - ui.add_space(4.0); - ui.label( - egui::RichText::new("Listening... Press your desired key combination or Esc to cancel.") - .size(11.0) - .strong() - .color(accent), - ); - } - - ui.add_space(4.0); - ui.horizontal(|ui| { - if squared_button(ui, "Reset Hotkeys to Defaults", false, accent) { - let old_menu = self.config.menu_hotkey.clone(); - let old_save = self.config.save_hotkey.clone(); - let old_rec = self.config.record_hotkey.clone(); - let old_cur = self.config.cursor_hotkey.clone(); - crate::hyprland_binds::unbind_hotkey_async(&old_menu); - crate::hyprland_binds::unbind_hotkey_async(&old_save); - crate::hyprland_binds::unbind_hotkey_async(&old_rec); - crate::hyprland_binds::unbind_hotkey_async(&old_cur); - self.config.menu_hotkey = "Alt+Z".to_string(); - self.config.save_hotkey = "Ctrl+Shift+R".to_string(); - self.config.record_hotkey = "Ctrl+Shift+F9".to_string(); - self.config.cursor_hotkey = "Ctrl+Shift+F10".to_string(); - let _ = self.config.save(); - crate::hyprland_binds::register_hyprland_binds_async(&self.config); - crate::config::ScytheConfig::notify_daemon_reload(); - self.show_hud_notification("KEYBINDS", "Restored default hotkeys", crate::overlay::ToastIcon::Info); - } - }); - }); - - ui.add_space(10.0); - - // ----------------------------------------------------------------- - // SECTION 5: STORAGE & FILE NAMING - // ----------------------------------------------------------------- - render_section_card(ui, "STORAGE & FILE NAMING", accent, |ui| { - ui.label(egui::RichText::new("Save Folder:").size(12.0).strong().color(Color32::WHITE)); - ui.add_space(3.0); - ui.horizontal(|ui| { - ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(340.0)); - if squared_button(ui, "Change", false, accent) { - pick_folder(&self.output_dir, self.folder_tx.clone(), self.folder_picking_active.clone()); - } - if squared_button(ui, "Open", false, accent) { - open_folder(&ScytheConfig::expand_tilde(&self.output_dir)); - } - }); - - ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); - - ui.label(egui::RichText::new("Default File Naming Format:").size(12.0).strong().color(Color32::WHITE)); - ui.add_space(3.0); - let sample_replay = ScytheConfig::format_video_filename("Replay", "mp4"); - let sample_record = ScytheConfig::format_video_filename("Recording", "mp4"); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Template:").size(11.0).color(Color32::from_rgb(150, 150, 155))); - ui.label(egui::RichText::new("Prefix-Timestamp_Date_Month_Year.mp4").size(11.0).font(FontId::monospace(11.0)).color(accent)); - }); - ui.add_space(2.0); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Instant Replay:").size(11.0).color(Color32::from_rgb(150, 150, 155))); - ui.label(egui::RichText::new(&sample_replay).size(11.0).font(FontId::monospace(11.0)).color(Color32::WHITE)); - }); - ui.add_space(2.0); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Manual Record:").size(11.0).color(Color32::from_rgb(150, 150, 155))); - ui.label(egui::RichText::new(&sample_record).size(11.0).font(FontId::monospace(11.0)).color(Color32::WHITE)); - }); - }); - - ui.add_space(10.0); - - // ----------------------------------------------------------------- - // SECTION 5: ACCENT COLOR THEME (AT THE BOTTOM) - // ----------------------------------------------------------------- - render_section_card(ui, "ACCENT COLOR THEME", accent, |ui| { - ui.label(egui::RichText::new("Select Interface Accent Color:").size(12.0).strong().color(Color32::WHITE)); - ui.add_space(6.0); - ui.horizontal_wrapped(|ui| { - let palettes = [ - ("blue", "Charming Blue", Color32::from_rgb(56, 189, 248)), - ("green", "Emerald Green", Color32::from_rgb(34, 197, 94)), - ("lime", "Electric Lime", Color32::from_rgb(163, 230, 53)), - ("yellow", "Solar Yellow", Color32::from_rgb(250, 204, 21)), - ("amber", "Sunset Amber", Color32::from_rgb(245, 158, 11)), - ("red", "Crimson Red", Color32::from_rgb(239, 68, 68)), - ("pink", "Neon Pink", Color32::from_rgb(244, 63, 94)), - ("purple", "Royal Purple", Color32::from_rgb(168, 85, 247)), - ]; - - for (id, name, col) in palettes { - let is_sel = self.config.accent_color.to_lowercase() == id; - let bg = if is_sel { - Color32::from_rgba_unmultiplied(col.r(), col.g(), col.b(), 50) - } else { - Color32::from_rgba_unmultiplied(col.r(), col.g(), col.b(), 16) - }; - // Colored border with each color's respective look - let stroke = Stroke::new(if is_sel { 2.0_f32 } else { 1.2_f32 }, col); - - let btn = egui::Button::new( - egui::RichText::new(name) - .size(11.5) - .strong() - .color(if is_sel { Color32::WHITE } else { Color32::from_rgb(226, 232, 240) }), - ) - .fill(bg) - .stroke(stroke) - .corner_radius(CornerRadius::ZERO) - .min_size(Vec2::new(140.0, 30.0)); - - if ui.add(btn).clicked() { - self.config.accent_color = id.to_string(); - let _ = self.config.save(); - self.show_hud_notification("THEME ACCENT", &format!("Selected: {}", name), crate::overlay::ToastIcon::Info); - } - } - }); - }); - - ui.add_space(10.0); - - // ----------------------------------------------------------------- - // SECTION 6: ABOUT & AUTO-UPDATES - // ----------------------------------------------------------------- - render_section_card(ui, "ABOUT & AUTO-UPDATES", accent, |ui| { - ui.horizontal(|ui| { - ui.label( - egui::RichText::new(format!("Installed Version: v{}", crate::updater::CURRENT_VERSION)) - .size(12.0) - .strong() - .color(Color32::WHITE), - ); ui.add_space(10.0); - let cur_status = self.update_status.lock().ok().map(|g| g.clone()).unwrap_or_default(); - match cur_status { - crate::updater::UpdateStatus::Idle => { - if squared_button(ui, "Check for Updates", false, accent) { - crate::updater::spawn_update_check(self.update_status.clone()); + // 2. Video Quality & Codec + render_section_card(ui, "VIDEO QUALITY & CODEC", accent, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Record Mouse Cursor").size(12.0).strong().color(Color32::WHITE)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let mut cur = self.show_cursor; + if toggle_switch(ui, &mut cur, accent).changed() { + self.show_cursor = cur; + self.config.show_cursor = cur; + let _ = self.config.save(); + async_send_command(Command::ToggleCursor); + self.show_hud_notification( + "MOUSE CURSOR", + if cur { "Visible in recording" } else { "Hidden from recording" }, + crate::overlay::ToastIcon::Cursor, + ); + } + }); + }); + + ui.add_space(8.0); + ui.separator(); + ui.add_space(8.0); + + ui.label(egui::RichText::new("Target Framerate (FPS):").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Custom:").size(11.0).color(Color32::from_rgb(150, 150, 155))); + let edit_resp = ui.add( + egui::TextEdit::singleline(&mut self.fps_input_str) + .desired_width(55.0) + .font(FontId::monospace(11.5)) + ); + if edit_resp.changed() + && let Ok(parsed) = self.fps_input_str.trim().parse::() + && (15..=360).contains(&parsed) { + self.target_fps = parsed; + } + + ui.add_space(8.0); + for fps in [30, 60, 120, 144, 240] { + if squared_button(ui, &fps.to_string(), self.target_fps == fps, accent) { + self.target_fps = fps; + self.fps_input_str = fps.to_string(); + } } - } - crate::updater::UpdateStatus::Checking => { - ui.label( - egui::RichText::new("Checking for updates...") - .size(11.5) - .color(Color32::from_rgb(150, 150, 155)), - ); - } - crate::updater::UpdateStatus::UpToDate { version } => { - ui.label( - egui::RichText::new(format!("✓ Up to date (v{})", version)) - .size(11.5) - .color(Color32::from_rgb(34, 197, 94)) - .strong(), + }); + + ui.add_space(8.0); + + ui.label(egui::RichText::new("Video Bitrate:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Custom:").size(11.0).color(Color32::from_rgb(150, 150, 155))); + let edit_resp = ui.add( + egui::TextEdit::singleline(&mut self.bitrate_input_str) + .desired_width(55.0) + .font(FontId::monospace(11.5)) ); + if edit_resp.changed() + && let Ok(parsed) = self.bitrate_input_str.trim().parse::() + && (1..=300).contains(&parsed) { + self.bitrate_mbps = parsed; + } + ui.label(egui::RichText::new("Mbps").size(10.5).color(Color32::from_rgb(150, 150, 155))); + + ui.add_space(8.0); + for mbps in [10, 20, 35, 50, 80] { + if squared_button(ui, &format!("{}M", mbps), self.bitrate_mbps == mbps, accent) { + self.bitrate_mbps = mbps; + self.bitrate_input_str = mbps.to_string(); + } + } ui.add_space(6.0); - if squared_button(ui, "Check Again", false, accent) { - crate::updater::spawn_update_check(self.update_status.clone()); + let mut br = self.bitrate_mbps; + if ui.add(egui::Slider::new(&mut br, 5..=150).suffix(" Mbps").step_by(5.0)).changed() { + self.bitrate_mbps = br; + self.bitrate_input_str = br.to_string(); + } + }); + + ui.add_space(8.0); + + ui.label(egui::RichText::new("Encoder Codec:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + for (codec_key, label) in [("h264", "H.264"), ("hevc", "HEVC / H.265"), ("av1", "AV1")] { + if squared_button(ui, label, self.video_codec == codec_key, accent) { + self.video_codec = codec_key.to_string(); + } + } + }); + }); + + ui.add_space(10.0); + + // 3. Audio Configuration + render_section_card(ui, "AUDIO CONFIGURATION", accent, |ui| { + ui.label(egui::RichText::new("Audio Source:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + let modes = [("system", "System"), ("mic", "Microphone"), ("both", "Both"), ("muted", "Muted")]; + for (idx, (_, m_label)) in modes.iter().enumerate() { + if squared_button(ui, m_label, self.audio_mode_idx == idx, accent) { + self.audio_mode_idx = idx; + } + } + }); + + ui.add_space(8.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Mic Vol:").size(12.0).strong().color(Color32::WHITE)); + let mut mv = self.mic_volume_pct; + if ui.add(egui::Slider::new(&mut mv, 0..=200).suffix("%")).changed() { + self.mic_volume_pct = mv; } - } - crate::updater::UpdateStatus::Available(info) => { - ui.label( - egui::RichText::new(format!("New version available: v{}", info.version)) - .size(11.5) - .color(Color32::from_rgb(245, 158, 11)) - .strong(), - ); ui.add_space(6.0); - if squared_button(ui, "VIEW RELEASE / DOWNLOAD", true, accent) { - crate::updater::open_browser_url(&info.html_url); + render_vu_meter(ui, self.mic_vu, 65.0, 18.0, "MIC"); + }); + + ui.add_space(8.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Sys Vol:").size(12.0).strong().color(Color32::WHITE)); + let mut sv = self.system_volume_pct; + if ui.add(egui::Slider::new(&mut sv, 0..=200).suffix("%")).changed() { + self.system_volume_pct = sv; } + ui.add_space(6.0); + render_vu_meter(ui, self.sys_vu, 65.0, 18.0, "SYS"); + }); + }); + + ui.add_space(10.0); + + // 4. Storage + render_section_card(ui, "STORAGE & SAVED CLIPS", accent, |ui| { + ui.label(egui::RichText::new("Save Folder:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(3.0); + ui.horizontal(|ui| { + ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(340.0)); + if squared_button(ui, "Change", false, accent) { + pick_folder(&self.output_dir, self.folder_tx.clone(), self.folder_picking_active.clone()); + } + if squared_button(ui, "Open", false, accent) { + open_folder(&ScytheConfig::expand_tilde(&self.output_dir)); + } + }); + }); + } + ShadowPlayView::RecordSettings => { + // Record settings + render_section_card(ui, "VIDEO RECORDING QUALITY", accent, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Record Mouse Cursor").size(12.0).strong().color(Color32::WHITE)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let mut cur = self.show_cursor; + if toggle_switch(ui, &mut cur, accent).changed() { + self.show_cursor = cur; + self.config.show_cursor = cur; + let _ = self.config.save(); + async_send_command(Command::ToggleCursor); + self.show_hud_notification( + "MOUSE CURSOR", + if cur { "Visible in recording" } else { "Hidden from recording" }, + crate::overlay::ToastIcon::Cursor, + ); + } + }); + }); + + ui.add_space(8.0); + ui.separator(); + ui.add_space(8.0); + + ui.label(egui::RichText::new("Target Framerate (FPS):").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Custom:").size(11.0).color(Color32::from_rgb(150, 150, 155))); + let edit_resp = ui.add( + egui::TextEdit::singleline(&mut self.fps_input_str) + .desired_width(55.0) + .font(FontId::monospace(11.5)) + ); + if edit_resp.changed() + && let Ok(parsed) = self.fps_input_str.trim().parse::() + && (15..=360).contains(&parsed) { + self.target_fps = parsed; + } + + ui.add_space(8.0); + for fps in [30, 60, 120, 144, 240] { + if squared_button(ui, &fps.to_string(), self.target_fps == fps, accent) { + self.target_fps = fps; + self.fps_input_str = fps.to_string(); + } + } + }); + + ui.add_space(8.0); + + ui.label(egui::RichText::new("Video Bitrate:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Custom:").size(11.0).color(Color32::from_rgb(150, 150, 155))); + let edit_resp = ui.add( + egui::TextEdit::singleline(&mut self.bitrate_input_str) + .desired_width(55.0) + .font(FontId::monospace(11.5)) + ); + if edit_resp.changed() + && let Ok(parsed) = self.bitrate_input_str.trim().parse::() + && (1..=300).contains(&parsed) { + self.bitrate_mbps = parsed; + } + ui.label(egui::RichText::new("Mbps").size(10.5).color(Color32::from_rgb(150, 150, 155))); + + ui.add_space(8.0); + for mbps in [10, 20, 35, 50, 80] { + if squared_button(ui, &format!("{}M", mbps), self.bitrate_mbps == mbps, accent) { + self.bitrate_mbps = mbps; + self.bitrate_input_str = mbps.to_string(); + } + } + ui.add_space(6.0); + let mut br = self.bitrate_mbps; + if ui.add(egui::Slider::new(&mut br, 5..=150).suffix(" Mbps").step_by(5.0)).changed() { + self.bitrate_mbps = br; + self.bitrate_input_str = br.to_string(); + } + }); + + ui.add_space(8.0); + + ui.label(egui::RichText::new("Encoder Codec:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + for (codec_key, label) in [("h264", "H.264"), ("hevc", "HEVC / H.265"), ("av1", "AV1")] { + if squared_button(ui, label, self.video_codec == codec_key, accent) { + self.video_codec = codec_key.to_string(); + } + } + }); + }); + + ui.add_space(10.0); + + render_section_card(ui, "AUDIO CONFIGURATION", accent, |ui| { + ui.label(egui::RichText::new("Audio Source:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + let modes = [("system", "System"), ("mic", "Microphone"), ("both", "Both"), ("muted", "Muted")]; + for (idx, (_, m_label)) in modes.iter().enumerate() { + if squared_button(ui, m_label, self.audio_mode_idx == idx, accent) { + self.audio_mode_idx = idx; + } + } + }); + + ui.add_space(8.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Mic Vol:").size(12.0).strong().color(Color32::WHITE)); + let mut mv = self.mic_volume_pct; + if ui.add(egui::Slider::new(&mut mv, 0..=200).suffix("%")).changed() { + self.mic_volume_pct = mv; + } + ui.add_space(6.0); + render_vu_meter(ui, self.mic_vu, 65.0, 18.0, "MIC"); + }); + + ui.add_space(8.0); + + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Sys Vol:").size(12.0).strong().color(Color32::WHITE)); + let mut sv = self.system_volume_pct; + if ui.add(egui::Slider::new(&mut sv, 0..=200).suffix("%")).changed() { + self.system_volume_pct = sv; + } + ui.add_space(6.0); + render_vu_meter(ui, self.sys_vu, 65.0, 18.0, "SYS"); + }); + }); + + ui.add_space(10.0); + + render_section_card(ui, "STORAGE & SAVED RECORDINGS", accent, |ui| { + ui.label(egui::RichText::new("Save Folder:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(3.0); + ui.horizontal(|ui| { + ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(340.0)); + if squared_button(ui, "Change", false, accent) { + pick_folder(&self.output_dir, self.folder_tx.clone(), self.folder_picking_active.clone()); + } + if squared_button(ui, "Open", false, accent) { + open_folder(&ScytheConfig::expand_tilde(&self.output_dir)); + } + }); + }); + } + ShadowPlayView::StreamSettings => { + // Livestream settings + render_section_card(ui, "STREAM DESTINATION & CREDENTIALS", accent, |ui| { + ui.label(egui::RichText::new("Streaming Service:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + let services = ["Twitch", "YouTube", "Custom RTMP"]; + for (idx, serv) in services.iter().enumerate() { + if squared_button(ui, serv, self.stream_service_idx == idx, accent) { + self.stream_service_idx = idx; + self.stream_url = match idx { + 0 => "rtmp://live.twitch.tv/app/".to_string(), + 1 => "rtmp://a.rtmp.youtube.com/live2".to_string(), + _ => self.stream_url.clone(), + }; + } + } + }); + + ui.add_space(8.0); + ui.separator(); + ui.add_space(8.0); + + ui.label(egui::RichText::new("Server URL:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(3.0); + ui.add(egui::TextEdit::singleline(&mut self.stream_url).desired_width(420.0)); + + ui.add_space(8.0); + + ui.label(egui::RichText::new("Stream Key:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(3.0); + ui.add(egui::TextEdit::singleline(&mut self.stream_key).password(true).desired_width(420.0)); + }); + + ui.add_space(10.0); + + render_section_card(ui, "STREAM VIDEO QUALITY", accent, |ui| { + ui.label(egui::RichText::new("Target Framerate:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + for fps in [30, 60] { + if squared_button(ui, &format!("{} FPS", fps), self.target_fps == fps, accent) { + self.target_fps = fps; + self.fps_input_str = fps.to_string(); + } + } + }); + + ui.add_space(8.0); + + ui.label(egui::RichText::new("Video Bitrate (Kbps):").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(4.0); + ui.horizontal(|ui| { + for kbps in [3000, 4500, 6000, 8000] { + let mbps = kbps / 1000; + if squared_button(ui, &format!("{}K", kbps), self.bitrate_mbps == mbps, accent) { + self.bitrate_mbps = mbps; + self.bitrate_input_str = mbps.to_string(); + } + } + }); + }); + } + ShadowPlayView::GlobalSettings => { + // 1. System & Startup + render_section_card(ui, "SYSTEM & STARTUP", accent, |ui| { + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.label(egui::RichText::new("Autostart Instant Replay").size(12.5).strong().color(Color32::WHITE)); + ui.label(egui::RichText::new("Automatically launch the background recording engine on login.").size(10.5).color(Color32::from_rgb(150, 150, 155))); + }); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let mut ar = self.autostart_replay; + if toggle_switch(ui, &mut ar, accent).changed() { + self.autostart_replay = ar; + self.config.autostart_replay = ar; + self.config.autostart = ar; + let _ = self.config.save(); + if ar { + spawn_daemon_process(); + } + self.show_hud_notification( + "AUTOSTART REPLAY", + if ar { "Enabled: Starts on login" } else { "Disabled" }, + crate::overlay::ToastIcon::Info, + ); + } + }); + }); + + ui.add_space(8.0); + ui.separator(); + ui.add_space(8.0); + + ui.horizontal(|ui| { + ui.vertical(|ui| { + ui.label(egui::RichText::new("Autostart HUD Overlay").size(12.5).strong().color(Color32::WHITE)); + ui.label(egui::RichText::new("Automatically open the HUD overlay menu when logging into desktop.").size(10.5).color(Color32::from_rgb(150, 150, 155))); + }); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let mut ao = self.autostart_overlay; + if toggle_switch(ui, &mut ao, accent).changed() { + self.autostart_overlay = ao; + self.config.autostart_overlay = ao; + let _ = self.config.save(); + self.show_hud_notification( + "AUTOSTART OVERLAY", + if ao { "Enabled: Opens on login" } else { "Disabled" }, + crate::overlay::ToastIcon::Info, + ); + } + }); + }); + }); + + ui.add_space(10.0); + + // 2. Keyboard Shortcuts + render_section_card(ui, "KEYBOARD SHORTCUTS (CLICK TO REBIND)", accent, |ui| { + let binds = [ + (KeybindAction::Menu, "Menu Overlay", &self.config.menu_hotkey), + (KeybindAction::SaveReplay, "Save Instant Replay", &self.config.save_hotkey), + (KeybindAction::ToggleRecord, "Start / Stop Record", &self.config.record_hotkey), + (KeybindAction::ToggleCursor, "Toggle Mouse Cursor", &self.config.cursor_hotkey), + ]; + + for (action, label, current_hotkey) in binds { + let is_listening = self.listening_keybind == Some(action); + ui.horizontal(|ui| { + ui.label( + egui::RichText::new(label) + .size(12.0) + .color(if is_listening { accent } else { Color32::from_rgb(203, 213, 225) }), + ); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if render_keycap_button(ui, current_hotkey, is_listening, accent) { + if is_listening { + self.listening_keybind = None; + } else { + self.listening_keybind = Some(action); + } + } + }); + }); + ui.add_space(6.0); } - crate::updater::UpdateStatus::Failed(err) => { + + if self.listening_keybind.is_some() { + ui.add_space(4.0); ui.label( - egui::RichText::new(format!("Offline or error: {}", err)) + egui::RichText::new("Listening... Press desired key combination or Esc to cancel.") .size(11.0) - .color(Color32::from_rgb(239, 68, 68)), + .strong() + .color(accent), ); - ui.add_space(6.0); - if squared_button(ui, "Retry", false, accent) { - crate::updater::spawn_update_check(self.update_status.clone()); - } } - } - }); - ui.add_space(6.0); - ui.checkbox( - &mut self.auto_check_updates, - egui::RichText::new("Automatically check for updates on startup") - .size(11.5) - .color(Color32::from_rgb(226, 232, 240)), - ); - }); + ui.add_space(4.0); + ui.horizontal(|ui| { + if squared_button(ui, "Reset Hotkeys to Defaults", false, accent) { + let old_menu = self.config.menu_hotkey.clone(); + let old_save = self.config.save_hotkey.clone(); + let old_rec = self.config.record_hotkey.clone(); + let old_cur = self.config.cursor_hotkey.clone(); + crate::hyprland_binds::unbind_hotkey_async(&old_menu); + crate::hyprland_binds::unbind_hotkey_async(&old_save); + crate::hyprland_binds::unbind_hotkey_async(&old_rec); + crate::hyprland_binds::unbind_hotkey_async(&old_cur); + self.config.menu_hotkey = "Alt+Z".to_string(); + self.config.save_hotkey = "Ctrl+Shift+R".to_string(); + self.config.record_hotkey = "Ctrl+Shift+F9".to_string(); + self.config.cursor_hotkey = "Ctrl+Shift+F10".to_string(); + let _ = self.config.save(); + crate::hyprland_binds::register_hyprland_binds_async(&self.config); + crate::config::ScytheConfig::notify_daemon_reload(); + self.show_hud_notification("KEYBINDS", "Restored default hotkeys", crate::overlay::ToastIcon::Info); + } + }); + }); - ui.add_space(14.0); + ui.add_space(10.0); - // BOTTOM ACTIONS (RESET DEFAULTS + APPLY & SAVE) - ui.horizontal(|ui| { - let reset_btn = egui::Button::new( - egui::RichText::new("RESET DEFAULTS") - .font(FontId::proportional(11.5)) - .color(Color32::from_rgb(180, 180, 190)), - ) - .fill(Color32::from_rgba_unmultiplied(255, 255, 255, 14)) - .stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 28))) - .corner_radius(CornerRadius::ZERO) - .min_size(Vec2::new(135.0, 38.0)); + // 3. Accent Color Theme + render_section_card(ui, "ACCENT COLOR THEME", accent, |ui| { + ui.label(egui::RichText::new("Select Interface Accent Color:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(6.0); + ui.horizontal_wrapped(|ui| { + let palettes = [ + ("amd", "AMD Radeon Red", Color32::from_rgb(221, 0, 49)), + ("nvidia", "Nvidia GeForce Green", Color32::from_rgb(118, 185, 0)), + ("intel", "Intel Arc Blue", Color32::from_rgb(8, 109, 183)), + ("blue", "Charming Blue", Color32::from_rgb(56, 189, 248)), + ("green", "Emerald Green", Color32::from_rgb(34, 197, 94)), + ("yellow", "Solar Yellow", Color32::from_rgb(250, 204, 21)), + ("purple", "Royal Purple", Color32::from_rgb(168, 85, 247)), + ("pink", "Neon Pink", Color32::from_rgb(244, 63, 94)), + ]; - if ui.add(reset_btn).clicked() { - let def = ScytheConfig::default(); - self.target_fps = def.fps; - self.fps_input_str = def.fps.to_string(); - self.bitrate_mbps = def.record_bitrate_kbps / 1000; - self.bitrate_input_str = self.bitrate_mbps.to_string(); - self.replay_sec = def.replay_duration_sec; - self.replay_sec_input_str = def.replay_duration_sec.to_string(); - self.video_codec = def.video_codec; - self.show_cursor = def.show_cursor; - self.mic_volume_pct = (def.mic_volume * 100.0) as u32; - self.system_volume_pct = (def.system_volume * 100.0) as u32; - self.autostart_replay = def.autostart_replay; - self.autostart_overlay = def.autostart_overlay; - self.show_hud_notification("DEFAULTS", "Settings restored to defaults", crate::overlay::ToastIcon::Info); + for (id, name, col) in palettes { + let is_sel = self.config.accent_color.to_lowercase() == id; + let bg = if is_sel { + Color32::from_rgba_unmultiplied(col.r(), col.g(), col.b(), 50) + } else { + Color32::from_rgba_unmultiplied(col.r(), col.g(), col.b(), 16) + }; + let stroke = Stroke::new(if is_sel { 2.0_f32 } else { 1.2_f32 }, col); + + let btn = egui::Button::new( + egui::RichText::new(name) + .size(11.5) + .strong() + .color(if is_sel { Color32::WHITE } else { Color32::from_rgb(226, 232, 240) }), + ) + .fill(bg) + .stroke(stroke) + .corner_radius(CornerRadius::ZERO) + .min_size(Vec2::new(140.0, 30.0)); + + if ui.add(btn).clicked() { + self.config.accent_color = id.to_string(); + let _ = self.config.save(); + self.show_hud_notification("THEME ACCENT", &format!("Selected: {}", name), crate::overlay::ToastIcon::Info); + } + } + }); + }); + + ui.add_space(10.0); + + // 4. About & Auto-Updates + render_section_card(ui, "ABOUT & AUTO-UPDATES", accent, |ui| { + ui.horizontal(|ui| { + ui.label( + egui::RichText::new(format!("Installed Version: v{}", crate::updater::CURRENT_VERSION)) + .size(12.0) + .strong() + .color(Color32::WHITE), + ); + ui.add_space(10.0); + + let cur_status = self.update_status.lock().ok().map(|g| g.clone()).unwrap_or_default(); + match cur_status { + crate::updater::UpdateStatus::Idle => { + if squared_button(ui, "Check for Updates", false, accent) { + crate::updater::spawn_update_check(self.update_status.clone()); + } + } + crate::updater::UpdateStatus::Checking => { + ui.label( + egui::RichText::new("Checking for updates...") + .size(11.5) + .color(Color32::from_rgb(150, 150, 155)), + ); + } + crate::updater::UpdateStatus::UpToDate { version } => { + ui.label( + egui::RichText::new(format!("Up to date (v{})", version)) + .size(11.5) + .color(Color32::from_rgb(34, 197, 94)) + .strong(), + ); + ui.add_space(6.0); + if squared_button(ui, "Check Again", false, accent) { + crate::updater::spawn_update_check(self.update_status.clone()); + } + } + crate::updater::UpdateStatus::Available(info) => { + ui.label( + egui::RichText::new(format!("New version available: v{}", info.version)) + .size(11.5) + .color(Color32::from_rgb(245, 158, 11)) + .strong(), + ); + ui.add_space(6.0); + if squared_button(ui, "VIEW RELEASE / DOWNLOAD", true, accent) { + crate::updater::open_browser_url(&info.html_url); + } + } + crate::updater::UpdateStatus::Failed(err) => { + ui.label( + egui::RichText::new(format!("Offline or error: {}", err)) + .size(11.0) + .color(Color32::from_rgb(239, 68, 68)), + ); + ui.add_space(6.0); + if squared_button(ui, "Retry", false, accent) { + crate::updater::spawn_update_check(self.update_status.clone()); + } + } + } + }); + + ui.add_space(6.0); + ui.checkbox( + &mut self.auto_check_updates, + egui::RichText::new("Automatically check for updates on startup") + .size(11.5) + .color(Color32::from_rgb(226, 232, 240)), + ); + }); } + ShadowPlayView::ScreenshotSettings => { + render_section_card(ui, "SCREENSHOT STORAGE", accent, |ui| { + ui.label(egui::RichText::new("Save Folder:").size(12.0).strong().color(Color32::WHITE)); + ui.add_space(3.0); + ui.horizontal(|ui| { + ui.add(egui::TextEdit::singleline(&mut self.output_dir).desired_width(340.0)); + if squared_button(ui, "Change", false, accent) { + pick_folder(&self.output_dir, self.folder_tx.clone(), self.folder_picking_active.clone()); + } + if squared_button(ui, "Open", false, accent) { + open_folder(&ScytheConfig::expand_tilde(&self.output_dir)); + } + }); + }); - ui.add_space(8.0); + ui.add_space(10.0); - let apply_btn = egui::Button::new( - egui::RichText::new("APPLY & SAVE SETTINGS") - .font(FontId::proportional(12.5)) - .strong() - .color(Color32::from_rgb(11, 18, 4)), - ) - .fill(accent) - .stroke(Stroke::NONE) - .corner_radius(CornerRadius::ZERO) - .min_size(Vec2::new(ui.available_width(), 38.0)); - - if ui.add(apply_btn).clicked() { - self.config.show_cursor = self.show_cursor; - self.config.fps = self.target_fps; - self.config.record_bitrate_kbps = self.bitrate_mbps * 1000; - self.config.replay_bitrate_kbps = self.bitrate_mbps * 1000; - self.config.video_codec = self.video_codec.clone(); - self.config.replay_duration_sec = self.replay_sec; - self.config.output_directory = self.output_dir.clone(); - self.config.mic_volume = self.mic_volume_pct as f32 / 100.0; - self.config.system_volume = self.system_volume_pct as f32 / 100.0; - self.config.audio_mode = match self.audio_mode_idx { - 1 => "mic".to_string(), - 2 => "both".to_string(), - 3 => "muted".to_string(), - _ => "system".to_string(), - }; - self.config.auto_check_updates = self.auto_check_updates; - self.config.autostart_replay = self.autostart_replay; - self.config.autostart_overlay = self.autostart_overlay; - self.config.autostart = self.autostart_replay; - let _ = self.config.save(); - crate::hyprland_binds::register_hyprland_binds(&self.config); - crate::config::ScytheConfig::notify_daemon_reload(); - self.show_hud_notification("SETTINGS", "Settings Saved & Applied!", crate::overlay::ToastIcon::Info); - self.switch_view(ShadowPlayView::MainHud, ctx); + render_section_card(ui, "SCREENSHOT OPTIONS", accent, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Image Format:").size(12.0).strong().color(Color32::WHITE)); + ui.label(egui::RichText::new("PNG (Lossless)").size(11.5).color(accent).strong()); + }); + ui.add_space(6.0); + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Capture Mouse Cursor").size(12.0).strong().color(Color32::WHITE)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let mut cur = self.show_cursor; + if toggle_switch(ui, &mut cur, accent).changed() { + self.show_cursor = cur; + self.config.show_cursor = cur; + let _ = self.config.save(); + } + }); + }); + }); } - }); + _ => {} + } }); + + ui.add_space(10.0); + + // BOTTOM ACTIONS (RESET DEFAULTS + APPLY & SAVE) + ui.horizontal(|ui| { + let reset_btn = egui::Button::new( + egui::RichText::new("RESET DEFAULTS") + .font(FontId::proportional(11.5)) + .color(Color32::from_rgb(180, 180, 190)), + ) + .fill(Color32::from_rgba_unmultiplied(255, 255, 255, 14)) + .stroke(Stroke::new(1.0_f32, Color32::from_rgba_unmultiplied(255, 255, 255, 28))) + .corner_radius(CornerRadius::ZERO) + .min_size(Vec2::new(135.0, 36.0)); + + if ui.add(reset_btn).clicked() { + let def = ScytheConfig::default(); + self.target_fps = def.fps; + self.fps_input_str = def.fps.to_string(); + self.bitrate_mbps = def.record_bitrate_kbps / 1000; + self.bitrate_input_str = self.bitrate_mbps.to_string(); + self.replay_sec = def.replay_duration_sec; + self.replay_sec_input_str = def.replay_duration_sec.to_string(); + self.video_codec = def.video_codec; + self.show_cursor = def.show_cursor; + self.mic_volume_pct = (def.mic_volume * 100.0) as u32; + self.system_volume_pct = (def.system_volume * 100.0) as u32; + self.autostart_replay = def.autostart_replay; + self.autostart_overlay = def.autostart_overlay; + self.show_hud_notification("DEFAULTS", "Settings restored to defaults", crate::overlay::ToastIcon::Info); + } + + ui.add_space(8.0); + + let apply_btn = egui::Button::new( + egui::RichText::new("APPLY & SAVE SETTINGS") + .font(FontId::proportional(12.5)) + .strong() + .color(Color32::from_rgb(11, 18, 4)), + ) + .fill(accent) + .stroke(Stroke::NONE) + .corner_radius(CornerRadius::ZERO) + .min_size(Vec2::new(ui.available_width(), 36.0)); + + if ui.add(apply_btn).clicked() { + self.config.show_cursor = self.show_cursor; + self.config.fps = self.target_fps; + self.config.record_bitrate_kbps = self.bitrate_mbps * 1000; + self.config.replay_bitrate_kbps = self.bitrate_mbps * 1000; + self.config.video_codec = self.video_codec.clone(); + self.config.replay_duration_sec = self.replay_sec; + self.config.output_directory = self.output_dir.clone(); + self.config.mic_volume = self.mic_volume_pct as f32 / 100.0; + self.config.system_volume = self.system_volume_pct as f32 / 100.0; + self.config.audio_mode = match self.audio_mode_idx { + 1 => "mic".to_string(), + 2 => "both".to_string(), + 3 => "muted".to_string(), + _ => "system".to_string(), + }; + self.config.auto_check_updates = self.auto_check_updates; + self.config.autostart_replay = self.autostart_replay; + self.config.autostart_overlay = self.autostart_overlay; + self.config.autostart = self.autostart_replay; + let _ = self.config.save(); + crate::hyprland_binds::register_hyprland_binds(&self.config); + crate::config::ScytheConfig::notify_daemon_reload(); + self.show_hud_notification("SETTINGS", "Settings Saved & Applied!", crate::overlay::ToastIcon::Info); + self.switch_view(ShadowPlayView::MainHud, ctx); + } + }); }); }); } @@ -1924,7 +2366,7 @@ impl ScytheOverlayApp { .corner_radius(CornerRadius::ZERO); if ui.add(back_btn).clicked() { - self.switch_view(ShadowPlayView::Settings, ctx); + self.switch_view(ShadowPlayView::MainHud, ctx); return; } @@ -2323,6 +2765,15 @@ impl ScytheOverlayApp { painter.line_segment([icon_center + Vec2::new(-5.0, -5.0), icon_center + Vec2::new(5.0, 5.0)], stroke); painter.line_segment([icon_center + Vec2::new(5.0, -5.0), icon_center + Vec2::new(-5.0, 5.0)], stroke); } + crate::overlay::ToastIcon::Screenshot => { + let half_w = 7.0; + let half_h = 5.0; + let rect = egui::Rect::from_center_size(icon_center + Vec2::new(0.0, 1.0), Vec2::new(half_w * 2.0, half_h * 2.0)); + painter.rect_stroke(rect, CornerRadius::ZERO, Stroke::new(1.8_f32, icon_tint), egui::StrokeKind::Inside); + painter.circle_stroke(icon_center + Vec2::new(0.0, 1.0), 2.5, Stroke::new(1.6_f32, icon_tint)); + let notch = egui::Rect::from_min_size(icon_center + Vec2::new(-3.5, -half_h - 1.5), Vec2::new(4.0, 2.5)); + painter.rect_filled(notch, CornerRadius::ZERO, icon_tint); + } crate::overlay::ToastIcon::Info => { painter.circle_filled(icon_center, 4.5, icon_tint); } @@ -2357,6 +2808,7 @@ impl eframe::App for ScytheOverlayApp { } fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + self.ensure_textures(ctx); self.poll_async_events(); self.mic_vu = (self.mic_vu * 0.94).max(0.0); self.sys_vu = (self.sys_vu * 0.94).max(0.0); @@ -2454,9 +2906,12 @@ impl eframe::App for ScytheOverlayApp { } else { // Normal Escape handling if !self.folder_picking_active.load(Ordering::SeqCst) && ctx.input(|i| i.key_pressed(egui::Key::Escape)) { - if self.replay_dropdown_open || self.record_dropdown_open { + if self.replay_dropdown_open || self.record_dropdown_open || self.stream_dropdown_open { self.replay_dropdown_open = false; self.record_dropdown_open = false; + self.stream_dropdown_open = false; + } else if self.current_view != ShadowPlayView::MainHud { + self.switch_view(ShadowPlayView::MainHud, ctx); } else { ctx.send_viewport_cmd(egui::ViewportCommand::Close); crate::ipc::clean_overlay_pid(); @@ -2468,9 +2923,12 @@ impl eframe::App for ScytheOverlayApp { if !self.folder_picking_active.load(Ordering::SeqCst) && ctx.input(|i| i.pointer.primary_clicked()) && let Some(pos) = ctx.input(|i| i.pointer.interact_pos()) && self.panel_rect.width() > 10.0 && !self.panel_rect.expand(6.0).contains(pos) { - if self.replay_dropdown_open || self.record_dropdown_open { + if self.replay_dropdown_open || self.record_dropdown_open || self.stream_dropdown_open { self.replay_dropdown_open = false; self.record_dropdown_open = false; + self.stream_dropdown_open = false; + } else if self.current_view != ShadowPlayView::MainHud { + self.switch_view(ShadowPlayView::MainHud, ctx); } else { ctx.send_viewport_cmd(egui::ViewportCommand::Close); crate::ipc::clean_overlay_pid(); @@ -2494,9 +2952,14 @@ impl eframe::App for ScytheOverlayApp { egui::CentralPanel::default() .frame(egui::Frame::NONE.fill(Color32::from_rgba_unmultiplied(0, 0, 0, 80))) .show(ctx, |ui| { + self.render_top_bar(ctx, ui); match self.current_view { ShadowPlayView::MainHud => self.render_main_hud(ctx, ui), - ShadowPlayView::Settings => self.render_settings_view(ctx, ui), + ShadowPlayView::ReplaySettings + | ShadowPlayView::RecordSettings + | ShadowPlayView::StreamSettings + | ShadowPlayView::GlobalSettings + | ShadowPlayView::ScreenshotSettings => self.render_settings_view(ctx, ui), ShadowPlayView::Gallery => self.render_gallery_view(ctx, ui), } self.render_slide_notification(ctx, ui); @@ -2897,6 +3360,15 @@ impl eframe::App for ShadowPlayToastApp { painter.line_segment([icon_center + Vec2::new(-5.0, -5.0), icon_center + Vec2::new(5.0, 5.0)], stroke); painter.line_segment([icon_center + Vec2::new(5.0, -5.0), icon_center + Vec2::new(-5.0, 5.0)], stroke); } + crate::overlay::ToastIcon::Screenshot => { + let half_w = 7.0; + let half_h = 5.0; + let rect = egui::Rect::from_center_size(icon_center + Vec2::new(0.0, 1.0), Vec2::new(half_w * 2.0, half_h * 2.0)); + painter.rect_stroke(rect, CornerRadius::ZERO, Stroke::new(1.8_f32, icon_tint), egui::StrokeKind::Inside); + painter.circle_stroke(icon_center + Vec2::new(0.0, 1.0), 2.5, Stroke::new(1.6_f32, icon_tint)); + let notch = egui::Rect::from_min_size(icon_center + Vec2::new(-3.5, -half_h - 1.5), Vec2::new(4.0, 2.5)); + painter.rect_filled(notch, CornerRadius::ZERO, icon_tint); + } crate::overlay::ToastIcon::Info => { painter.circle_filled(icon_center, 4.5, icon_tint); }