From 804b75281c528c0ae24b14b734d92385dd2ac39f Mon Sep 17 00:00:00 2001 From: sen Date: Tue, 12 Jul 2022 19:41:14 -0700 Subject: [PATCH 01/37] Fixed container bug (still another oneeee) --- README.md | 2 +- assets/client-logo-rounded.png | Bin 0 -> 154740 bytes .../type/layout/auto/UIAutoLayoutSheet.kt | 16 -- .../type/layout/container/UIContainer.kt | 43 +++--- .../prismclient/aether/ui/dsl/UIPathDSL.kt | 16 ++ .../aether/ui/dsl/UIRendererDSL.kt | 3 + .../impl/background/UIGradientBackground.kt | 18 ++- .../prismclient/aether/ui/util/Shorthands.kt | 5 +- src/test/kotlin/Renderer.kt | 41 ++--- src/test/kotlin/examples/Default.kt | 141 ++++++++++++++---- 10 files changed, 179 insertions(+), 106 deletions(-) create mode 100644 assets/client-logo-rounded.png delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayoutSheet.kt diff --git a/README.md b/README.md index 3cb0ceb..ace3cd5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Aether UI - + **Aether is a UI component library engine for Minecraft** and LWJGL projects (or anything really). You can create your own renderer implementation or use the default implementation diff --git a/assets/client-logo-rounded.png b/assets/client-logo-rounded.png new file mode 100644 index 0000000000000000000000000000000000000000..3d435dd50d94edaa7146ab895e7932697f65b9de GIT binary patch literal 154740 zcmbTd1yoc~yD&^iNsAI94T9v*AR!?j-Q6&BNOuVW(w)*J-CZL{mm(n{pp;TWcYJ3E z?|c7szgYh)7-r7d`{~|?4+?S;nCQglaBy&#ZzRQ(;NXxdVZW%zz?BV(no-~n+FMCY zM>x2~aM&;W&`}Eu9NdF`3l$9~4OtmJV>=sWLlZkAFteM@TVORD9KVp;TSH?juoJlv z*v!IKfD+o$K}l|5B0#CmDa$JRRupV*A?e`&R`!rnG4`-B<~5-d5+vt$;{yQLfSnA< z-E6FF9r@e@C~x5M0iR))St!YGmN;1nPzu8kl55B+kc-+mfXTU;K}^Q1tgPhRyv$$| zLpCFJQ$}(&RyGg|2>9V(09;20dx)~rhH0b;&;e^ZvvF&PEK$6SXf+L zU720kne80RSU|kIyezD2ENpB{zzQZucUvbzHzr$0s(T1xU`JyIi?>b|cDCd&h=xXX z&Q1c90I9b;*u4D**4FV(PJqN%+zj8cfS6ffJl!lbG5)vit+Rvm4RRA>7O*wg25jr( z2&@JDTl?1B&dJWv-0pt^`rqyUh5(SQtn5D=|069nHvdpKI=yxQ*tiqO|G3&w#r-Xq zMG5R^=j>n%e(eH~Nd@EPEuW|Z*wD$&LB-C_`d(8C_au|Eu``3n>C`Q3P3&A9>HlK` zSj^A~EIvJTqHZnDCjhvh$ja`Fkx`AF@bqFSecBBcy8c=SwV&%Q;;dgJv4a-3&7C~ zt^X4$j4BfVA{RT4p^1qRJCiYw5f2lHlY@(ihnJI=iIazmha1dg#L8>P2D9cH1L2dj za0FE6e&-*`VEemI))wS9Ho<3T46}6sN@JLk!2mzEmo5GWdHg?NzT59=4hDe!AC!NK z#?j8y$<@#SENlkI?*D3BEdM*=9SvRn&!qpqsq&Vl|B>|nG{*lwq~D3f*xb<83=Bk0 z7D^a1EU*xBs}z?1ufA^X{YN;uUk=1L*vEg-75MTmK7(xm&<;TKwKH15g+owNd?O~T z;x@B+=^AalkumA&v*6lvP~Lhxzu7j*JJnK<7PQ?ySJ0hbWoaHABus3mWu$fLSLxeblD~M*8-b&^=?+z5hIgVPZ_k&?KbVUI(8?4 zFPZZ*zDr5Az_<6nIv+1!P38~aQsxZs)?fo%e_4ODaH0$Bxd>CQ7pkng?jaj5kWJ&V znW?q-4t%KhoVkLatkQL~B)44D8!_)c>YiJD*sZhny?f5ABl)3oXj9B;$PBG%O-Ojs zlS)J{yc7PNM-V976?pMl!P*z~B*||9?Luvl8?FQW#BebQE)#AZi4+k9LR#qp)SX0Q zj<$IF95`ulMWqXeE8Iob#~n`BcsmV#9q<=xkW7Rt*~Mdo4$Kq#MHIhLa~MDW7~>LD z9*Xg4O=iEGH^MqB4CS3zpj*}OElPI0EH}b(Q5fnM!#DV#1H!)8`3V zVMRZhOBEV@_Dd+-HPsI0Vg=ka{|+wsIL?O<*h?lH$L--skXgc0)z6yLEcHAwR8 zHGJaSgz;X3&P4+R5GeA6?V{Ld(#?jx0Hd&`MJV%X&;sLXC?IgLE-F{=MlstQk@;ol z!gHwMVS~RAh8G!*mD??0c{l`)-9i`_zG1w}9r{D`6GGCqV(^JN66vk(3;NV7XqPPh z3y~fgw*>VaG7&iTU*nzd?X2~CS|)IE?D3&jE`u#0^3fH>SA3@%${is}4!Ae3rj{2w zR{aZ>C!C9;2pxEm92|R;z~bV-i(y>f4F1|PA%g3cyzJY>G4Mr*9k?fh%Z8Jg6m|#| zT$~PX;1l6m(JtOBGAut!^{EKhVG)HS0guyyV~M?bEk^L3#6H8;}d--5e!w zoEf#0Bz*!b5>4l?yUO&tj=YF?XBqzdcG(gw{c7#?vmMv4CR^Puwo`lq*?QT%z%%LP zji4wF>N{8zSYb`uNuf>9f*=zIdPu5FCj1rN4tD<(D)+xN*~Hy*$7D|&e~p?;(ncYa z_BSVW;GLvFe6cT*w<~T~4B@YI-;H|g_`5@MD29_IBP)$zi3!OE@8l&!A6YP);!b2J z+mE_QP64x6RS;#H$2W+mKs_mgETCLb?bUVMQbf5oWC$|5Lg^;q8;cHq{y<~tIl`62 z-r~A^F0J$3l8PF|q3mlPP@!5hrzrqss_iTSa*c5j+>M=iOHPQ+f;gz~NOOh6wxcizrWa7>(f6!CBh88MbY=lyE!@Fy2(h6nMxc0Py`P|C{IWQN_fy61`|3N z5W%%)_grfM1<^AA1~hp27!izlYndT^ZFB^V`yElC(&9k~rRT2b7u?I>P-$YUJ7L9C zY79+WYmX5qygkz6aPOv7FLLz=_YD3%6lvd{_a0P5<@l zy^Q*pIQnYd`(m!JO%?bzXVC7b`w98Yv(eq@&(*k(J*Yz}gSXELhzM;0i`Qru=$P;C z#DDN@t)D$u0JFP<9^z3)1~4TcdAE^`+;e%l3)`Gd4=~)p%kL0h%vw(T`Adx%aZ-17LF?&RV z+0Bk#nPGu=O|TgDU$T?Xryix_BM$wDP&!AvWqBaDAoXL2;~-oUcJdz-(hu-$P;9sCZZ{jQOd$41}X->jrx zyq3xU^GjlLLe6j(I}p~-i0+vHDLfx#^I4|QyAZ{w^uaih{^J~f&%U12a!OB>KBJd( z1-1S%HHyXp_JtW;E8ZOy{Z9~mWB4DYNX8Kxcw?N5_Up6I-y`}fwXE6KOS}7<76jo# zxO$jXWTt-a8PT-vv=00h1Y8#IlWaRv{zL*bciZ;hhT*O`-No5JwDb^5gzJgCpX7HI z8uL_(c2ttq)OjWkNdxVaZ}gEs?%giRk8r*ZKFM@;%ZT_rhV%7D+pcA~yXEi@s8Sy7 zZJLzIiufJc!3ha)7E?`gaoYed#)S(cqO7Vy2_Qt>#>jNWs}ZEuOtfYZNY0I3cA9+3%Z>araz zNsCN(i*+{~K4FZiRJ8(^d?2)?gI zlkX`=zx3HF1v7?A`_&;{SU~=q-f083iCr|KZZop0KT_0B75++jyjJ&))4a@7h(7tK zp7;z8lEyXjj^t?k9W==W0fehIMWKF0>`I@9J8>R~cU#1eSGf^8oa5!+#v_(`FUqeI z-J>CD6cWzp4$o7>ti)Rsf-m;)`M0GU{la}u#&kK{((h2huh+xn{!Y*;?v8}S?iXEeE_i&E|r16g=qVpb2me|tvYuAdut#LHQZBa{j~7~0UhGX&fw z?)_z=;XcBAxD9T2{c-kt6*xeB9`B?{teA+?K(uImb#w2S3<(bslFOK$Yf(h2yh6+> zTDy-DCMF#H>}j6d=I2n&lV`$ zhCISE0hIgeR3)+*zvj};F)9VIFDwNR|6PZwBkDI+1T3igsUlwK{`9;@P__HKpFKUh zr2=ll7p3%Xd)OVR;SrpR*z>*qP$~!?vQNWy)4kSzoa#dUR$}vQ!VSV*lj5xV{M(U^ zb0QRL#xKgg3AZtYnE9>k$sH0R?M#YMw;b|rNX`i8J@RY=#^KPS$XI8se}zA1tBBt@ejf^ z@D8NlKDxJ$nCz27hy;pvv>{+?T%ta8IrnP7cn^Q|CkvbH58TH7a)lcHy)gO@i1skd z4L1FbFDo`kZ!1lp0B`k8hhLQm0OI1a9m!+)JG(IXEkHT?4%tWnUJdD@q}lbJOza*Y z0ihlK%t9z#z_9+?e(y;9gxbp~3JaK&kWjycLeG0u?k!#jl5ZZ%q2K};-$3+O`VRc^ z=V|iMptIryc(st?ihElmtzMANEU7$uW^gHPpnUx1-{KkS(U7x(O!!22pNj36du0V2 zd-m7lwEr>lJIsY-Z%6&R?uzP_t^aZZxX!XZe=iovY&%WG4usNCgv_trcQV0^>3r@# zdSEPZ2}s@j=cD_}r+&WKWH0|p_|?ghO>x|ds_)AQg1d{;^Gno9pXZlb`84-JiTfOD zCgD2YuRHF)kmGq*4pLIG>}vb1>bkt00vrgD^C!wZboUR$JC9taKkFivE|hzc-$SX` z9i3ZAgW?%@BlC=%+#6n>6$wIS+ZOi%4dl#>-5N1{j*+6Hi)H}%=+VB{g6$t9w~c}Y zr!*jSjcNQ_%yc=K=F=sMSW0+OM{IV_OH;o7Af{)@0te)bA}iPa-YH_V*Q3p3rd+-Q ztec15yTo#&Z9M2FHKZ6(|Le&=sXp`e=eKH3OheKTLRR5u|Hq-k&YDEKcup zOUS`2LB)Rd29gGNF_c#gNw|`q@7vdFc8bB56$U{A16LF~&@R<**q}dU*DpE|af%DZ*{c zkTWo`&^;oeN_A6(3iQfiNH3?)JymQ5nQ)dY=f6t;yM_PIbMKvE$c(h3IeLFo;U}>z zH}4f^BB_`9y6pwz%qJr4US1ge(lwuw9%wvU)L6QAN=i`&B$>894_O{o3U+NQDBPz> z$}yHFCkKP=JCvEamd^dcAc0{ubKEuC&(2GDlqSfSpY1RyymAJEy&oi@DUW#U51#iV6MJivWPAy-r;=B2H z(UjG=rRs}`7x@nF^HeB9A9b>J&cGQDt+Xpf!@PP7n{1#!58uFBW(n5!#&vqiCxV?? z1YpEL+m3l(zU(3tZPPRJ@rjIx;B;|mm|j>|`SZu`xgG!NQsxO==Gl1f=}E89z*9TH zUnd9g>20EkQw=E@+BuV(I5adn(JrHZGlcSI1)fWLx~*~Sz~tKCS_Zd~OZ*&0K0d)? zrEa0hNhK*Mrvg4}>vG4lKY{XId&XWzj+v|H?bnVMi!A5x?K`a@7wU5eFzJye5oPoj~>n{EGy1N5>j#TN?)_hrYPpLa*gA?vo$A^S>WuUhn|y zJwG`p6zVS&I)5X0+$6v6-LYKbb$#p`3BA4=-ED%NV`N@!7M}iiF4P?t<@GM0m(2M5 zgoz7_tft;`V62DqdsxZ1wsN5TM|JOf0iDFh6B=4K)tUoh*fP;XJ~pyO6o)t<6t)p& z4;@_&i{lYx<|Iumz2E&$qfYaMPNYvB>!0BIoCNzsk9w`f@w@M%1xuW7Zf{k#)pW1+ zAWKV!)iQuz2uqeI8Cv)R9oB*C>nDHAOdDEV2ey5Wa=3^Y-93rxh?mGb6ULgqz#r5` z;oxHrR19~-T4VdD9OnvXAx7KH>(JMW72=Vz3;SyCqUa3{&sKTyEoHm1y{hML?jym& z6#3mj-&2;AV4vlrUGL0ujwrIs%BH5zn{y;va%rYD!ud))2n4#NUb`sDyr5cQngTv1 z)3qm>S__67e=s8j_cTepf1~hR{Wz}LKQU&sBO6Q6E>N<=6W?9oU7<*P#|Xo0<-weL zX5e?Iu>>pVmi|yS4pg_U(?>pf@`NCiRGn!Lm(q4?d zQmh9D-*2@8*9Xf`N9OM6FF`yXB&BF9hhL;m(>gMg05~hXFKEx@1wT*rOK51! z<^whwNWnZ9O7q@wn;AgpARAS2h0R0(M8B91i=o`t4}@RkYk}=8{Nug@BKRx6Hw`G< zuJtf*VnVfeWo+fxZLe}YM>?Tn52`NYsQ4btq(Ne$TdgNGiMU3Nnl))&Q`tUz^tYGC zZ9~~Qv%UM`jO*}-L-C_Q_-D@t8URMhsRJExXHJNeA7%tBt%v6_4oZHHr+%XWn80qF zgxBvSvX!!ij7s0e-mwdkItMgt{s;3cSJMy17_@u#{U;})A40400-Zq9MCDWYu2>e> zrJO1ooTg9IHKqlOHV$ODBIiwfyo0qoQ&~g(TBU|wKG1~Wt*8>tP=y~w(RlhQ>sIOZ z-?Z0_!a74zritXp(sxoueNv-*Hf6b{)Ga10zjTucCVh_gt#5m~;KlQ$lkj(+0*m(g ziXSycxBK#fF-9PWqp(0@W*)q8KxIwR(SI?+Rd<9T9Pf9`lT=jI0Xr_S?@f)Urf9P>n?ZYc#bviBdY3 z_}ix->ZJk~;xr9c9D(j=wNXm?vAVHN-qA9Wj(J`tWL^fAQuc_GG$$tD ztfZGHI^{jWKH}w{Gjj*0SeCgoS?{dJ3i?hB3z0cIaHCAaoeux%CbO%0J?H+8MWpm- z6M+FGN<+veV0wZ{st&max!c|2@C&S@KdpScH?+l%lCPf~=<2Foy& zE0s^|&d0g5G%_-^5B@=hL0!hndN7JmN)!Rh?%{m;2W@OUIM30xncg&wD#4^WN6fTJo#Fvj0eM9&6w{nS&L=}k@4DJs;orzClmEb^3esJwCD3u zQhrlQiwHXUUpiWhG%CtB!4o?f`Noym#H$%|r#1}B@+T~-n(GH9N1l{yl*l;Gd@=Mx zR1!)i>~qXVV5l7*z>TmJ$C_Z5LJD=o)4tQQ?zxJD7`;~BvlORIO{(Yz%T3PA+W%CSc_^)OTPk)m5 z`tXhF)$4@e6Jg?lhkCdFLVM@uQaB7SA<-LLTFR{aqT8u|fzK`O&ziIUWJx>9o~`Pn z8zWIq;bJIT3BlX#X@}0n_^-(sU>~cfgn(WEi;_h>sAo~9AZQzASffshntl`C+yDf9 zbbIr0V4@M;PP&{4+p5~?p~(@hr?XFGTxS9&V-e^Z9#(F5E}}(Hu@S{MYwZ7Oz||92 ze9e`h!XQ(@SmX3|pz{d+@UP}Op8kO5u>3&uYM8gH_`~X%Tqq?3!JLD*)ULG3w{djx z=m~pGO?(`Z^nC5lQ*1LqAHOszeJPF5ti)F|0}FC)&PBgacu|-G$o)K8x;JN@?9T=d zmD+qy&Y+@KholY$9l}V*o^yPxfnEt9F`;&czWI(16^Kxa8{V?Pip18`)XY&Tf^&~X z>hku@WZ*K+!qZMo@YCWYN8e%??IwUvbj1mTPcV&3spEWdESEexmphZuLnxhCVEG4@ zauAsM5K9|>z&H>ec>N$26y(q}ba6q_a_+fFwpqg}mVI;hIj?)2mt$TJ3Oee}zef&EAN- z{*|mTtYORT0Y}y*{r;G%RK(iAI@)veByCoEh=oa^Ol9X4cHj1|3uYr4V@!K|1?Kl( zVm`$3YT68V`@LPAWv_Ttw=jnRPUImxk%?L3JdvQM zx3v4(UVdPYh!UCwOL?W!Nyti9Z|#%BDn&65pU92J@QEM~RzOP26vf2|uwF!b?DYfX zIyJD3aeCbIC?1{}GY>vrcZVpmwhG(d&DoPSM_^fT$*8^q)N*^5p5_;o;>Hk;7$}3mjuzJKxVu?C`D9K405(zKS?jCDuwtt7h-m6|=W=cMF+B^m zaQ|T06o{gfVK?uiubab9AWbOn#{5$fA}k436B_HCadobh^D@lK5Q>zscORT#J@kA& zWCQ)4tVKtE{A|)e33;&;&0xm}`ni_g} zdL?*!r5~F2x$Wk+&npFdM_xypVysZ->9Tw)PojHH(bMYofr^YjlOYq0@{@Bx%Iv(^ z$}-ccD^`(90YpnD@mE_h%vzT}180&jZO{y|0kKJkxu&&w;A)rRsdidzDrPunw9Wpl zijJ7n8+?TdF(5A4n}=Gmxo+jqC#?unby8>7RrYnxqwT+x;8;rxk$w!(VWv4Kh6VAM z;9L{`*dAd3$97sr^(1qs(Twslu7(C49zGKro0Yw?03sdc!Hub-&i$tTRd_^yf9iKU zd2UY)Lmf4*;NyH(Cyb-*gi#Y*0!9XqYi7@7D(t=|Yg3m`LI^Lq1!38Z^fstETz_=T zvye&zf3Y7LAL6ijx?#O-Thm%EQZhkZ85G}oJVE!GTYYqeG8bJ&{8i|oK*mJ4uCadk zr_Mbbu|%cZH=gbZ)9IoG+^$8=rj^1W42hbsew82R#+{daRw70O+H9j32R}^!kge?x z{sa!E+sve6Deh|e=0hjK4(b{vFxyuvE?oT}ZoeUl=p-@DgtqmY-_DKb4UrC0G*?}) zQWjoj*VRht6zR3MfAy@8f|auFs8UauuWu}ih)*90H1GMF+SJEBqw70WBxz=KZM7tR z@Bv*S7S=jR<9v1J)YekYJ)nReq>Gv2&?9_|`R<1cEHrc!2kOs@O90xaLXBAsGc(_)!(r!{VdO-wX#GKpne2N!&2btd_B=t(Pnd;>ZdEGQ-xBnwP<3}ah=bvTDCT4(h1siq6|(e%#+V^h;qI@Ap9L~g9zp&C++Af8(4 z7#a>D=8f1x4{q&kb&e4SqDK68tXYEWT|=v+H7ycOY7KM^g{+gYhs`Dy@)WS-CQ~da za1+V@lp`|<8x&1gF;D*}(3+ML{KW!$txRW!|DLzn=e5I;@5IeJYKF$98XeRuJ>6ac{Lf;L< z#@bI`La9>2q=%k;WNYvhakNkQq|;3p;D`lfYfDjp={-Wx4d&Th=Yw6#6T%iOz*qr~ z1|(g=K$1aP<>?05?BstF?c1|!eQF?dxD*jkF+0`Wgfgk{Mnr2OS&NqveSb1{`w7Y+ zGc*{CNY_AItInPLQ^3T-Dzm4P&AtxVO(*ep+ii6l_xdrcQ(@yE81891MiPG2^4-+a zE((ab({uBAS{1+g7Wn@BV#9479D}5JZ53e7>$^qplNn45fYQZNOqpoW8Ryr8<2!bL z2x!X@iovg#rX8&#JEA-v+9XlROn)9?)auB+QFoK4&JZ~5fqFeAd*`k|V9lGhtZoDf zge$DHmf4{pnMWvz@eJ^jjd~33e&Et<67BT_KPl@f6BCQj!MUyS3e>9BVDkF$HB*2c zgYCt@)iGlGrdZ!n)T;4u*z}NAEN>k)I3YrZ3TUQsgDSY6mRgrqc{ai_;djTH_$4}) zhsUq$VxaoUhlaqx%bdFXRnSHH=o{3S;Y#Liv$mGQqP<0SR%EF|^s)ri0#~f$nYsM- zN--rng8-aRDQ*CG%$wVBSr9NzNiA^09tNs~bep3{ETFN^)1lW_WBOUgGVuxI*Zqi4 zLQ?8gP+Qky4{YW^?r@4CHFIWWP>pBb&^GSjDGgBLcpByxk@5pjQ&4YfkVWP*Jxyb} z1nD8>p&IYX-tAw4`;IkVgm-klAEh1T5S!$y2_1pMDe4s>_n2zkE1ea=F8lCpV~++! zntmFYLjAgdA%*F2h=UpNqrjvkpL0Z5RtmqXViiW3a`pz4%hgh}lg+$6;|{l&R+F9- zsWG@^p_mxE*i@7}X(@fNj}siEMW3aHE=d^%T0KgM?RU*MBHs|fK^|WvNS+olI@-bA zim(|-r+>%#1xyck20Oq3E|L5#3t811xt>0eprDi{B9|yE9v%Txcbf#uvUl&-60xxc zDwAmSyq{*n$I@jfqFJbjNZI>=7Pmb_fUwSnLmMLX;pz~Jjt_OTUsQPy zGu0*Fi55U=7=geXg=A8zxQ@8y;eIL9wyLh9cWjSx!r>hXt+)N`Yr)5aq9jDIaHaCQ z7TD^HWnZH08lG)n=&&qE6e(ucRga91z^4xjSk^og%V(SCt=7ZN1bgrR8PH^t4gyQH z?fWoSnCFdE`p}gBy8ih1$IL9)$0A0nP2BBNlUX^#-eSP#`wJ$!&<0#$)bOq|QrefJ z%7s4h2D(Md)fCi+$ipkn1=w=Q^Fja*k~rONYkLqGz!OY{ZV~tc^9H>J>5pF+%Nc7t zogince8Z-Q#zT&F&%99QR><@t`*@5tLQ0`l+ApAxLiduAJ(b|1bOBRrIg4^zys5Ng z$m-qvh6p$v2O#HNxAyX)^eB`}Y5vqOJN^@$Q1`ev;i4=+^Qvk?9Ryk~F7mNeFK5TV zn4r$mL#LO3_ir0x*x2sM;C60MuvD=qF9+4R%9ySBosGSClluWvwAC>y|0X?%1CNUV z)71qX#%E3=xbG>3UBccc&`j2IX=rI@Pgvq=h)T_$uTxCG>8hl?_s`eEvQVXZ@y4%v zQO8#e@Atls#=Ji!Q@(gW&mOw6XRyo`se--+En`CFgYZx2u%faj00Cx1f1g5uvH&}Y zcRd1mV^(`JlccJ?=5@(;pULA;b&2N{=VuWkAAa?4u#~KNJTV}$eDf-o1n@SR_bHA% zYv*1pdp;WZE@?-rSnLu}7B<@omd>og8m$kiwI&hkO~*LKUH+#6KMLM@U0rdEq7yzz(aAy40F!a3wbsj`8& zF-mO|PwUOCuNDne$R6J7he#FJHR1{I2#DG5`%I7oyU#F|bbX<35>YhZ6nS&_8eVyx z`xRXp6PIj=B@i>q_K#%{Hv@fkU!U?#h3UE)mLVmKqhQCSp`mAF;~AH5=xn-bT0{*( z?hAz*)c;ASGojLPJQ1~|+v@#|P~Jo!pPZ^H5^^#K-GH|BH&p86R5gzr>>3wfMHY>) z;KdIR8fSFeoG(#H-ZN6i!Me`Q3_H0(eV$#AI2^|iPRtb%>=UM5XnkPj1@vL z4rB8ZHFCb{`LtE^GQ6k?_s^TotEzuv1`U~pL!)0xiYN)H?HdQ~$1);(OCFAXr#T z84)Lh@YK3DuAJ$hI#&rH8XD2N7MR;^vpMbsj$Gj;r0}A?^PxWyc&-uMD;$o;F@Y$(e+Y9IEdAmphfhWVL>lm zVXcRe4~kPvq-gR1cKb}ApsM+ePKj~p@Oko~fH3Sluj%EDQ8`#yq{be3Q0E7BCxBh# zBY*Jn8Q5CHNSTB11v1N1f-!RG&z%>{c|LjTdX2zk`%|-t^%z$(GjIK_#vff2VKk;F zSO3Js^nHFlx2aCT**>LlzE8B2ad6>g)Ws%pU z^?twjB!d09M))(%M1poYvF83MTMxOZw(=zJ3i^78nZ^RuTLH#tzio9=gI|z9oQYx; zf+Tb3=C}EmdCiR?8bzy_s*i6*5QMk9P|cqY4jV{Hp((So1Hl!@cB69_jEeZN9+H=E zsj=)t(l{)sO`cG+vO!q!0*YBm@qzJ;Ich(-^nL?8R`FRs{xQe< zO@8cZsdTR4Edd}mTi8MsGg7GN5_U#@eb<}+0V~s%Gj5ZUmA$5T#ca3M;P@tnHcHE8 z21!+Gm3Xcq4PzpfDYtbRN9o&*W`WW(@74{kp9tChUwIHRjdZb>YCf|pIJyfl@+W_d zuC?T7a7ppGz@3qZq8l9w2;STz$t*ZMCd?&6h6Q83TQM??@U|Yu4}cS=rg**flDBfb zfu5E>datzbyKL&mj&-XB-E!9@8h>tc1mB-4;T0_f*gisjoY5Nc{d`6nRXx~(*6i-0 zt_y?LW)`N2BfC_l^CyH6WzRZ21g^tf12!08j}Yw<-!`5Jt;cTA{h$Ry$=nGOcgOUt zE^oC-wIa0vlULB3!tYrl0sYqKzTbv^!pFP^>MTvPB3gK1gJ5N*Rmfo|_ailt;=#e% zBP@%pXNuZ-G{xKyLY<1>mQObmRwBw9hW3=)yN+73-SB&BftFB58*F10_r6Y5EB%Q* zA)G>~*oJj7 zXXxiOdA>UTgIiaJnPZ9B+S?+ddeav;-ElOg7gak&C5QJ!EpeJx&;&>VKwC(-{A*%@ zRl;^P2VoE~n^Pq&+JiR-pE!K5ZwzDY;;URwp*xM5N{P~}MxpmbNY991|2PI$WEjq1 zQ4#FS03+ch9lO&5#X7FaxRxx}kqfWaKAP2R+W0zW)~D9JhEhQzh^jI-f1x&6??P$R zFT>W!R{P`*l(F9F)JcQ&c08esuKP58kXMS1}u#fJXL+h`1cv{?2vuv|tqLZ74{9Ta-}4(QEE<9x zBeiNiuTMT{c`i{3-TDeNgH@-QI3vPbesESehTkhwAqy)!1PfM}@gkpkgQQGQw`wY| z*WmlOjmb+BKb4n{@p*_DLqo6LdSi^$LNNq{4)3>_Vg4e{s*F z$Uq&flK2nu_%~*qe$@{mK*!-Oi8Fz)w%z=3H=);ac&{_80awfT(--(*2T3Zl4+wD! zNfc@=r;NVr*`VlJ?Q!k~i)}Kc*&nni88me2tgmSj6Y+pCN?#WJO^GVqZvGY1pe$dh zjIE)zZ}qd_Q_tC2ZCN-E!HU-A2@qivO)%oKwWRQ_?x$!G{thHx>4L{&{rXZbBBDhx zLR&rSKYs0j-yO%Vi?B&W=^bxyk(_x3r&E!}^8BV)0v`5;g`a-UOm#g?>9T19TlbiR zGTB7#$&}R0TqUVW{t|p*Cw6D~YlgxLzXre#^mRqdB}GIzvFIdl7I!p5N9PpOH%=Lz zn~REC*hcpJ!YzyIRhi;KvvRIH8VH7sn~+6sLaNR{b+}$ba8+ValO`81b>RPDY-$Uu zGAJ9HMQ5p04cd&X%fqeAF%`{G?SIsU$+t8bs`I7fyeLglH+sLqmXQCL_*`QA7>k~z z0&n8!Y16UC3CDyi>^#x`HtQq-)47kiD$F&t?LD94dN)6k0Eesw{_$%J@AaDTBR{M2 z9PYby@j#y*eq%H6gS>hrzS#VuG;CM%3XT0sa3_Hs4U#pIeXcMV9I^V1et1bjMNJJ& ziUxrCUGK^d2x+vi@|EF6WU<^fqio@zcm{C6s;TDBY155Ae(&oZOReA$xkDL_Ukw%p zozixNE&US6-i=3)=iL#TG;Sr-j@BWJ!JbUiH?nFSu~ zutM$NCJMx$i~z2=@|eBOG+-i~$%QPvvb9RCNs9uJEd%QAJX^5Yy1G9#Jtdp8{?i%l zx3i#p=ICWLK_V#rceG2HSczH=Ua-L)LZRWBXwsSrwUsb0ZAwahb)BsDb@V_Ftzk(- z6_;;Z{%!qFbt6FRA1kpeP+$mUTG+LI#HAg4KEyPH`K6x{jYG;F=y{0<#Pl{ z5$Q4#1%~%x@F$F2;BB85AIpO@UMZ9*XzCW&?ygl3MnNMF9oH0FPM% zUF6;?cc}gAiX7nJWZg+aL)R4S8P|QtsZ~a+Xkd@;{=UO|0-yV0u93?hy3yphLu|TYv}0b&)eb!eXbz~Mm${|Lh4sP;U-^-C{bqOonSa- z=Kh7`yT_(l zE^lS3Yh^UbR;1Csb70p1EQs0OwDNt3OQHDzJ%sCJv=6NqN`WbJW0SC_Y3i3{*#x#s zl;n%*0|)dU%6fBlA>@Tr6USa}g(9T+LqNb=*lH{#N>Jjq%wji)CSkbvh z6nIEH@l!yH?3Cv)S2^b|kqpjao3V+U2EbiUNn{%(_Gqgv7Z%icbR!K09Z0(v{B!b{%# z>Kcjh3yLdi`duv+UnD7SA{w5*wJ3|C$TyNMm*u>|vPUkRJ}Bg*Gg!lJD~! zEBZU{a!DeJ^(bSJ*K(Lo0g2j&H9@9 zTl?7M9)-!6V40ATPs4?U{ez#+#Kk^?wer9({@aH7#3(m%q^D4M*gB6D=7bU~c$rcjGjGZG>!hKm7iT7my+08ANeHNC4ToE+6+ zSf-}Fp?7r`zGg6t`n6EbAbEZtPEg&KZPoL?Q=syekGp)mxl!lMgM>un9MKvfvf_U{ zk{KT)f!H`r38qz8fQHKIaqNWi3lxcX*W(WaiIs)P5G&M6)>!i1}bd~#dC zkzd(KHZf+$uIj|e zEI84V$IgRkB>Gk(@yG2B)GKTcH%c}I-M>usQ>0H@ARsG)#K?|0izW;JAMBn*rNFKv z&i50i&m(#8L$L(X>_xgC$Nj{blLDyqTYX=xEQ9(dv4l$=j4XM!<&Y(Q!-lob{1P`2 zAIbjp+XpyFcv>~=rJLK8&JIJrd-ztQQtEabieJw*U;X`VjFH_wtb$EcAu%!IEn?|O{X%|OGx)m0T(QjoWnP_TaAw~GQ)~rBt zX3oIfHLWS(vE|_lc=v^p(kN+^b0Qqfm)68RyXD#+oWFY;lCwq!2K{h*SNia0;mN!^ zDFTsrMRfm48XCoBtt1y0{nj&unK|0j^sdCg(}$>IDmQ1oF>5zXW(v-{=f^=-!s20M zY3(#?QnKpWD!IC8$WEzxy=%A7<#1x_VXlR{(6RGWzDEGhylxVoo|(S-bpr?|8uygc z&_!<1gAgio(DcnsGOPg)fvY}ni>rTzFF!z3s>L&~#xa>^giPVKh<$&np2fCaf)6S< z$Q*pn5`j}R;MCOC-k&23qh;bp37$Oh3is%Cw1Rw0-1jK^Q{@LwjL)9mgP`q3r-mNIKQuJ69DWi zC3PF8`xkg0imj@9nq)^n)@<{7!iFJ*zsm+msL#>km%v6>d#mr{R{?ZbiB{y1B2G_R zYnoR>2Hod=WJGR0Az9B|^pQUYu#DP9F@+LgoYg@zmUpmmsr%QP5+j@!N&*~py_Bz0 zG_&_kuj}$otmiTxmce}viYBP#Q8oDKK?;t{3O%d zpQx7u>?R1PgpbY}>PaWVwXF=P(Q7x6OrsiRleSi(vrjMPqyMxlj(!dd zGPn01`-YYuC-G(DDN>mlkb`*p*lr%d!@jKpKr1@pGt!_*B`dCEeQiUqS#Wm*rWX76 zFR!Vf2LofD4TEJbF|5y=w=;GhmX~`n^|*4!ZSpX}IlmTS#IDE385vdNNW0FN8&h~+e3+dt7Mt`hT_yC!@wG*1IO0W7%iOm~#8XB? zGc@d9)z*uzP?68DmI zR;nb?XW#UMX)hgjxkQXHR(P^B(Tko_*c*Hfe?xV}s4o{1oTW-LKbPm^CIgfJJls)C zNuN(!i(0-108c~heZ6s6A}YXT_uXKnxm51R!Ec1Ymf6znEw&aO`KFJ{?tUE|$bYyk z5Eh~=D`(Z%PvLC?c#hS%oKQ;rwMbZ1KUgGC{m#J~Jq6L}G+Y09z(wiH{%eM>e;2L) z7w)eM1lq{Y;1kC0EQkOrDB?G)B)@49@c7AWNCIl(8C}0(X7Fs0VHyEW$yg^B=aK`T zj1dqRW^o27;VZDbSD}&)>?$N;ei|2=Px)b=$bdZAGHw$Z`%#cmYXLH8dAj!TyMxGo z!ER(l2BrcP5|h&O>LrXeb~XFUpuv`xDrpg3wxfd#Y2~X(u4N<5BkkpN^1f|l`fFFk z`@f$oeQnS)^8OL-|5XJ4l{E?1mI#!jnuF|%{U6_Lf5wg{v!C$F%Az;P@NA$sL!-tF z6Bl8xv3X$5N_gw6Xi$=QPYfIyw5@3(HQ-o@htI&&!XKzW=sfh>D4!CpYNMVzhX(@& zKIMIl62OXxBuKSKA0v+zYHDt*Sg{-yw2dWWf;uCcGokvs%{%9(?mHlq=g0bT4c;7) z?Y+8HRWNUxVGK4(^d`+GT-+LSGX|FS{toyCex(|GJfNvzw^m_0{GJck;i@Lt zc!b5-6j&HN$-6|-NtvIHUzt~?bM4M!<@eg*014HpfE;x%Uizl9A-x@dF0p1FlO};R ztSp9yDm^NDdv+<Zj4!!SvtVb^sKjLjXaVuLP@NKBqq?Ea=DePy3IE9(uUn6wr^PZ$Q(@1Bu~fFer#FvZ!H}(Ymz8#i?&# z{z;k3Bdh(dou{*$xMVBqrB={=zKBI4xz`h{J!kp7`|cqRa8QD@jEZ^_V-E?FIwE#* zqEd;vGEtP7aLdc$_&sF2yhJJ+L*C$JlySq#oj3Wg$+Srz*a8@?KFFH1Wb3Hv*_&tH z2Wp)HJ~8`vU%AH`EQen`%ypM}{D(dD6A(h)B9#Z*PrF5mBe$nzdVICTv$4T_Mv7~@ zWngB?Y9Fg^0PtcAI6rO*&rJ#!kLlkq)=7DQ%|1obIQZ@(BZz#wHv&x5()>iES}EKK9%_uC6F~yfJ~+gB zMH(G#3VwddxrOH%T0T#Vz(gjuHPFq-Fh(&g*E5PCQCKzA`AI$3G;mUGVV2f2j<0CG z->q0-A$RrJ-B&2J2)mAgcWDZ#ePPWf>9X;6Yc52A;dSi`P#rZj>+70hdznh+L_81j z`X6#(Z@tt1>_qSEL>sshDVMF14YP5&00}mQ{8n#o6SqUxW3)=T_qMUL3!ek|Rk=8x zB`HW(=Fe-rFOSpef|N{(df=<_VUKgU+P1O`1$;o*bV_^c8usTKRcD!vb({Y&X|>9Y zMI+tjQp!E4EU(UIVnC6e{x_dS4f{y*C7*$ZZ6Xhvy`a9}Np*2n(stUn^3>+2rJv^d z-k(OAqa3-BMG&jrC*!BNP;9N(Z9SdJObC-=L@B^~RLX?+EE9+HnPE~QYaKC?u%czb z&GB&Ds~e=BWCqfywp{)fQC}Su)fcrbNJ@j$j|Ssh=q33cYA~Vs*}*bnlpNrJHu;H&HiWq1vOp<;D+X zLqA_#KfmutK#o%QrTi9?!bI39tJHb|1*U5x)-&2~5)PYrwyWBcjL#YZv{BoC@_Z7C z1b2c7o(D?71CEv|g57Lwf28Q!eynJ|zSM2Gw3g|bWc;v?mmlY!bvPnagA;A~ykllK z^C73I`;Wyv0lUHvVHOod?<)CPTY4@Bi8wKRUlh-IeHl38sIH=Vb|$7kF{gWW-kGUznC^m^t|;$4-y{eEVzTe-t_GuJH^)PBTvf-3%9oyIww} zU|hpCBuG?!8^}iBJ~%U%LbA8>WaT$+_X?4$Cqq>p@)ulQz^Nl5H{{1IWcNqYDVY01rNxUKZFP)ae{-f z6!!QTQ;EGG7ZPhX{OC{nELl+J?M-KZ5&uDxq#xt46TE59*hsx&=ja$h95+SkNTuSA~>kJQNl# zi7(#L@|b4e?Lk?(*9OphG1E%fu~1#)WFcB+fb0>2_kfwb*0?a=ai`RZnF7Pgx~BHN z%|r#z{=wV#4Ls`-rBcG3w3g`8yT?JTd&30V3lUnu9*Lu4qu@X}kLmJH;G?CiQ4^TX zlgD!#k*du38ECpE6|+~b*q&49-l|uk=fUle0Z8d{ zk={o;E%TwgKCj%)SQm}DtY{&U|9Bdnc)Fsr*2gZ+_f}MzWmLA&rIg)Qr1XbBoEXC~ zbY&bDNL@B7J2<6;RjQCdAZdC=d2?UQgC?an`yrG|^fL6@y~1RVDbd629ZN&QDKPB_ za;?4Kth*m*ya)9vLyb)OBm-h3B>O>JG6_#hb7bxn5Z8Z_OJo)x9`$0NjLcG|AkTHh zB+Z@PCo9&wUIeC9bKzv3vcN+mVLSsROC$Tv?gulrsE8&<0zeZ*hU&%9@0*Q3U5ge7 zx7G+){<)-R+x^S4CL-bws~S&hYg#%bTWWWj_zySL&M(0G$p4ES@>IXla1CSSN1k7c zmjh+`G<5g5B&^5Oqbc5anH<@)%XA-LL>DoCNj zA$Kb)inBu61Sh6=8{EWBx9 z=zB-J=SLR5Hd!Wj$)yS}Pt&AW%R9O-HuLBIpU4+X~+^VJ{b5qid zrWq=~rdc*mpVl{GPD~KWjlz__7T@<*FaeXKfTwn`{K^3hyFiKL?(ydEb_a_C{S(|bk7`G;THs?6K;-X7935Y`x;o4Y8ZpDiU*5u+p2?fI zXC&#D4<`GMUxhruZW}0Khxu~Eik@UrRb*4wH`S~7`$X}4>x1VIR7+Cu#mGE94cLbl z!zGafPbez0m!$Xnk{p4dxtvwn6;KW*B42s_o9AdUL-Taz?2P znw0$-7XI@;9um=T@|zlJZ*2X7Cl14hTWI)krcU@D=n-KXSr1qsANqg=!O96IZ>nok z*w}w(WtzD{A~r(&MZx?(-h`f9%rE!1n{RbM+v#S6_G)SSkDT0$$VV0`1tt?BRe@Ae zR(vSRUu^D|EkK=a*DuxX-;$f_XoAyuQ$wl5<~iJ-?>V;b!}<%CPR!vyeb~8%BZryi zPQbt3JMuIFc?Gi^jUu%5_UeU4n|)>C8tBsv1Ep9sK&~z^QkywX{nN+CncTr10d50h zC=$m)Y)Oo1xgm@1g(W}izfa2;;{4{*ZX?kR0Kq!!A;{W>u*F*ey~2k%05enqM9~~- z{`MA`tk6`j4Z_)2;-swltQ>VWZ05%zM+`+*^Oa=I75$Yf-W3d1sJL&>k9-ztZ^>qW zWf-oed0ohz2iC!0LQ8=kIZJj2UR%^Opl=YLj{Au`q%Z#E7X3G)u>%cDdD;PHQb3}O zPhhL8vN9MgDXHu2vB*>G4F5B*c6zc~7hpNE#rG_s&;;e>CSW%#ypZ^^_%W;X!B+`l^kZtzV!9Q6S^suLmqv+4TNuu;nA2;u*-fY~f zZ3p>&VqD7zY0;@1P~cv+aLIsx#wWL@8Xr*^*XWg8^}-<}m+q8@WQLFQ;_pZ>&34$& zfC4+YjA+(L9aVOZlv*=vgi-Bwi7G%@b^kFUpH&B-PTa${=|vH66B`@4FAIRqBu_hK zH4?J+91|Vhv;3M>{bT{53aHO^fHA2fPg+@NChZyvTyhKe2X%yGT(41X-b{9M10gyg zmBQq%`Dm_8W2O!@Wt6pgV($s4%Z z(+hyK=LTe5`7as-&Kv;)ib$6ujpCl|5;Z49b2(Yi&7_GJ9t$o#QpQ+GXZ2pv3?Tn+ z=~VefX6SmAY> zy8P!BU?Rh2!aBAk9>VrqjVqtj(%fCXJAU@)JBrOf7~hSS?CyJ|wVOz7o?dr&JoK8$ zejl%=P&-EFUnC~`K#0}_Nz4#H0uLKCp_DyieqNc5c6@fOP;iRj6RI*1hH#*o63p+8 zs@nvah3R92TZMi7RiFleq%gDr1EMfn%?l3fi3Z{kH)YFx@aQjLzCQX4T*oKs1PMs- zvqt{nfR5qSB}CnErzPm_o7uSoQ7qCt|7lFb*LMYy4SlCQBy;rx?{<|!UGXeryEyM_ zqyDGxwsD1;ONF;zBYTIZ9mt6RhSx08^9(ArnyUp&*c>S^3av6T1w_e8H)Vk*HL%Y` zfS8wyd08joi>8-#*Q!EuBqmX2g7|&wHguXQ!EX|t&fR;eW4c40ccJpNjgGLHkf4yv z&o;6QZ)aa+j$h%M^?B(e8TJP>ghtlxb_7u1W*PMF3fszOm1xlZ{6J_=I&@O+E;ya0 zuU9;0Zx+(_VWW6TC8UDFXQKn-ZT8eCxYI%4kE{qXAlJk+@99f{AYw}C=@3v~EAQ@w zWzKu)Cxc?2`gK~B?@mQ<0;>ncr4E1H$-IQm^G=oUx^mP1#GMXoGHGv!PcnE>?78(z zs#u_1LF2$wIi8zSJK!EScpk!kC&po(_NUIccJz7MYhY4fO%AHLG zsrZZ*M;u7@udJqdGeNM9UP&^!u}i0z%Qx6v$XT4PzW>1WGxZV!{%PHpGTB*V$Jtlk zkSueGlew$EoRvAC!t!&CLWnSYjCD1amiu^Gutx> zVyb~uAumRcaGr)ip-6**?|Gz;LkOGQayu(nqV1t`W*qXcqg4W8(2}}--PRze7M zKs3N`373vWS!dVa3PY&W#K0p5+f;=+*)QtN#Syd&3s$(|-a%o2qG89(5c}*I{^K_G z%eF;$gAs^4<~WKoXxUEjg32hPbIfXou0;8cTG@Yg{8InrT4u@)GSLy(~M&@1&ppD2v`w?xmL9bP@4FR5ML z`F-+9h`*26eBlYWNuTrs{appvP&ov%8SQ=He_lo##w{I~FQ<5<^Ummu-e*bETdd&X#HjdAI(4=)m-nhZ{9K5!OpSz* zd}CyKKOz+qwl@xqsTV1X{!vB>q!)gG$=I-M&JfOnN^T5Pm-+CtW46`mzlnj*~6TRXpDV^ zCgJLqW3LbOG3#QK4bH+DCSlX5c{ilC{^%JdA(xG~8^{pxwkNW6KjXu4KtD6%R|lH;4K1qz{`K`xZI{JRmIsaAs#xiK8z7}ZWe$~KH%-O zvG7xC8n@hhEBUI$isbJVZLYLdh>?v+NE2)0;(!$s1gAFaPsyux@P`9RmVKjer8I_X z5ZS;Hh6`bqcY<@tWaom#P6@wtz#RWgQBZ%Qg`y3w`lvZS54qf1*y)8%2t2=wgKNr8 zxpE?1stDMrQodL`lCITu_$|315E;ctJw*Q8Yt$?%-QI?!%A!(A5QoHr1G4R7;>og$ zHc^$6TOBv#+m{p9pG&)ZejmvqUK=V77KKXodP$|7ycqQ{yr3V=QQpCZL4GWOlR7uP z1}v)Il@(ktE9}B0D!FBE3>Y>}(z@jTvOSBY$B}?;lIq;tk*>JnR|q`Y5R5&?*q?oz zp?USr9tX|r1B9JR>8uyXEx9Q?9Hy~g1eEWWH~E^x?CI0R5D6$<_SoV=ko?UKh^&oH z5gllL7mR#Tkx7lvy7Jj@b<)lD$Ud=pWHA+Y#ajVJAlut@xyUiY?VrAleyo_+M=^0% zv%gyTp)Wz2Zj+U;BUk-WK5lOG-1e>@2|6mQZ&Ek8gwGZ@N-PytgL#iH&ML57`>`oA za*0~dvt&VFsn$A@s(@uQP4@UIq?}C>+>2VD{#k{&`IBW1D<6mQED6btz4drU&&?gf ztBs|A`Ch)iJDIx}Y(Txz*BFAwEkOS`gg`jgnKrUFNM58^>kXTN+1*^H5s2WM+|qN* zVmLNbV#POS-8bi0h_B3K$o6I}2C8QBEfD`dXUwD=d0o2Ykmy@&a;dc5ax*Yx(~IgK zWrC?``hJPqVM3*iLXinRFmae~*nOr1^3VIr)~ly>dW=yRHpqwyDM5TgPHv3GldTMk z@_GML*Z&}#=Gvr7pBb8B&7PnBDoGXxnkZs&>fKVr*yKj^7Yi$BlBgpw!LL%`j9g^chmTN@S&Ghw=0}EI*%|dqK5aMk^xRSAWwFfq{1NV(ssJ&H zOb~^g{T%0S8>TQ`;{jr_!H0_c^^lWo&~Rqm0QUAZ_2P|D%T;S3C#U5|e}qj{TQT-I zrc9Wb03BT_3%vS?Y1I@OZ0Xi(ZJg~+a`a}0xaX&SSzZw>rD8ux6K>s}UXaJIa(|P# zQg@DbKEf)gBSqtCHfdT)+3t=>;ba(qJjMj4S1;qMXX~wBp$?m`N5Ow%q}YSQw)=$A z^u0p#!K73yUWh0TkX$9OaY7G?ne~u5-0`O-im!E2MSzq^Ypcq%Q?U9}sJJ+}db9vA z4TEIujh}qlSq+!-X2A3CVVE`-hmWCmoCqXz>CKyt&+4~j1(&31`+yw~jQF!6(O9UU=1Ix7VkEl`9IJOXJEq{^bB8C%rtB?X$pX4eDWmAT93i8>Gt_&%@{fr{Fl8P{;P zgTA50v6nLPVM%{%{t&!l>^N4~H$Tf7k#ZcBSZUN$G&xYe+NO;SO#1y-@2kJWmush2 zeze4vWV?99TR)xT-7kz_a1Od)dR#WG;;)U3Tfb-~GDN;;WDa2P!mRo<0Bx73nd<>X z$m?(ui~q7QW*&^q>lOh!hCucyew~$G3Vyu-8{mle@RZ9sM)%Ik6gttJ(QGvNJmzw1%3o#)<^xm%8fX7;pJM} zz)-2%t!K^)lKhKzZ@AWOFuKfCny)u6rjiz zvPGUNZcL{gN}9M*r3DZ9)k~R5weF8YS*6f+nkhBxg)@`<@d?I z9FGL8gBa<1UZF_V{e47nWM7_oCwkO}(XXGNk*E)joj#?FEj8jeL+|;D3WF!>8b6e> zRX=Dbw1SB44N^kMsGk{lK1xdNkVar*6B_jYD{yU!Ro;}TP;ae@KB?^o8o-jr5|(QL z!S?)6*zZ*b`pZ$XrEQ`B3FZrpxN&abA(Qaq8Qdag{RD*WD&+a^^ge5peqBG)Y|)D=VTNZ=>SVZcwM#oO3plZMf2Ox8dqGgI+erH|uWW^;YT zl>NbjT{+T+?J-*p?1Q6cY~rZG+5chqBheEvMo^JUu5-2Rp* zyIIqIgfzb?yuqwva$HR7UXE+~@UWymbs&WQ219m6erxCO#c!1D1(4(?0ZB&gl}910 z-*fbrzph3?HGe@g_vR^o&rl!fNDOkzXk7CLP_05h>Ox(FFP|Lff|tp{wJuP>acigz=^htJDroshCXU&Qh!(Zd55&kE$>u6pEM!7 zGvZYKKD(3p08a#o;CL%j+$w#B?A(IZ_P&b){PqoI!)CwT(jB~#Z~p23In0TP()Qdm z*={)jJ<%x~3=RBB7Y2m>vqX1UthWiQ7vT&(+|Pc=C{wIPN0r-5X%oN)&-$vVg9;ab z7-@9!nj~YwH;Ds?vDq_P%31GKR$xv&F;E&ZSjb*H->X2hy;?t1?(R*Lf2d936F4%8 z%jv!YcMYczz}@w_X`$=wXcY3_y!1DM#gnUff7!+EfMa)BMZ&)SSb7o(!^XHWIV!#-ZAJE`A*C1Czrf=J?v zds1AW3V}q_3+1|WbjnUU!t4_Qvj^{XKLiJGTeVC0Sffm+bml#e7;#Wc+foKd3Hs!6J*m)= z$|nFPvGprg!`Yd*&!g+>{TP;Y^m`)m^KqsuS?LGfzt0%5cgE%c_UjVoZbq9c-qybl zIQk4^Z`|R9wDGHrtd(mcueeqz1>1xJHuk>V`$r`o#j^ zG#xW78|)rF@5tm8|Ke^SbZ7tf!Y>@!zP6$2a3w-V#(}1x)&?L<4Rx68UU*+Du@^qq zYyVU-=A=Dt?QA!o?HG2MDe$3l`{c#1BVL`9yCPa>t`F?4YNZ{a3x(AdF1CrZ){6hM z`Y>}Puzn;*sZFS_xB(1k6CK%>$j}sfw}TU^Ui7!3^|}JsaM_18TH5hnL=*dFKI~Qc zMUBnf7lTBLu>y7SYtWVC*Cw=@@{Op&Y2sN!e513ipCFyvUdZ{wQwb&R2Crj`{U_DN zSLJL3#eN-0JnW5js_)0s17CsdBbpY5q~ID4=>pV$FZlyVYGM4+A5XV~#^HNiZd&-1 zbN1(Vq@^Ye6bTU) zlgMvv|L7oq@Yr7V;LLEUA2rLpVvFteh!fiL6|e9=JIPs>dNq3Mo6&P|6<)GbFMi|n z66qfe!LVp}g7gf@PW?mq*p*n*xthQIFciALzA-0`A)DfHW)0wj_u<3ETnFQ?iF3W# zs;qK3Unhc>3|Q9l-(*L)omg22&5n8iWeqk`H-NKCTx;9Oau&t75r^@ln8VItl1T46 z$L86->1wO+Y6h9MEX`voMH+cwxg325$&n-rdihaJRab6 zcB%;VGG((6ibJ``B3?3}9PXjPK)GsIPwFpEmjKo2w6?I+pOVJf@bW3zgRw7Gkp(_# z^0B<8(S+r`2i|b1Pl$js^X)Hv6a_=&l0d`junsmxUXfvT0U$Kk9H`TpYn7mc3`1S@ z5UWtz1;7ZUAIdC9hJ__+KZL6vkU<6b;@y1}~h8w)^ALG^D|6BRlQ3qN=jdqxPlM3+fKWo>f0G0(5mR0wAGmOR}){ah9$!;Mt9N5bG{|$9Pu57KlHsF98fbj1APxPM0>$Z2!t0%a|(w!23^)?N-%& zs_xk3!N@01;)7t;P2U9=XUJA8^vAqx{-92`r-OtGA~8L z8ej{Cl|3!-XUp3ViKUy2wf07d#S*#iq+7_5mk0;-w`8H%4V$R@vm|q1*2#?WSFVpO z9v{*oo&r?iXYYgCW^tOf;lrFx7*PcWxc!)=iUY{sYNF*==^ho-aA|Jrwn5h_unI&3 zo|$#!5jTV2r6}1*6q0~?#`q88 z?;JHbbePRzva}he(B&IvZ`egRe9w28KLig~K2QQ5IeSiiI9C zSSVT1(9?e1ETtpKsUYCwevIedXX7aoo#6D~zruiB|C$B#LkUsl%xuaY+(h%{QB1-9 z`VTgSWpP_;&HO+L*}-a-CoW>}ZrhO7IJ*-E5jb;r>KhKz3;|Sr;Zvq4l*Fo_xvK@km?x@go1ImD;pYx|1qQ z2RXEM1YK(z(2pE`ZeQsENC5z4DSeTG)k_R3YN{v^B8dVv^#Ls{#0yR?#-+OicHEJk zUYFA(Exuw+))tW@!>ORYE7Q?Ua>o$E<8vT$3VyTf0o^E8BD?Z{b;J!cALz!dHg`JR z75_1OVzUHzU4c<#z;E!m>TSHf0h%tv`z7YFCekH^**IKRuE5iRK{aD?KGS_u&2D5* zvrO4S`E969nKO_iehA5!h>VTe28UUBXWvB}oU_0qu1?9|N9r$TQ>(X2|NDcKt#u#s zeeEl^RPGEPnIDB6SuWa`eM}+vgbx>oh2U9moKfme_`cvSt6811irnz`ED*Y%c^|`f z|G)DEv<+B#$C2x>e;$DXfqO}7yL3xkO!*ALsy(suc+^3MqrKSX#3-W>JTe-q!GGrZ z-c%}*m!h8{J^ysb%H_5SzF6%G^PLBuS9KR*w5`C()50%F#3i+iVBRso!lmj!r~t52 zz%c;^Z)m^HkOEF6H2^||2Q4jp#9O&K;tjuHr98wT1Q6?e92_iTeL7%vJ*CI5FV9oI z3@(Kno@N7c+*+M|eQbcaxM*LG?VkqT`qw6nh47}ZIlXn@nz*MFV8BY^8W>{cV;RU;Bf!{d$jF;%7;2W>$QEw&Vl{>mv1~Rp^l?|JKn7_TPQkh7X zu=cqH(m2oaHE=9?hr5(MH%3O@12_>S8T@#8$QQW34Vl1WMWhzsevnLnFKticPUfhr z`KjClIIRAO>6Jrprbg)401v$T4hPU1j_|EJorPP5=9(Z}(+5N%dx)p{i7V3|&+$i0 z8aO;FBI}>K9tl;wkGSqX1qHwWklzCe89UZXr}Ba70i0Nv~7zs)3%f79Aqb+UiFs0 z6$<#KymQ)ZhRW%tRjJpmEI_LBJy-uyqLnxQEf0sLvopZpSoUWvhR(cuYO!RW?_fz!x_w1g7bAXK)Cc&pCk{ zKl*_->SHqaS6qS%7{|kdk>D2X9`{GQJ0)8Xg!SeXSr9&##Jre|&h;aT_49b$00bszrG80!F*oPBY%Dca0UQgCt5) zSrsTnb?CA%&C${+0z#7H(Jb-%!z~HB^Xm;*LZLi%C8+GXA0P zTi39^P@BBe-ae-P3s!Mk#2$4{0KfodBoLkZ+a0+k!FW-Rl!QEg(J9734)|=Fh(j~z#qmV_dh&@Qsv%K0LP@kj__>&^Z+Q?_Bf;DCsvCpk-}|@&M3R^ zo8yLETAQcUyB!Dnm~r0fdK=M=_&NRqC$~ZpHcQ9uOJ_~Sw=@@-m}liS<^kni5lC8x z=S7MUo2RsZ4)fps^8`0csviK#8Sg3pHiP?bW3xlNV}=i>6I4954h~!b{9Zd-p_^33 zTuvU%DfQmh1#@|~4!TOZQ0172p5-$u;X2tp^QSJ@jTr^P9aSV*rZZQ_2}B$bFmiM& zM7@X6oL0s()0p_zdV~mFcqo?W-tm zY%~6NP(&Vp*NMj1=!NOAtFoxj@I^N*TmjRve&R2^mu_#jrI2&rVVqcqGYlVi$I%YB zH`3u^y!+JdmbttWgY|sy7`o;k-c1;eeGd}pvZ4=s63q-7m=~`!H3lTF<#TQ=0ZK=d z3HFWw1KNuQOJ;g;W3wtGF!f*)L$CfQSaqU+Ps^_>w#RuE?N$`t6W{+e7kS!&*ze=z zU5+jafl4FnZu`5Du=6bcfy5kAp{@MpD;$XEGCyr=SygwA%Tjqulz+?WTXn+%*laNn zvI2_h#tyEuZqL(B&32s%(Em7?Dpnv}eVuHpSE2&MJ14#VKq{oklceXZ#S4ZPTC_F! z$Yk)CFCDbwv0CTG#a%rTKKkC$;5*A-8puFb`cvkxO>e9)^91vz^ovU^kEz`mKcgaaO#S zgzr=$W}D6v4py~fQp{2&71=@wWl&QHWw2Zstsv>kskkRT+VkvF6cyB91atY70R-MwA z>Bn|JwM{;m4dtVHo#!7m&aS?RaxQ-=vJXU;We6oSH;xZt27d7EIU|xD4Wy)&(n@~0 z+fW)BhGYI{vFm%J*9J?*fVjR2ELV7RYRVXs_%0uwGWRZ`q$IBj!(z-e;^i zMRx^w`Nr`4alLfTx-}?RaMjpWIZr0(;oY}3oP>7cjHd`&bni5BJ2u3J5e2pBTH0-p zQOy|Fx$ZM7l{XGXa~*`YqJe-F5!V149%(U&b12SQSv}LeZ_-}a;!B0_)S1wt?*2`&=wQ#UzZKsy2ie*TU%9A8otsP zypaJ8FtShLBvvbT|Pg?S?|htqOwaL#2hKH)#>UJSb9=z-Wedi9vj5$e)WD1Ed=v8e|UA= zrozM@+JDBar`M|XTi-D!(S7FLJjfrc#MKE6EE5%PRp_8Y>34yCgD_T?^b3M;8Sj1> zHC0{{u&KRE%&K0CJ{oMTw-%A26H{QAJmBDGL+)r{B5%!4mqcq3UXCzuL}*nh^`Cb( z4)O+_JXCjoM_R--RUgeQ3QP;-)_9+YO;`I%L$*b|VNNiUs$0$fJw3kbDx-I>4$u^FLzJojcBxEU7pW)foXFZWw_ZtW$dDus569-nNcSNXvcAiYNizSWb zyp;%eb-m}4E#p7CD-R{JIjQ|H{DMqAGa2he=}pers>_6EW5D0t8vmM#ldAJu$I}2o z^G=xkn6uyNW1Zj9ne>+4st~iph7q_U%&Tv16Fisgu|SfdxM7CP5Gt&5x*G#ro>>d| zn$X7ica2VbUKby_qv1bBtJdW55)!r>+G4MK3dA8ASNkM>*-Tg0#!4&C>Khc&NA0ra z{r|H`7AwTsPz4|XL0+)nsiD^x5w?tGJ%-`w>U+gy3SYL2TLOJ14h31bZjfcpbef&i zJL?&Ds+G6P{5(d&UIH&3`d}Za`q_3-TaUgsy>9c3=uYKk&lT^V))tUTJ^Kc3?wucv zzS%3+trq=r)wg}OlO(3fL9m}ShuK%!xcN$v^gzD6;pg}q4RCPX$qpuUb+MfVY!jM<&34v=3s{bPlb9SSp$G4xEI#82Gkh7lS?_XVA=&;i6Q4YxY%Koxmo0z@)<~chn zmCEq4yy3_3oWG!RhxtiAXUgp_l9Aj2X45rZl=L#Re6L{~y2^{@2il^Z>QIA8#nf z&5^P=+)!FwocB`d-mkBkm)h{}JanpIK}J0){m|VidS!n&{n7W6)>(HL(f~7z;wZqV zwcOrHa9H1+Oi-`&I4RbQR@;5HzB_R*K{||qC$wPehhSA z{>OmxWlGP?Y7K>IrqkSNjd%Y5gTc}ewrL7j{7K})wVddSi8sHQY0xTDAfONrl=pxO zc6|GKA%b8`-u{O6=|q(Ey#w);IJ6_~(x-D4>bmICn^N%x;mf3|7us!LgN*BksmV83 znMR;hiL{5SI$5=vqMS@*#IIewNSFJ0y`nIJck(KwQBSvJXs4e^yIS1L4u}vA{?nVb zF9teIfO}IM_plI6uAQv6T4?;^m;Y@fDgbAz+7^H}>NYYeCzk!wx!Ze0dO~!|xDj7_ zUiD6=02$5eWm719l11HsABDZxG0}m;$+@H5v(a!>VeD-ft-H^>P-&b##%v0JKpNMB zb5d^H^uz@J1dk_kCdJWeNczjnwV^f#;AR_`4Y@ytNu7HX6QN4&u89$%u#ljsCE8hG zV~9avxv{{X+i>l%fBo-1H$D|1O{1!+0uufMkPpcoz8=ilbz?f4gvc$O}l4fwja@f1Du%Vs|P>0J-Kjz#xFixvtd=XJM+~k zK)%ffeU3(DOq{%28G6H~5%3iFyh_F^+8N<1$z#`S%VTWJrOYAGt1=|3&0#r-?^$9= zj{k<6hl&T6HS?qFm99m+##i@zqps$Z5@%Rw+kKinN|j^_KjAy@GRps8yEsH|aW@HG z(}CZ35O37^=C@wLn{^UXS{(-JjVqg$CuUyGZOF?_(}O0F6x?X}J<5&wAIszxhtpp@ zY2wbxLZmyVK9;uUMW7nc54%N~fAqA81&n?l%Ha)T4t?8Nv;O1W4KkmWe*)l)eM&4m zlju0heE$i~45F^iGeiJU*FvL5EnHdXefg{!Z+*hcr0p0PjxrdNM9hmH86vsi8Fy=f zaT*YX8=(4wW+2~zuWyfg>Ft<3_k>ZCB4mZ5@_byHGmq=_{YE|z!^qO*gC;75b$2pt zJ%gJVhwSnN3irQzD(iFK>wkw7J^4nqf-W(jET!x}-n};gL#QJc+j!akYf|Ub#_x7e z;e1?Ij3@Nt34$Qf8RPVp#}tYDe9JA*L7A|h}!gcfdrHPN@ZQH=yyrfbq>Hg5*X>dncw8Q07>1&A$^<-wN zYK7)Tg}t*Q={ICda(r|6MiFwTN@!$*x-~ChAv@$Hcd6DJ-G9&;cN;}`>xF`aTGBql z-|O#d%`JLFnb$TGG8^Ap4((;K!u>L%Zyr6h1bIcDO>XMXZcaOOFIILHm@Hr9E^pAS z^aZMj-*G>ty`%G8xVxc+1neBmW=6#tSy(+<#A*($z=7PB9O$Nx?Z(z+PZ%R^FmDd; zdb^(lr_@NlS=jyTi9Bql&6E1V#5Y|h*4)Z$4YfvIAN)xBTJ~B?`$t_*ITr8d%n7$G+z9D`ELA>H z_TO*hXR^@2Ef_dJp}h8Sms`$61DRR1%7VS-{`c3s1GsjkANncwomRoK>qFMSDJUtm`_4HKX`_EqAxxnv*?d%&~4<(v5xsg;M`aUGpFxICw8J;#;Kk}xAd6Zl>erJ zJ>FJ;=`D`}mRl^?iH4)fmP+R4nfCWr+9l=me!uxB2T8%zrkR%Mm;b%0STZbc6WpL0 zk(}RWo-=_L{9TswC1(WV%Y~52&aU}fQ|EHy1%Atw$P@eVPv5t*T$)?BhJ1H>XFkqw z;;n`f{;c~D-+;vsL6`v_489LiME`|=1?EGw%Z1B@k9b4UWT#>#hp_kYCQNDp?O()>>O{%r}=kPgv$AfNj?ES58 z`4eR^jQ@GR-~)fyDOYllP+ zR)sABTULv1vstq~vt-d~`{$#2!v+_Y(ExzBn# z?S7p(gWhvPkqSLiiFHHvIY&>jKZd%N{$`)24p5^j1+&FVgx57GMO$GA5ZF1i0aqh* z5dxi;qC^1E0fbuVcAhEsrPzGMhw~C}-q+`@+La$w*60acFu_cEAB#{9i2VL^Jm9H;YdJai>?8!N&;TisJMe;uiu z9J@#MyGHh7GUtQm4T9$psKB)akQ$*vLm!AFUpwa}6$+qr*-w?Ea=xr^#M9l<`o_s5 zmM5vq5Puw0Zk-w85EIhXu;(JL1%8f#U84B048(rrUlx!%iC7Ck(Id4>|Lvjcu4_-N ztwgKKSo0KB$r41QTZnI9!lV0$OxBclRL`Ci^xXjXb2mV*WMp-&x?<`^+|KjjT1kOS z+b;?)3yliEyZ5+MEVd&Qo)vqL zAR137?%l!L_=yowkWF-jm6GEeYVO(G!r~M0OLX9PCGQeXF6d%M15J)jJ8`{EuV|6C zV{4nan1;TZwY3%?WcE4s^N68I(IurFq10(Joe5v^Phz?;h225lW_r)hO@ZkzbYo>4 zWcjt`(2VZzZVbxIO2N!UbV2eIT@zi)=BZ*Tx2R3g;qVzVVd8R+rfI3E#JZ)hjdG^+mAKfQ3t04=Eu(UnpE;V)b2GZo_mw%WihYstR$$&jRM=uvQF|1W{xg*& z!`o0%#A}Q2-n?BJqx!n)BqLvv6VewZ5j(Rq=F2y zt^z8+L9LHHM~yh1_V z4bwK>?s5qQ%vpGp=SQN*sQ%=8g8KXmIwmJj`R?45m@chnc5~ZN&vAPBZ8)lrrP_YP zZ1FLb9@P5xrebSvGVmcf&xi)?%#8|D2Fq`y)rkwc)D_|{KTk0^R~2-vCj`j&ZxixG z7S-RBpKP*P?2=p5=tS#WIhT3QV65+C%=%@`bVO_d3Zf!2dvcoGPH086%jYKyp$|qY z(^hR77Sw7{+k+wDI5wcUz&&T97c#rzSZP+QjiE*Rx)Rr+5j&f!@|+Rpg0UQCRquE* z>(rm!7#yK8yizYEq81P(er++>o{OchJF?LIXLngj&a%xfuN}j;8U0n&z6H{XL8hIME>KuMy&3h|}b6o;D1e390IflpV>OtjG>Z$q2 zf~saJ!)d-yp)QISWZJk#WqIcOhVn;&;=^i|{`0b=7R=*8M}7iCyc;7M_{^OD)O;0s zMTIvfHm{m)RN)_{KFPr{`Pt<0`>W@(HN$XkYz$~JYh6(xv>WGQWs8tPHy%H)Dtj8| zcOU(O&HseIX7Us;8FV|=jtHA(Ebk-U1=WdfEw_pU&f;C)ar_NLhu@`l-%&3ILSGaM zW?b9^!h`R0Ew2sAO&b@j^A^q8C#)(btV-FoLM!m~rPi}V8I@h%B^}SU%bn|x#xS2Y z#%2pap;RTE?|%eq4m=SF#u@h%tQ0DlBevnAD_ESNLK$YP-61MpoC}+(kfE?7|3Aq> z?rE({Zy8E$qwLPUd96KdX0K>^$Xln!8ec;bNJ~kBfP|u; zJ-3`*Blr$m&OZU><9ZPphhcpY%S$KcGzvuPxU)gi!p1J0lYi90q(Z1rC7w%Vs zS?-deR`5~ScKrOc2GsBq@h!u~ZF=Ki__y zyQcV48ZE*5{W{Vl6S5_?+HT-3keaLU<*L3j+-u@N7407Td@(rx^Y;)>phfys z2Y1~()#+EyLTR=T+5;sWcomEGXjGM)se5}gMf`&IY-))%wP&#Sr~bdV)m-SU=7=L2 zUn1iM3*?WEX@(>Xe102FV!+-{VOcN5>vlu#K;G^d3Ldh1D0 z-I7WwJV$D%`hEM=>*$>I`l$*4JE&BYtwgsQUGf) zB=0eKFQ9J8q_%9UCFrY;e8(psoKR}o+p57Y{FIc#E)?{7Kd_mx+V|aFBae~=Ug|eN zS}$$K>EPWHWv$hL%{3!UTq@g>x!r`Lz)1<x`Xm}L4Z;)8&9QKu ziuAdNo!FUNVM(I4i@`0;uN4Y?2axUwmWwEUlIu8zwrpUG-jDTNZ9NfA&+P3;?$p4{A}K-tbe>kE2LwXW z^mXS;v?{l=`Uc+4>_@~o4*^D)HYRIr*i_o{Q>{M zb4))At}GhoXs>GL;c-jqelkX(!eWb)!B5L%D|Fn_LBDm3J;se9++Ff>B{P-jPfHkn zqx{rQV#cxB=k?;9U|G!u2-Dd8xiP_$j`_s}^e7;Ii}o#2fbWFao}M3i183M#IjTqj ze@WKK&<)7X6_QgSYhat6yzot#ppX9WyU|u$XWaL@M2_yP z;voO$IS6I544tAT5276NW9m!OYylsE_zKi?#@4>5(a)2Xy3hgjt0Ye0m7pxJ!kGrT zw*pM-zMR1`?Lc4imWll8#qJ&U$*()j19IDYqN}r**acS^aB@7J>Pc7veFVMFT%hD;D_(t;{3yRtA1{pCMo-L=J)8G=6!6StHF+$!L$yy*T zEEFD8)mMg6pt}R=xw&VTB*y+2MloBV`?`*wF9(3CJy+c#YfFd??XP$+T{Xp;nqTzq z#M13G@GahQog8evojI+el5h48!wT2v7<(cQ^^t#gHLQQTKY*%~{8Qq+4F)UPZwEgh z^Xt{eg;MO$l&5X8?Mo+IuxRFFnQU(UVBgWdXtp(hAEM%8e3)~Wbv1$(vUY@po1fYG zfT!;Sm2fN*71{L%BPg~5Aa$l)oq!^$Lu#x0{K-K`&Uet6Tv@26@jVhIIb59o?;Zb? zZ1K{Hld!I4$^w^Ng8y*T1-XN8K-L1;JaUBxDup((LMGpR__`-r56FGz`mS#1)|LV) zHWaw&kxuYApd)WlY9*kdjqFA@i9JIlVT;kRzwo(%HVBrRGLjurPB@Y0Z-JIV8(Ni{ zOn6u4`9oLdp9PDvVl3}8{h4bS3pKKgjwIWgmcMabJ2uA7{A0{Hx`UokO~lK9$9?FQ5zbn@0b&pt@e5M&((X$d0ORGQhVSctox65sZbBR zJx^=u_b9@fKyHPydKH+_eoNrW+b{V$&!oPj-od97A}%~fDuBJ=pS)+>OaWjvyk)7{ z;jXyna%y=G)K;kb?o{eeE8cS}HEOnt)EeLD+mq0qAqsjmyz(~*QJ!Ub6W>ml_b!65 zk}?L}Q}1d;_DaF(aeNU;owJrbBPOh>YVd0O5r9JYv~_ML)*M&`+q13QyA1m@J_uU( z_o%Gvpu_VC(L=^GqP|RgV!MED>NM~&WaIiKmuG?xrawG$H{*7()v=hcM$&Uqx}?C` zfzlYve{nd0&3poX&CrL}-1>|#==Sl7oQAIbpyv&>!>mS1@W`KCL3ZbPZ8CsP4O%@U zk;^S?7ZLn{p2d2f&^ndKDz~bep=tNEy~eN(KLe#Wb#CbY5Iwlt7@BjClYpa~?3 z)8L1f8RzskmzrlC(~p^+Ywbfw((A)6Z0|l8Jgy>OJE!aSBuc$IoBl{wq6K+*Zg{#4lRL;=3gr?*D#{N>fEWtF51 z`Z}+w5pR{#k86rjeP3EKO(qEUMh(#NeD$8b<2&i6VOhLY8C}ZPDlESxh^Sm8Y^4}w zX_^v(4{!XhAEv0>sKu8#>n&S*nlQ0cAl}-%9-;0|uV3}6tRWG!6vlQQ^&DY0ApWrG z>lu(2X-*@3dywwGx4%}%BiNJiXjc0IO9f8tpX>8Zsakb{(vHpscO-<1x8X>JRr295 znU1kwc7V`SKN6ZVMDy*I6A=DQeaGT!wR5{u`TY+wuKFl1mEcrL6vUGCN{5^@)*3iB zp=p?Swc{PT>mm+;R1_oLa2IlvH`Op^Xj?9M(#Q_s5Br)Wuz)~UiDl@ z<-zs+ec2H2weTArkFTtPO=vD5w|I81*hap=^qn=aYOU0auY93Pj6kVzD_l-u z{Wn(u_x<>28@F%}*zTqyz)$LVKUR2QrV;tVn1!@D$qC9P`;nqX*FFF6XxTC}ax1N^oT*@rV^+#@_w)-A~V7JfwwUn5-l68)ECq1oj{1$iKqbfTD1KqsWxK zTUcC-=Ow<@$A-Vgv7w(rxA)!aMcFt2dVj3bra3I?VqD)8vNR} zZN6fi?H?UbDfHLJ(!FmHn(7}PevO0mg7}*x`-G{+o3hE(v4v(ff&%I?fW3+qNdj)X zs{JT2l5itDNg&AI`+@4srdGwiOqlO0GQmn6c9ACBcAO>=$!hFzDL zP)@szb%8zBFatbGpCfi~?(Z4^Pw=8I9=MZE&LNJee!fQV(Kxg3dHXySNkNMxjT-7m zbViTKa-uDzSgirh?9rbw@7(GR^3cIyWi7)WzS1p?B&CK`vf)Vc`_DUc=-|^)4}Qmy z`<)BB@%mqQx@HTe6Ppn;G28Eegk9NVlS`BL3=NQhBnlF=k9VpXzIa&$x&R6Y-VOo6 zJ`ke!gL+~=+o@eXR7*-QRcd5Pck`TJD0Z4S2@h5vv0lDp%3!YZ-<%s{P$%75-@Lv| zTU2y9eOr#eeE!bDFPlUX@XiMssBKByZ7=T@9?GpE#)nAT-&;Xq-M_6i!ko6;*G3>Y zuv;|us-Tp&rKp!P&JEsE=z0uS6PyH1{)pdTMV4JWSJyxT*_Gg;J%QGb$LZMyFO`C? z(d=@$6I|%!>tod{2uaJ6YTJx!yLG1&IF&9%i$BAcsqW(Sf`Xo5iJx2yIP z=k5hNMF@FQhJVig&+_lveE#(14!mk=G9iy(HsjiGVxxB?4wU?QRc99Z4PSE7Jm1G0 z1%6Wh!^i^e?PcE(cNQ*(-xMav*MoV{;8@gLWDmyxjPCq<mwA#;Df*Y%lx7v2Oqch2}yP_wdKQX4Tuw`ttV5*^U^`^f*ZbrL-}R?A>1_{G!n z(!L|bZ6p{hc=1VjDjTPHaHwjz=GqWjy$6#L%WC z*&a7~jKJ;EL}rIWX_VXsXh#`mQR&>5p-a^3r9R5T;XgRvC6QgXa))7X&cEKS zd~B<(Uk+;L1Xgpvw;F|505ItIfZ=SKB^B-sZq@3$(^A^wb7dIloGZL(d&8#>I47n?WP9SoHL|gcUuO@-vinj_a5w!0-mSq6gyBb*USK%`@iA1r)_rb=ZZCW&q$(c z(dw9um^Yl1WmZ^lg7LQW=s-ZnGzPS9kiXsn?*~ezPAM|M-3fIsQ544pLlK{Ce=atF z#-mN(JkP$_+Sje|#qIP@*+?_1OFyG8m)GR}uTiinK}P{D;J}5zvW%s(-ukpZsr_f8 zH{2M1lHNJf>gbTNXImxz*dO=sH>xJvL1CoqE~M+C<~oQ~a$j!w{uTbS54wz^L@QWo z-m_y7C%+ku+4~pa*jHs*ypsSCR&y`|ZtS_wwL|NMGaPyn2n{idy=PsX6xnwBGGgCNE{e||Pbhvqt6eppW zA>i?^-c60UAB<)V~ zfgSbGCu`O{R2Aw!#+~1C_PJ?A>Q{$AqjENK-{E2Z@f^=ib(+3H4e${n4vocpbM-2o z!O*@%*?(?>%JqxC&nIz7!fE2?o$?!2W~BgnVIiknFz<}6p!;*0T`ha>VJ_A% zCZNlQN&oG1Xg*<5BQEd8Osml?8^Omh9D22-TQ=07ZwLzD?$}z&t6q(Uwkqp)B zB70hVdc{Pji^z1&DGsx^Yt1<2;dtx2XMtGz$aQ4J#r>~n#+NSEWC+I=*{Til+myoG zMeOw7!N)`%vvjWilyAeG(0G6~hgmlDPpLU>66IFOK4vz02&OcUq83s_R-*hiG9b!u z-g-s-mNSdI*w1y3n$0VpLBbGU zTx3n!T&alSn(g0_bP}erX<`yaI|_w7#5buGta8Q0^lOr)PJ@HBg-94c)sMjZ)GlIq z5S%{-1?>2cpX_`23je&8|6Q)QcNhy6`+XUvI3VFpSsox_HF}jddNG;;eM-MXH?OtNgau7to-zx1>FFoJ$ah%S+y!UBKyT6$aLjfJl3I_hXFZ=-i;(uer#K zR>OE!o4Lx2YJjvO9DDE_ikN6AL)(F!=U7~B+Sw}gs{sHK`{>7%PBTDf2a^gZ97miI ztkhh!MWwN57MKq6K=EXPj2EYFG*!$x)|oU)u9SK;u;CQS^C>h`=dS72mD*%RYy(w` z*zlS=_fR#+0X-y8FDu-LDA3HxgpA1pnc%a=N zkqcUDI+>4_rK$zYhq%iRdi7oL@&`)k<+5}u%rw4GpUU0cnbk9HkBUR>uuKGiP>rf7 z5I)iMzze)2A#2QrWgW^W*FpC+N_q)DHc;3xg}6qTqtr~0H-ZQM@)a@Tk={2+RrJWH z?sJhfs?@qq39Cj_FDI>4Cn_yYAc=u*-9OwzHF_2BhGG@HtLZgRww!`R+Cb3KJf|3PHHLLT`jcq=v z*0ccvtK2N{|3aH7lnPz!ilC4v=G`H13rzRw3LtF4&#?!apJ zpeX~_0lpDUMIJL;;`?ukI-1g%gKD=p*>w53N!W0pJ&I(W_+Len44}x^+c!}C(aAlaLC!C`18D$%rd0h= zY8|;+yjrtavi3n=;AW_T4OufwI+TQp)ja2o4c^{245LXKrl|4LR>m%(1{qSmxc9}y z`9reP&72fm2cMrVepb`^4O2z83WbyWN5EuaGF%B!MKZK~sXrsZIGL;aazAC1ySyP2 z9R=XpaWeJ1w+NI5I8Xg zsBw+dA<+N)A8}jN4)TX-_ENgG584TTa%|&8oWhNGntOihb;)-NX+19T7qhe-MLSlP+ z^9CtD2RPLxgVOlxZ5x$Fs%zW(0sUPw!{$)_{#t(L3G;J*z_Db3&#Wc41HL8nIy-_V zW4l-0Uaj(#4Aq)NR_)nuEQ)+_E$Bmmq37) zX${qObi<*K>A#6}3}>t?po#zD{6a2}Isx3SC?qw?XFAANaQ$U|O7O%>oZA+d9bhrb zQio6Eu_*5w5}Xk|rTWandC7ON8n7B_EAVkBo<@^OjkE>;Ex|{se#)6XZy??QLyCzZ zfDG9R-^f^Akqg^-cbvBn>X`*miB`f4B5+ZQxXU5RWtBQ(wy>HYrww~&%x~xK+b{TM7k2nx#p)E~o%<@fvxwVK{ zY%Zs)Qmd>%Ym^+sZfCJGv~?0MD6RdgKGL&|edYINbT*bYz(#H#m0gjzk7rCUN*s+0 zz@LyQRq3NX>ut4{xbFQy2N8Z*0_*2*0cxtkU*o~GS9&&iirv5@8S**XJ};Bb=|E$_ z?rQLWqGP$YP>Ogk-cGfIYx|gAoCkSDw!J)HQE*W8zPyoj`L=gBHER2B^H7l7 z8dIneAlB*6Yr6a=C}UDVgbli~-h;Th>dG#=AAVr1?FdzNV(soEj|F919+d_Qe^Yul z<0)l(!5J6m3Q@heumpVNKjyC57VyoVB7aYL-x}X@?l<@gGRNRZS@3@8cN}sJHDGY* zoxg$wkt$u%iDeL;8H9$gR7EL4D-NcY-^V^+2$PWfR11#~u@S`=a4=KnK}e?~k!MTL z|NHNdV#<%3wW1GF;+SA`B=v)o8#7Qr9PV-CFUonZ`}_>Y63ZD?wfC&1MPpa}$S&k8 zKAGwBt-`^T3-^)g-F4oV6$RunRNuIzNsr@bn*|kj(MOJ{_mtIrls<_i%(3yRAD>30aW7Qt@W|VB(r9Oqy8D&UNW8vVl?YJ?=V5RUlOI z|MBv-0{>{6-{*|>e`bn_@~cuF-Ddzi*N=N47|&=S5F-kyK(^3DuFQA2MG2_J{x2RJ zS$-yWvAWI|QrmO8M%j!D~PvfaY1v_n`Rx@U))KneG&P2>mf9LmxKN6@*JNX-`@ zsKvXzM`H}e*iwy3Rz9O(YVSEm6C3)*6|&K9Kc6;$1Q4UjzaWWz#M-MSR`m7gB?K+fC>v zYOrU%KG=;sy{$-+m&}s}ZkI%pajgNX(?^mv6Z3qq8@O%2vZuA}bTKDxBUpb}e>8ja zktXa@)i%%ft+AP>$^gA>;hlRpv>< z&pD&OZrqFjcg(jmW;imGJaf7Ed6*xH(mq;;@u)Ctxrv1ksHVvZJnz(Oai}q<0Z#pY zFH2{b=FD8ANQ8!s-n-3zA9RKTz$eXLRa2Hgl zD+T8YeVWDyMGV_}bwnx*m}@+DtObC`XfZVA4j4HPN@9I9eD`-|yyVyX5yXJ%VriQK!m)FeBojMO z?vK6RckI>;^l@_Svxo+prlyWMU#`qD;G+|*S2jv_YgV$+D=N6W?Dr+w(osDYc0=~O<7W9h;h(^3ZD;a zR*K~6NEGm%m?I0$1mWc_eWi$+x04^lgzvBXZv)h8c`)~$d1bTN+URF|mht=)`PS7q zuX4~Ebn>EYCy&?z!1@drAj-bd>mIHl2HK9)CA%fhP;A=|e^^1lpP7Hmh?Gzjrl1U( z;`*TpY5~dV>%1aqdfPjsBLB$g0D)C5;ZL%8UCm-q8)VaMUPG8NsCFa7E zVNW79F|^MG{{7PVBhxZ=`{A&Ucr`$``D2rQxF{-2kNh8?OD(`|C3IHcar^W!ZYu)7 zSUFFqSq*&?rUo9&e&>wd$~+wB{;J^<;v?2zp&Qb7>gp#qha>xtlQXeAhd? z6Bx?t|EuJX-{kocX|RPgT7w&1f45S z9MqzAS7(<5)-;S|?4#_j&l`Wnq(;6*p|TyVjueqF4Na9BXdeG7P&4ioNf_-rZ*|XB zT;yoy{Lv&ESWo87-UWi7wGqk=XcvW+YR@UFZRHJwjwI{;q?q!;1Vn}em{TyogsMDmyM}zI0#}#`fr7#%^K_w zLF8#C;0u6VP!jz^lqC@+zd5m=Jq}T%;If0Yx9&zV3Plk>e7^WDq3`>fS=S0xHX0<= zBWdsoc``)IF{CxhB^3Iw1>xE_rq7K{e4JoSFlO)Hy*=}S;;wX&G-R6Hue|5DBRPm z3NJ*`ip@9Lnf-hQ`7d%1Z15KzdJe1)RdXtZe;}S~&08?)@A4_*GPm(nf+0aWZ+K9CaB)|oEv zJcDgg@;r}3soGPhNe&yWk786Mz<1G+KwAbw(zPv4S{aSZ9z|DSmBQfGb?Mo9u zwNlTM#&={%s?nTW<-_|d|AOuH2CV8AJ8|7IPCVaQhbd>oM9d_(RX1X0bi^bE@_aKP z-&c#Usp%hEMUN&ni{zZ6+V1AW5$uKh&09 zHo_mieZxtEo{_3-xjIw;Xio>zKY#hrs!x5{Z2y@kayc@u>2=?ZRD+DIwaJmlOCg!3 zqrOjKRZuWr=lp|HS7fWi=R))z=6i8?MROl|-{N~gbH{Xdy>yT3bqe!sui$K2JAmbC z&os2Iu3}1$(iyX){2wvb?EqWJv?~X9mUI06$9&N}wKB2#o)x8z3)qsx#4=8v#n5Y% z)NZlj2kLIZ9y{!d{vQ|X2UU*X@Q$RO)6}ywK58+@@RVNjdi~VW(Hid3F~9E_(*1$0 zPq*GZRPPjq>pQ5vbjPRXb*3(Ie}b0odqt(F=hYtAt!D?@6irYE_7&CAvP?qsP8wbT zm?rxlreW7)5w@J86h1NQFSxdr=1DFH&+f{r*ihjz)Qyj^Z6@y&4LQ8Wz(FCm<}~+I z=;uB`{&{t0)$@)o%-+uW;}~4JWB9C-pGHQ=EPWzk zWMq`KHJpC5bbHgha)0c5pK+OSXL~Q|dpae4GB~Ao^6#(m!Mtb7S&D0|%PBSgq1fi= zkCMwB=;7tc5)94f*7crV&*z4JzV;$09{Tizt$RT39l{|H3=eWOQv?l1W?NR@9$OWZ zsUCAB%hpEIUozo`84CNbg}Oq)hxJ~oQRxqVf8iOXmwckvco#0Jv^T|yha+7S;iVmQ z+E?fz%g)AT2fOA=7xiw75%=o$z3_!y+=kxYHE)#M@1QkZ-jeg%E*iU=ME!!cN-5}8 zYJjv&POsSy#C=-(ZGG0~XzsL{uI}#JDo9Ie?RUknBIM7ke!}@Xm(`=4>)XWfcFq$v z?|)->%}09VrIEN7A}!1{hX4A24AqnLQbLwo;5HAlJc?diRQz`%{=={YxEDeDEj}x+ zLAYs`{wJkQxth6^$w3M)*aqqCpx%9t-u(fZZqsF|l{&wMwsz)%&5tcXX z+fI%N33G+kYz}La&eY;Jsau@QYj@e4pk{|Owfco5-Oyrr9WCJTupS?;-~=tMT@|CC{&n1%ea)gYG}K58|9$-)jbfgi9s!w7xP@ z*@yubs1ZSiLW{GHyJM|sYCV`*QmazZJhgsFZZf^*qz(Gh$)SX^SYC63=Hcl}*QLvD z8~jBNyAGuMd1>t;O?`o*#%6K3fU|ko_HIY^ZnMN^y+4r?;$Q;fUz*z$_VhOTBqXIA zFWi9%E+1gZGz)5;ah0K2#9L78dGN-&PakOfmrG z5Z`EOQW(~UltC%8kglN;He}P;rXX$gt*3zm5`>pA5JiFxpgW_-<7^+dthjLED zM0xlcMaBgpFf-Iext(qF4__VMs5khW0RSatXl)(8*BFoN3F|pm$kxuB*0<+ggvAPM)oFH76mP>^05L@V3LuXFhQ4I!p)0xyS7@IaXtbB(6_ccAmhvO!L+#Eq?c7=!Mta2CjCqdt$@O z(zsXkln9sq1zQl=yH=sSmL2>X;fLWjy;TAlSp$CsY$qcbR+NA}bxE&qel5Fh5kob= zmX`kY0vE`xOn=@+;`3zB?K_-ynR#8EIQm>3@GW`4zN6tDl$Dp?wpzd7L5&~fjN+bc z!f^+6a2M!+67=mY{8MTU9q&i6Y!-cbW@j0TT!3xKNmuqrAk`{MKQt=cS>^32gw#96 zKAW{$kVNrpsdBl?64Tce2rS^aILJ)>bz!sBeMvuZ5>QPHFLfp)4nk{~(I$b=q%moN z%3*R9QtlO+Q||6`Cy(UHl=JpI1|CRk<`@e030#;V~53;BlSmm#cJJFU#Y5fG_OhoGzP=Lo9Sj*Krof*8T zoTXBV1bv4OHjh8{)UH@d%LCYK@yma4 zH>M_SL+60}Hr`KORAWBj zcfpKSOtUvdMqyOBme&pp5&vX+mZsa=Hst40f?t}9UH;0f$b-~iAO`}j?ts~^Aa3Rg z(Hz>&a(=KGw-X=S$-uyhzygf)qonC}wZ;AHs5R`T`k`WxyG#$}NyGDXcXki&est)t-wmc6j3)RgzR2lvcRrP&n!bav zHeFq_QqMQ5o9RZmVfVJ8ht5v6XWgtHgR&7q*hY3LK|0NSgTlDAp7+dC#Y=z2{~9uX zS5nCYs__s1F~KZg>;$`~6vRD!_j57jzEA&#^EX&eo|U$sS)oJH{=4I@n;8_1XNid% zBKAJ-RxY|6_e%{I>3Z)rHUXArtb%y#&qLB`$3v&&_E(oH!OQfKSW&6xTjh2eC^S8?y0(8MXkVcc%o(@0PM%iBWk zw#*850Qbz{7$;v=y z_-UGni-XSFFrO@K9p%NPnYy~2LdJ}7Rc=Xih_I?^tl_UYVA$rV&sD$@?1-vF&wCJN zFt<#WBpfKwIcXD9v>*lpx7qbrCdO0oFA`RMm*7PgWGiNK*r?F@vIuG4=~AtWkYATB zyZ4cm{9gvNe`hhTrjbC1Q}a}z>ZM5ob$W$Z202wXa}0b~Ndh0xeX>=_rJYCK3zTfA zC5?FZqq1*u;jmJ*AETW$3T2espaJhmcJP?FaeMA0q?O~DgA?K_uIry zH_@X$Ng%722#Y9w^gy-JgF&Mb_Dc=j{^ucr6u*AO*ly}qw)Pem9-~hQ9Z8^VmmMgo zo=@wc&u#QnNz=+5te%a7Bi+It_o6P?uH?gBd>HqPEpp+7&KDYj^6yg$g@rnHXOkb( z8@xKRhnr2jPlYyRG=LAnaSZ5HC*etIzkQ@TF>S#kY|$GKX%RKIaI}80eJhX|w)O=U z3FNNz+tqlDw`L9P$LIF1vY{p0d)VfOsze(rl&QVQb%z;C%#{ae&XnneF>{kE=UuN( z-yXcHrU$bhkDJSj>`6H(eoQ%EQQNm0${o5uVarQXud!KJhjW_;P=@l0cwG9$Z*0VP zP|;_Xr}jA#teDH)aB&pk2G_|JoR;?JuU+#|H(rJ?0fgkR%)SJEGtpuxLG?v3nJ{pJ!Ova>gSBQCO^`2|~T^s&#%9!V#!H!Vwj$`)iKp z#Le!xe=j8M4-X?5w)kiNWo!x95Bc@`LR?3LUl?w{!)_jr=t&u)*{omuM55VWLCG)y{xPZ6#ogAuL+4>jfM?tru$bDjOtR=R4=G+iDY$P1PdQnLiVusPpq*-CprD-P{o` z`J6J~QqLPXJDYHqY#rqP8^|mGivP!TSPh#zX>nkKm1I@ zZm9a*aKZ!_Or!aaD24=TH8#}Gt)GAwJ;Qheb-K#EJ~EcgHN*8HKW~IlA zmWrji{4NLm;J|^xq~{(B@7>NO*=@w!hT;K}9yq0Zk*&f5_KzR3cw!{u8{le)-onS_R`Q|>)}emMw% zcbxw`e1B0kv=#wdLZ|kSI^hItp4UwOS5cxK-rL)0&X60uwGWxipsY7Te0!9Fwxs3X;|c$8VQfXzAN zhvOfhTD+YS+~fH6{#DLc0v@8fgC$8hBWdUwZ&rQ zy?Nm9Bs%HXil(yGA7+Y|2cd_TcQ;ca?w5X58zz$I0(gQfx~*<>3AH?5!xp&9)umZ3 zJX*t#R;pXqA|rCcHE^bMaJDMPGKqUYK0Cy<+fB;PHovp9GGFH*J}K}z^(LiWu?V`qdO9}3ft$X_Dy_dR# zuk#}eMLpP4+$Oh9;Ql6b1bNL^KsbGPcnBphxwD$zp^QAmd99pwU^;ZlJvzhw_aLqk zwQQ2#xP@T#`-1LMc_gJBBQ@$E$Z%=!tDLumEK2X!-G{omP-8O0fLqy1+|G6=fa-hz z8s!}RD--d(CI0Tj&9p^{)zISe@`aLVDqcjAs3)D>!lfZG-N>pa>!o$IT~VQmS9^Da zYOjk&EuC>TH93G0LNbcSgcR7dzeZ*gOC&F%>g>Pa|EiE%26pNFdwK0R^`#M7aRzkI z8NwobmZXT~;(K_8V_na&xMK*;i?rb!U68MQPe0xIW25xhs6StX$U$!(`G9jz1F$4n zR}}(FWv%#LK5O{uG8kkHu;lK1o^n&JZqtz3amy-5xqEQ8BF*vjjr#48y7~h zZTVpX_mHuVC<#xlI%D;9Y?{O`UU77TjAc398hj5wKq!dkQ{xvmTI*t0<1ZQ40loNC z(vYVH@V-YCj&2RO^Xre>h$%i|pHXspkiV4aNHJm>X7~hp$f~iy8|CL51D%>nQiK03 z4gl%1qmUA0D)T;H*ShrO^mIepTCU>d<{7JQ<8{JThWOe}Re>?>2pZ~y;mC?7w0k>k z^Alq(X_}7%*c<=8eNKpZcPH@1POb+`M>aGT|I|jrs}C@?hGw%8+L$`SANZq@VLci; zh8`#1Kuk?w;f~j)Y1lq^^j$RA$XY|Wr{G!lkmKHMlZmm<0QD8VM{Y7v?a z>QMq+Laj_0lo!1*}*j`6hj9n7X32NoMq`RPXL_%FT&q+t^L+zu(H*Xsu={gRk1&rl}v6=qDADl=dK5 zW$0`ikS^I>bk-E=)xSe0F9X}jU;D}Y&vi$FMsCFpM}RMft2jq%Wkzca41q62`?*rD zl2Yz5fPVV7ALHTvY9{N)<^YbY{#r|7`f}eS6sS&F{Zw<|HN^dlB_>^La~BZofCKls z>)g_=cZrXjbyjs~cTWBLN))%lAnT1qHbBDJ8aEW#Pd3d)v5rv zI{}e}rpWrJvKu!-^X5h}!~|v1a!uMl_{)0cx&0hglu$dXJM5w`ZCq!+{CtM$^`Yoy z-@YeXvGunNL1qRv2Axy@vk)J_l-wT=5i%95Rx1nka05TF{iWIV7HMjjvRbUY<*Xty zcJ8|X$o`eQjAzNO3Z6_TD@L!?mL&QqVbWdfsl$BITfdv`nu?Dl>I&9j8E#s1QFA>6 z%xC#v9bl=_=3~!I9CD$|bE}#;?OH%Y&W*XehSoIPtq7<49tRAvQXpGSsZwrCTwZ1! zocX-@>z_pIw%8VdINT=DzvOA{=hH^EA$X@NY(Px=tTHj7*5eSLKci{Auc*FK2+Mgh z`yIRo_29i6gk3cR01-A4qVfJX9)BpOnn!&!P4Eh$@K?2zXg8U*EJIh~>1Lm3?6~11{=B*?wsgaXk#ODf(3MHh}>c zSarIf+n1n|KTEwqFRsymq;0yV>Zt0617o2pfDjVY4oqQ!#mSBs@B6%kCd*(0tgf0x zK&cn1NPRj8_Z9%NZaW=c^Yi!Q&Fw`VR}?L3ZLM`277fhgM*^J<26{f_m2q@C@vdHz zH}9*yK{dZr_?)6nQHyN=VhG?VpDJgAx@bd(r-zvSV_jc+i8W0zDuP%)c)jL190?yO z&ZI)BMS&{kYUfL8zqs{2GYd!}XJ$}(Pb5vRE9%*n2$(cQed*-iso59Z`uEg`NIN4g zDSoJzcNa8&K6Fh!FNc;)7Z8+Lq)?09zet2aXb)+wn^wT#Sv}^Q?Bx#osOo^@?gTOw z>NZP^{#^#@=?N6zk`AWFc_T`7P0HRFaH5~MidWQH-t~x!qGR!8+zHs{lsi`#?4?rE zE!XY!PawoM0L@|8LQA5b+X2hi<>hWz%&>Qz*FiyG;OQ$)iFevB)u%cKwv$_p6$j2h zX4B9MqIOX#2ZN8M@u;r0vn!FmUV`e&TVdUR^VUy!-P9ktU+-~I>{<~Wh6x>FiG?;h z3>amv8@3)9;gP*CGQ*qMN!(M7UDs8A!{cBu<0+YdvRzncw}0$-;C+geAqoo!hB~t` zD5?H!2d-FnObn7c>NZKz^M;L@lQ(U{0#yvk9(Uc74Mte*%0eN3FtKUB1*g&xRVv98 zaEG(R{-k2IZsLEc(;M6v%Y5}z!qk^fO=kRrQlHS^bF{aT>dpG zE-X@7C&$Q(w%2Xs6lx`N2c>@Z-zX)GO6%L^CkKV16yjFhN6qRAL?6Xd8X^y8R!o|n zZBk9zEErjwKHCqLE*B}e(9!^Oe0#P>SWEOG07fGr!e#S0v#@x}i>LWJ{5V+#huvJx zvmI@du*;astrtLdxZ6ib9D2hZ>-Bsf?axIQ^K~pB7N6U^)BRKDT#Ap!sq^H9w|km& zw3g93qd0WHTWj?Qc9CDhK>eklY3Vm?vh|&znr#Wxro#S=1$e2=fd z+H#3Z7o8l7CTMi6voM~2li9x>vTp_-^{?}2_h_E2iTR3;h)%R~_tfc-lxUmmpX*H8 z%>Suxc#n>t*yjygF7gO%U&G@OLR7th#y)tFMD1w^uBDIZCm}SRaEad)T4S@UxRjt| zA6kyFL82!W-^pCV?9nO=ke0e@HP{&piQh5DZU|aY?9Th1eC5>jZtqvuJKgRzdObPy zh;eUe6g@;gS_)eWBOI~OZ=$F!GMWmKw7u0t8lLso_9ItOP1}Q1*0xqL0o}g8Sip0l z@EZ!Yu>TEV4Z=HoZ0w{$wGbrpcvKCg!5+WCF}c$!EbTEBf&@oC9?L>zxZ1u;IlSQ& zecjlGo4;Yt#`#`XJMNC`jzCHWnET$EmHZl)CyJ+H#@uS%=jF#KHWf?bK|c8Zc)IGS zsJgc+CEeZKEggb@bobB=(%{hD-6f5Rh%j__cdB$tOE*LFUG?{VYYnW$f5W-w?&mrC z?7g2!+i!PcO{N@;v4CBF!#dsYgWhzn>fk-Fy&D;{6JmtU>-&!)(MGz!(yS%V_-^^q4REz;)fIJL0 zDp2wMV$`|mcH17$%>6a z%`wv2WZN8(c9*toFmjI0eGH>Q#L+LnU>#8uR`gaKuguwa{i&9*&F>N2q~j@L5a`uL z{`6{GA3cm0&@Pvu&Z@S8k`aXLZ8JR=WSHxP=M@wC6T;{1FMB5~g(0J5Yt4f*gOdwQ z7eW2;$}uX%w^Xv9j)DD6^siehprtHMd}k)O`BNKr93t#m0F2`6|Lv{4xj-zI7VggV zkQ9rAX_>F?C1Hlpj94Ru!6H;<#ENB@SQNKtq=rb?) zBhag7dLMzny_|=4(9qqcUFBmH3astfwrAz-{@{uC%Hoc6^88z z8(2mz`{SRSrX79>NSa^gyOQ=cNUUKxbV4t=IgJB*Kdg1`3=?wBxA~;MKnwaQ{uw># zM+bC%$>y7MZAZ7xwu3I0Ts#PG)t-E7b476g^BiYh-;@!DhBgxg3y0Yv0UQ7=JH z4Cn|@`)H7GIYqV7)9T$EKLuL%1Of9HQ#2kNa zRImTp*E;k>=py45D7?sav74Sg@%LfYBL@85PCPLG zf(-T?g1iOJpulG}5&q(_>hZ#1Mdba8l0$32S9Kc{F{U!$A%Bq5#Kr0YCAdYK^OgDcJ3ROb ztO(eQS_A2T71C3C=Psu>kRVnS1M!Gh>C(s=DS;r(wbcp%>YRXHD>SLT6%*8QO?*^j zRnsO|#}##$|DV2#Y{QF1ew(7?a0Y@ks}cUydSI8DleQdC15E^djzio*2efCJo$BMX z+}F|7vNbX4Yke6w)ul`GHKI1ay0-%BehYE-Iy;&JATTpN-n~Y&5MBWSH=??{_t!%t)0jT!H)sjCHS)7hJ4+o15~)wt}+zGSg@sHk5v#1Gn{PdF*1X zw}Nf6`35~@ePs@k-H%$(8Ht^A^*VrUJrVQnBLtX1NatO$EN6agb+d#FuEMbm&nM-d z@qB5AV|m8!8$Q?K<^|tMpi-wITZz|#4<3IU`qubh7>p`9hB8#H_&RD1_;|`iY-30Wo z`!&Lv{5rIjMIgi$UkpV7F^h|ST|@74gjn6mWc*%!NuyE8DAoT_06D#i?IMM{+Ibou z^Rj8{PZYA>xL5!98I8d`&Nzh7&t!VGHbC=TE zH_@2AIz@}=f8%*UqFAtEeztGprlT`#aCG;OIUG&@5A-C*D2)+Io5*o0CqUoh@BM38 zUKQU8Q(insJrhXEQ0Q;rZ8589$q?YK*Hz`{?dO6S!jQQUPh#$L{Sj;B6#mdJvahNV zSU>>qE!zTbq0!3L@zkJb;3GO`z)gZIr%4siioh@hj|_Sm#J@KXJ?05kH&S^?2g^Jv zFC9B=XLf=Liu+*C_ugO~iI$uvAs$dsqoSs@>l#Y)8k1b^+peorLwQDo3d$)8FDwr6;I1pzgp+y6jOwM*wvyx; zP^@lw9!zKay>a2hOQv92=ugo%e>pHLLgf7Xi(e5n!*amCSMtc?(VO4Q*#PCMM zoTBdU9iv`qmm!oLU$GkAZqUzl_>AwjN8-gQhN(4%=X?FhG5x#JeGaaT`h5zt@R+g8 zF){8iWagjib$_lBt6j3zeBmdc<``(7KA`e!)PErAsD@FD)|$!EmD-&-VodbD2B^38 zyKDgRYCWv-L?@%VsAlq320Ji1(Y2qyUkuzr)wxYgzbk%kY)MaII~3=KaF;Ia&;PJA zBL?cAJDes z%5ZzdlT*F^DI+Ts!9%g;o4O63`^Fk5i*DB}`j2nrI!xdZw>mbyah!60Xonps7NflP z%p7iQ_FJ6t&mhe+%g@Gy&Bi))N%FRG!Ye0$ihVMPxYTS=k}HV0Vt)Pq*g4NtWZFGHcHgiTlKP6>HxF zY7f<3#C*|c2~ubxmi}RWHG+RE>fbX3umcZQjmysu#0PT~DdB2}4NK~~Gqx!Uk@?_d zsGq~{`=oaRPFY?8-{zTV({F>j`Yq2(2Zrl}Q$YfFon)mrBQRl3IqCP9^M1GN5GbW4 z8%Hp`sI<`r@Rp(cYxUot>kyiK0*LsSEcnfA1wd?I%Tr`QB*yMVb&=bH@_uv}{nelA z%j93b4w!le{hrVUve0FC`00^d@9K$=Ea7W*_QkeJd1-;mR>#m$pzZfLk1Aek-?_zH z1E1_!=v0;^3ru`T+hMJGeB}0f*4c6RfbZ-B*Dw+jtjCUImFtM#gPp5281FVFsQ9E4 zbG2-<5^@}GUnc4|_VcKc0WyUL8*8t{u zU!hL|bmHR&PEc!u{0oJB?X!ovrG4Uwb2G%LZ%$>H?W@2e@xZ^Wk8?;*4qgaoT`lRE zL3Rw^ETa+Jmm7>*6CFIL#0xg}PM)YgU|Ve-{dwf;R%uA@>}h9{SCS5J22!%RQnaE_e+SR2OjJlo z6SdNR&r)NhO10PHwIkH4uIV`sl`ZRh$WD(`Wy;8?2ANqg17GS){vQjHJ8P=!p@;+` zQ@d*TcDS!#BB5?Y? zp^M?$<3hA?6n ztyaV(Oi|_h)l5}vy%^#F60!$mTjACMyc672c0f5X7?CIC0ocAEiXuy;k6#2%5k&@`BT@FGx zyk<|0_GFvT>@zqt!v~3HqNxea+YrM%?dj$0GCP@^f_%%2CdF17RnWDipHF z`^eUKR^ov`tZj~x{Z#zm^l@b;Kyq0)SLtu+HL-YQZ>jeYo<(H8O_pUFKpV{{LW3otvL3h5(+3;ajXLW#8Js4_SaiEiF%KvQg-YvB#OK2F5m*# zmm&b0STwNRyM<6tB3wlR13^55ffEea?j{y4xvqsI5Zf$9T0f`G9fjpf?GE7Um~>bk zZO^$mFVl>I*<^$ObLwAmDGHoi*~0|~p-F#j-Ygq)QZ6Z|^6QSt-;*)=4K0U}_)M4k zs&l+(wMmlt5}d4kg(yW zh{MPyQ2*0hr&99WDiHU{m45X zeOeJ>T$Z@&;^m8Sg%Tb)sCV&A#?>HC=wM&!UI4GU%)`_>$lO1x9}yE2a2?X$iuta* zJhgsF_C)$WpWcgpdI#;;2IeB)q5~NG5yxH)V70X%Oj

vbIU-j5Nqo0b?)3cgRZ&WFKGU$E4xC-QOGIrb2W&Jjn9wP1%mC4Nt(tv z7WstPlUy&k&nm=!DOXOa=p~P-KE_#)eVAukwk$j>P%nVMt6Ac)8np;G*Ixc+j>V*$ zT4+;Xidl?KPgzBij8gn(=mN7Oh4~|9^uuVMkZ&K8ZkQ4_x}Dy93qB~;mZd!UmR;F( zb(U}|wIpUHmNUYw73~M0!xST>7Cbm2*lHK&i&!`JtbGC9QnRX!p;qwrXN0jt-;*1r zshL6N@8!m)G@P#p<~}rC|6+i41axmw8uLv+ls`JPDvH6G4HRz;pdiBPh%>AnU){Pl zPI~BJuh`)H4A@b-u5G}^2rpwmWhJ&`g|NN$9@+n}R(10WyMM#nAN>mxP$>V3r;xa% z@F5&~Y-ZX8T31zCA+2PfU3N*{=ke0F)j-S*#t@>qo*IxPxZ6gt@ksD zXNO-;yR+Etz?3Xd^QcC_pQZ2UJVR63iK3N>)|}s0 zarD0HS+>wi{BHZB9h|ehYf|O1OL5ynlgWwXJ%)8@l!S;s@VG>#g)o;A0lL2xoI z&2c%pSELuyObMm(i}({a!o}n~zv8c`V`Y9M*7@LjTfvLqRf(e)Pmy6Hh!pYom;m6F z8v`V;y1yfJN+3-VDyIjv*LRI`I=P?eC>jiCCh3b|1cS$huaXbT4`%5LpV<>AuQvZfH8Uj6UG3 z03Qvr>r@y~TM`#Zq^o!s$EWD>mDgiU;A~(d)x-t-_^ew;yd@OPHRVvMULZ2eR_>4e zD)hUfreU)r^w^((LhQ5eCWE&pj6m;aedKVg5C5{4cRFRjgKQfFuBwzzR({JAX|5}; zYTwW0ZWH-=41T9Onnw6yvz4z7QgI^ALN}5J&hgne_ZX1WeDK&Z5_hXFt(V(!ZjzL5 zv+1o&4y$U5gP4)|(F6^uJ%aZ)ggndmgP24@7a8NWv^|+fgsz(0nw3v$dm04n8c4Ds zlZ-+WEhy_>bR-_ePZ!hEE7__~e`7<|{04v4PBQDvo#CC;IOrVXOGd^1BPcGIDO|8N zjx*#+R7>yfi9v|%M3ib^83p-gmu+|R;*~nMIn^?xz>A)^*-1g4dhZ8rO1pBBr##_U zHcW(OLOkU2cb44P4xP!TmjrDVZws5>p_7>v2r_i}PajV=A07g!%AZOyFBuH!Qa;;(Ox z!KwQ4XG4O$Peodla=(eU?8${2Prf%pldj`)@K_<1AuVPAS`JzC@N zSkeixHauDHr!>+cjymprT+jF9c`?I+&13tOdNAcPxkWjSv-xfdUJYIQ?HEVtWD(|( z@yUGKJRTN#>sJxt6gb~K=U>fx5b>6~akZLW+eS1c)~|eON{2%=im{*D6Z3kryXqfv z#R}Z(Xl5U6!2d^d7e@g>JJoouig$S4s}q>O0MHY3CJ#h_-eg;lC}IxF9P=IT(rjNe z9fB<{0)}+eb=?(mj5aRT&Apd27OBv9@aip1JZ&mC_ZJ(umgx`57c%%UA3e}T1`Rcs zsN*TO=99sQbe?F0x!oZ_oy@Pkc|a(K%Nk>(kT4{rtr(&baYs?{W zm*dyu@*Xc6r+Amu3k8c4kTfV$i?-uUv!u=zUZR?`nVvzMe=m1Py=$}&ADs^1{2!B} z)czh4OEUM$8LYqs1i7gTdv7B`fV%;)Cyd|msUy^*k-RlW8W@I^HNT;=1j3P+{&pjVK^x^x)NZkvlp|-S*mqL{d_zi0YT%5y zGaj)(fUuFni=TQED?gFTo8nz#CF z?4HOxO~w_C1YG@x_QspRmz9aEK6!QHSlMW7p4S>1j>O&u(mu>Hv9GL)D^CULXFIrc zh;v6yt=C>5t;B8Ixwok1SDzk&7X_TL^^b&6S*6>7SY^Whu}Eoe-syk0gMsvEXrt;K z-*EBY17bsqCFso%eFFnllUUCI*~8WM0@p>j!2>1{p_!+7@9kgi$d-5jE0&Qg%@`Y< z^bi%rm*vquC3`BLfPH;$UQ}Ax(1%*n8;Ms`hE(t6Ug%+5E~aO?=M*_x<@mk(CmxyX z)Sj0Zxa$mfJ7G%FYj(CH(GekcbfR{*?qr45MBk1a--?*SG2EGz*#U62X5KuM?96nm$$HtOr1aKP~)n>&YAC=9R z045%Rr7}@HQ{aIsLNRi6hY#cmv#brtH(B!BP}dRRw*8q7;Y*W#OrqZDNuVvLtJ@5G zIvf$SfMg8qQ_%*862mxM*g3BbBZ+uj@t5` zbvat75WDJpMeKGps7xWUUXHrY)>Ei(xb3rtG5a>#vP13pyWn(LBLb&J+j9O12s}Yf zkSBw^hbTwt{*b|A``M=x%BwY@I&u-KPBb}qb7L$6M{|huNUcB2F7NKop)4IR??hc7 zOj|nktgh`!tL*LqSl{zFg`Mq}cDQa4aDMCy|EFA!)tGIEc8=@s7rrcUNokGY5pE|vF#5L|2`ABpmwBaj@E z%-if;fB}p_SvlysHg7gvw~kC_L@g5&aWM`*Z2#^gM)pfAHMf+w!}$F-vX@;{lO_7> z8m$CX*Sb_>-B-LZsQdG~`E(TVr#`fu6S975E`b`GeJF~kbri2Z->*R30G{(<#QG8Z ztu$L&{Sp}nuKJ@QLE;vYO!YBGw%4IsZX}PoO(t3yrop4f!?(1RJaU&byoRdM8n0uW zzp2)!a)I=o7jgg%1>5q`PkmOZ(6s$A$RmOt=U&j-e%lTg*S!1h@VHW@s?S}u4qq{f z^+Xy5yxaY$StVGyFWv-f3g;1W?zMTzAIv(|gooel+WD{Vp)-XwVf`(-t}sYguud{D za2uwWkUMvV+4X&AvLY*j&m)4Bn>Kz=xn#ZT#a0wU?9MV&7cNQ82#6-CM7T@N1w%8Q z8)Cz)RAP7U`35&7mQ3xNzb(B4>hOa@a<~Y&K4byA-R!rYu-|a+g5*p*W)8RP%1)C^ zZ*4%J+r08btL0&)rt2@6?Cc;n$(?D#MhgAZM1r9HxWha5n0w53ul?j)F9>c&6jF^c z_Zb0dIQL&bo*yn%n7*WoK-oSnqR1D!N)NLm@rmETTNYYsvP-%HfN^WvUB&O|k1O2i zwdlG#Mx!D6^C9KInWm_NCK~*!_kYf8DinEY^fi|5nfFm(@gHxlkQXFW94H-(-%X28 z&37)8u-hr-&boz(s+H}yIeUNt0IZ)vAOO%6Q&}-HzG@>+wX^-?(KfH|pq(?DTx>!u zFhOE_!b7301ZRnv`<;GfmbRd86Uf3u4g4`Z*{D3Agm& z0wAi-U|CfOKy53Q#@n9=Dt@}@kcHLSK5$W^=ed%BCRFgf4Kf_G@zWPdlA{>w*YoO) zPQRRxLg&AB1nM;A>Ib>F=gr32)OFs3JS<)b_quR>yVnQ@#2tbC(|-)&nSnh4dlnfk zkg7Bna0sQHXpZDB;SXFcd23|5NCTP43D9r7ELzD9m8u;}jA%5O0t;+r`xs@k?9sR= z@5J}cxW1x}6rbAM@#omG2oa9Y@O?sdy5<8Ix%zoe<8o0GV8~x1L1j-j`-W0uNTQoe z_ns5;fY)o8tqr(PlH zW-wETf4sUyJs0sD&iH_=moV-19~s;use=@ZRrA;&$zADFC;&Sf8t7B3<<#^fjcJNpx{8@=_Rm$vtW zD+|g4twamT(fdgtp^}G^=J7h=gD3APpZ#A)WV5=%$IC@t=V=?=a5dw~L*Q>LQR>cW z{~wou0C^BoYxh?&p5cA}MP__6+tPT3X@UhKt7yQeGazgeOcIHYW&ZiMbWD3pnG!r{ z16D+$Cun>j6OnN0?TX=Mhrl24IK_@mr)+I5pax^8^1oVuRaD;5MjBkZm=L|voaoEG zJ|M@g3i70wr&ilQq&_Yd%=YU$`=tFi;I0f@N70_FHC6Wdb{`9cL2~d++P*xlK=Bd_ zun0b!rk$o)=PLdu2QySS8v$YkO2r5i!zU*0If;?aN0=2Vdzag>8+iFPQrjbEHt_F* zkNSb>VB5p6vCx*fSyV}nA#c8oMXci2V#sIjG`vBZPGC8gDy-kTLaox>#`(BXnN0@! z78k`k!@=5t-@co^h=j*Svv4lXzanr)-!!kG5*#C})*vVIm49St+ zFpYhGLilF{NxHIYsy{GoUP40n{NRSTAh0{Z>_%;exOp2dBh^60L+R@@qUtStB-TYx zj>QiQga$%))B7oDoqYCcuVN{CK3Y^>$+^9gQTbi{@r!wZFMkdz(w$Rg-|et34&A72 zmJL}sBo5YHRoE-T)m7h!U{yFLtpvXDfO0IPqf0iT>Um&5LIDtZ_fP(Lvag-$i|-Pv z73zuR@hrLEYf$-CuDzkaI->*l1N$YzTdY> z4P;kHIcFkcKpVa!tdt!-wwsR zu`e2K^O+W`Jod1yoN)}brkrTaj*4crESE*LrX(TBQ2+k_hRgD8S}-Sn=pbLqtV-Z#(<1`>@X>8t{&r#UA_=~CAA71 z9g8^d9$U(ek5E-hLj%ohRJqLlehbAejV5l0WRkV9V*cAhStGy;r;pUYz1Jq1tJwianu%Fa-z=HWQFNkyP7-UmnpLhMB^Z`@u&~4+axvc!*l-ItnI11w&oB3kO-NQ} z<{9Ew<4HFIrk*0En9qjd@^68QvY@?W+Vmx(87I2=$X?qmZ*A{o?Z+}-Q^dK2qg=f{ zBKT0(e5#tT^stZC$cFkE=5g}M`tKhNWbw^@JAJg@*l($#SdVFL$qvxXrJ_*E%s@|Y zwzLX`GGr|i7YZ5({#duVdA-e*3!=3bYWyHgM)PJyXmt?TxiFK~zB+4E^L)7E|V!&rEYK;E4Ks4K?~_6I@)0U9Y#U)RAc(TNGXz zJ$-&TOBjH*7Z>`82u1AUFF4q>G_Z8>!#8^deI}GI6N&}GT*^q{!%1iL8Z5>uIhJRj39T9^F8D4F%SuTlC0|U44e>F*=H1P4J&Plq z__~`=BsA9B3ZF=6F*~b)sFU*N36G>p{M~cgBNj?Q+%K^s6-$KA88Tb%8ww!Nz-A$*tWP!IE!dtacTon329p*I~y6b$4B`0DAOc}RU zZ_;}r4OG}%p*4?-rmqtR4K4ilx`f!hFS1A?$JxD3UtIhWv$o(tcT>fw(m8RJR_DOS zuL2%^n!T{f80nilMwi#cw|n}C6?4$&4m1-RbX)Jx^Bf%=RrBkRXq*s*_>VYdI5{Y9 z7_QzKwf?&HnZUl%6xqT$gH%{wwmbzYS5#!UM_9mEu+o2ZQhnubk*o3gVjoAD&3{*W z7(t|F(Xp~*sYc>Wx!Ok^Hk?QVvK>pA*Qt|MeD6$sJICv7LtnN*jYt+wo{}2|#=?S3 zG9w2Qxz_wby-#0QTmO6|iSG6v^jN(M#l>#(6tt=4rr;>UZ_$_|R*`3K@3bFAOn@ex5(7*ZRO!Cyc<5oR_&V(c?*-LvlJ*kbffv2Uue z7FHq{Rew!5X6B4{gnf_J1n|i4?~8r=?xmV?0?i5OxbGZReH*yx@Pu(xj=|Wd*jYPA z6h5>GLc|Npl(-aqv8%nku=>$kJCC;GeGBBbh>g-K#0>0ix`dA@SUR|I%AvvW+_d9e zcH=!HFLQyLKm9zP^a!?^$vFw*jAc#-0aX1d8$z5hlR%Bge|?c|S~s8C(Lgf)h@R*CWS z;~lR|CAlOPIrM*|@IQ;kjX?rtk@GI9_PstbxX`(v^$q+AeE|y|G2*pjoc@SW9Q)ob z-trmtbZTm7_T_R5vOx})P;!j!?EY@$XQmG!D4;)(=g~$dxGSF$#uOvBLBtiOs#IIm zG;%jUu_SQpdT`9R9Qc5rD0WY-D)#&uQ~Z{_@g#6$cGXBnz$4Sd(9F*qg^Hc0OuJe> zrKPrF9PR4c znQy%N^0l$u2GqheWRDt~k5eelovas6df}S_-12@(mmA738O4jP$itLu)W|j(?4W? z9iCg~M1ux5Frc8Ms$5;w1cYY@FNp}uw_YVJLB+SbQ!BmCZ%=Ly51t2f*B(4O9#)=z z#jZV(s&?GaLMa3L%Y6%Kd=Fr9L^L=L1+T(61!+w*Of<6vbO@PkgT`qpv8%y4~S3-9R`Lv7#S|P3GrMnqlf?TH5lo)(ER;T69Os^O%E;^dPzrh|fl0*hp(;C?%xL zSAS;(YISX*Q_`jwl|U{}CzvxX$CO3j(%o&hi(yE)zLYj`Po*-F{$32h;@eY?Q}TMN&MXr3Z7;4eXeot85$)+`}-(> zU3XK!B(Ptg^Pbk^aR*cM>5!B{^p+h;>EGWjYw{ktzPsG~Oze3aIIwbG06tkK6!m)G zOBB0hPZWQQ0G1D~AmECT!v7AvRe+L9q%(>eMm{&9%fOxoW}~LlF}*HK&2Xoq1?B$B zC8NwsB3v9Vq|fl0vE0!ag-n#lu!4ziDRi&qOKF80v{nbnIuisRst2A%QSQaH1ikc% z9vEm!zN8YvBOsw-g?-S}$&~#*!s>HXYb=Zm%)Ks)0sBQ+01bimF3E*h6tVG3T~3>y zTne`pGf(m?Wj=5Lc4VFH@ty9d3k$(T^hA{6$nJ9;2I^_3Z~qkBv;$uL5emFsE=wP~ znDKFUG4J!Jz{d~b=Yfv}&y3Fp&o`Yny`2x9`F_{vjmQ226XTH%4l6(+QtWNN_70&A z$WYJ5pU2&H#s5YqQS6c2811-hrdgl6}x{xby{m5yu0RzWwjrh;N%i|tz4o-gC-TyrN)GpnVD_rWR|== zTk`c%LY?J8aXrnaUY38OqqXKD(kiKi@KZ_&{SultNcRI%;y)TWVWcF9BCq zS&WAoP5*5gX@oC?Y1a8HJ#pu!Qw0ketQ3eVtY0g)o=uI8huJd5oMb*!%yWutDE$;d#*tK9zddg#gcEFW|ALPAuVZiNzDS@3TWY-XU74*@4EWD!%mZW|1|2U5Mn zp9cdsbJhaJL+?Eo=0Rp2{HA_n3iM0_Q^b|1W_QxISeESfzST(_nk0%#H}GSe6II|% z3}E9#a0XmB`X-+<^eD?N=8E+LlU>34>@Xx1ut!~w1F z$vZ5qEgVOFUL+W0yvqtI31hx(S51Amy5#APnW4JctNwvwOMw)%&t;Og%1RopjLo3# zerv0OrJ#{p#e;ppk0kJ9FJU5sKUpj7tCUs>6tglX#>&5MP;Y9f*m_FVLGnSw3Cbzx zUugyWB}kpk$zt!C4li_plhnWS)LWN{_4Z;lrm8Bg!J{RC+an+YtupWquJayHlec0qIPZ1Rwve%Q%M&`l^x*dU3L`bDZA%kP3JncfPqh;B5BY z<*%&;_Kc-!(Ly{ZkmklOt(@_XIewUfqtttMU`lS*Rqy{6kXS~c>&Np3&EFN zQPgQc-^1ZoP1}SHoLcrJr2{#cItgG#kq1x)^4dS3nJUdD5@T^T&EtMxuMZ77UyAxu zngVrp9;YlYjJ3YC#a7MKxrw0t6k7}{^t9UM_QGKCkigjd7SxLsiC|ZRqrO)cHZUnHcNgV&V&M8sfFlhTMry zegNDYIL>^jlIDKOkNr}xaR8nO1s(@SP%23Aa@AKodUQ?e2 zKf7XEUDB33D(L={TB;`ok<8>x(mzZ~q>*7-*HIi|! zlhLGZ62Gpq^5>j0Thw4!td^vnaV`JW&I1)TlQtK_tDpVnXG z-b2;LfGtn9?hpd)(6c2uyDhOX)qBe$Sg}nT#Z4`PCb7Y-4(kf0I^|xf zp$a~65hArfl9W>8Dv6>urIkJ6am(n=x_Eahh0GJVHnv@zid8t*@@XOtA zp{pVRly98bMJq`m32>z^M_{z1q;N>+xT3fn!z<0|+P1xR**9BR?st`0bQWOgsQdI{ zYpi?Sy1B07$U)YtbZi|_pn&u$wLEhlhxDIYG!maO11Ju5t3ERko?m|Z!&Huxizv?(#K^9o*n{<&nl9Y`KnTq3^r720q=Ef-{AN-0R^Za!l}dALRM-Vx|c ze^LeieiW{HxzK2KTwq_D`1AbX`|tcb;i*RTvLQ=Lbyy_tc0hOJgwS0rkdn^cS&;4NR~ZmRZ2syQ5tsy7a9^O3`uwx>+4M~=^+Y9hw0k6-uGKQk4|{_O`@g}ye@$xGUn^^NEFKI}j^Z!S}1 z**Mthj8^qk*=w|D70rz@5c#}>KJhAR9_I`i!^Ej}?eFk=7WfSpuRVwq_&stiH$lU0 zi}*sSf@-;E7u~pCZ6PlD45UXgp<`sEJ25lU z)`qJ@hgC$AaZ{`3YO~1Ps(a_WioA-#|GsZKYy>E0x>l?HzCc+gJA2a08d)NdpW$aA zfaA%|LYbWE-`?XGz7{#l?-kr}tRIme!}k4fO(X&${V8!zgkmgK=j_P(xaIvba0)TXW>emheqpz@;~!FhA;-ft zV>?%x7M;SMRMO5A!68`fJ?E^t8>zlsuDLMki4O!hJ&(KIQ|9Nap)84aEl$EHB1&6t z;#gS2TEM2yXlhCHbiFPL#bCw%-`eR)XZzFR#Ukuj+vu4XSlRf-#$Rd2^h++C zOT5@6Q+J5|41bVK2I?gVBXUWJ0YxiQ<_|KUu$8e@HG4*iBzA0_=|1-qRz8L2EhT2= z=Jx}3hw_+O4=)}(>$P(QsV-rf=tLKe5o1gh8NtFYx-XQ9w~2q8hd!)+OQb$Ah@r-k z`g_8^NzHo9wxMmitNBvuq_FvULU?D)A79bVKN;qjo;E%U<$xxwX*wRCoKZ`xkE}gm zK2UFc#WS+)aTjq_T;$_JLrm;zhA^riE_hsw6}+Vng6V@zI&1Cl>qEY)B7>hi`;^--_bLSqsAI!kE`5(v@^YJ^aAj!&y3RZ0;ij<<}yVBFP5Jem;LPKy@{)C z!ND_)2pQsdAVH)|5iTW4;;#v2pY4be(WSY=t`=Tx(%Pka(*1y0rT@zt{5jK%RH9>d z3q?ZnOG@NDJR6$>p%^(i2TDqDp5&5(oST*JfH`Z)j2F0? zQ)P~8TTeD?rvRxZg4y{1mm_e@&Bvn(V&a8o#1>D}ptOK}b1K%oh{9h&HDL|mHyVKh zMyx3o1Rog5x)^D!Z9i-WuLM52MQa>uKspo(@08k3)O9Vn;KPVeh$v0VOmQO5Y{*%q zSfQlmJm%T6OL&X^*c&Ao^w)t###(xxzZ?@f>~r-s%JWR}Xudrd&v!p&;L4;rGDt1m zyixAEFCJ`rS`u@9s-EnQ-JFhhKdhD~Vb>Mt?!=t5S>IH;`-~^QNn5XqpMxRzyv3cNfWvR8yqT9 z%awdDm7=KSY^dUDP)A!*)ySlqJ;;4#s~5HUdbYLtz*{54R7hwN)cDnHiqd8K5$lt|1>`l)8%gI&uNtho-+<%{+Us00BbWIw5|5qA?p7#Xh;dEFNGA*q zMk`9wwars2QmB`ioUy6e=W~CbB`vOn?Ca?We$r}C>P*zTDH+(0CCQmI_sG}3&iD2x zD5eiu;xxOE?Hy2sIp~Bhdo&06qboX_J#}#zA!o{=;4zz{Nc&X>v6DDpJ5ZzhdS*rK z!0h6dm8Z~I~==Y=*F)y1l)BejZe@R*4BWk?FOF z(D8FAnO=GAlAmIJTu)c$s{|J}&-hNc2r!|1?dDMRId)04!`#d&^yj=F15)k!Fc&$o zw~_WMy2g=89+%ezg?3dJ3*W!By-712;ZrKg(0?GgOD8Ucv!|mzj?eid#`wsG!mO*S zTdd+91T85M8YT;W|Jf;5c-N6Z;N2s*)a0EI;?HI8FVN_lpeBH%uvqo??Bfjd9t3_y z?@ut9Iw5BE9p%Ih^BdZ&odTRo59X~LvoEPdPWlv72E{WJ`}1{AR2+;>WlSp1VtfLFv*o^ksSPmiZg?*H zT?*OzHfCj+d{$%I$m?ftB{&uP&7}ZP;L14xaDMHT&v(C>5q zayDXXXn!tvrY)Ct+$9E9N{cNiEhZ`bvq^EIKhZ|knOdAEu5890CNlU0s6j$HCcgZE zA>)*qo--4eJ;&h5Ri4TC;vV3044!{b2L;$?ieO|_(Z_TXeHDx#Wlf1gpre;V31LOz z$IHcTv!%tdDPCpB(7$~>b8f$}XeN9QY{LG=00s zZ~`(QRa?i2>_AZcKJw=da+PBM`zYU;-lME7EWw;W^X(Rx!vEq#^ybcFzU3lnq2H=U zlEu7e{rC}CmEoQr^cIV7GX;gCgo?vCj%qzHi;^22-*JI((=lk7D+aS*hz{+32d<)couW*og1Ze_CV7-4%=urVCs#GD>kz z&^w)4SfWkrtr>ki-)1-^%>=uO%yHHv7d0w*TfW_Uf#UoW&~?z+zS}#6N!D3nT3A=c zHFW0$o_ z*xQK$?ch6XM_cgIwA?n03qL2~nW0HlAM&zf>MzlR{G!5}x_Ri!`&r-pSs ziYg4(f~94Zv&6*aS^%qCwrpk$NTe*j)4u{mdxzzv+-{( z)D`7zNC}%%!Cmz=JWNaUrav5yhY%1TWwLqKUr&LX`DoDkvSPmioO4ku5w*nu`~cbo z-=-F&J4oHvg~|ihB$<_?tk4W;a*;VV^zu61D8QFf9nM$*%{zvd&f-HG*C*0rWR=I0 zj8R-@_K{dGD`V(BXfzzCer0q_EXi_*XXn9=3sldWc+}3P*MQ*k;gqfU(ledi^5Gvr z_crxQm!^vYMnq06DoP(g@z5xpwI3Bz)udzDr>1?6SvF9v+MD(+>H8>arLz0N z=%M>-!)gZPoRBW>_U{*W!tOVkrfX|4K4C>gpaD%m{OnB#z`rtt&-f+|Q|O;r^g|XF zumxUzvE%~=d?4TY`JK9TTEysw5xZ61X8h_Zzd@$@BoWlb;uG4q@Cndlt>X;}C(!hF z)x3`60=VU3u7vC~6Zt0QQu%WC82pJRA&xyoXEeHfxPhIU()T%9@JV^3>PB<2SsgTJ zM0>n4P~(=kyZ@;~7yZ$Z9YtH;NG>=(_Mu7 z7O}(SZo=3T0;sv{9$N^{Tx1bVtg^F&>>6-aan-`B9fBs9J!Y^fnO(nVJyRK zLLfnuxx&m}UI|HhEx*jpDkBZfpbx`-;u{ryz>|bAsc9bEamhL%qKS zp>=e!`8=N!!hv}6TIPI)r{f78xpca2OE!}v?vv$XZLj6B%37-X4IarP)p`8amoPRNTA3h+NFxc$oj1XPDP>Dh4Ng zbm)>RTbF#dq#!R8cP#|7Cf}l+?-n?hI|k*PO1g;q2|5HC3yxBjNHbEwf*OmPN>0Cw zwO^&b&-KXXmsjWn*csX2Tm9e=j8B$@X=P3d)*V*a>bgDAdEK1c)U)77WHL1~DWu~M zhi4h!8pnCOwD@lNcI5NaZ!ScPqgeNGUsh{X5l!hVPNCb>Pv?$!quBI^Q)1UXOB5Tv zf0xXjv4yMd?_?t=)A|68=+ecFQ2tWCwk1^ygJ+iMT==#K9asvI)UIsMfU*BOT33 zJqMOQ`Cp_4E+|Q+JXU>1!|xXnLMvX(DI{0j{7m?!y8{S>gGqqWbP8Nn6mAbsu?M}- z!mI!(=Xpe}Z(=CSJVr-37UgE>Ohl96wy_rn$=lGp6z1FCA+?$zju^%A``&*xEW6xSNiLcT6}q z2Tw$|qT0+shL;(4A2f1VYn&LJ$8r~#cq=w8%;k_ms4_CLP0kL8&Yuqy_#WrpejQl! zm>Jnd8(T6WW=w|hwsm{jXNq^Hca&=^dvzF0V>j{%ZF`}Ft7y2Z^-NV6L02D+1WvaY z5LMb}p~Xot7UK)IMMYx9OcPQL>9(PP4=~s1GI;Gf?#N@?+vTUb5>jn<0yUZ0`zQ-R zmGa$HLuWsSW>OEIf4TZ{48r#fiJbj(yf=yEeB;|b6zthc@9Ers0Y9Z|sY_Zl=Ta{& z(VdqsWMaaqPa-}|UG-_xZ|>D(hG2VSlp;8j@G%JLaO5wvUV-VsuMI=VZxIiF1o;$R zk-)E(tylHELme10`|##_Qq&uGpGh!0lJ7jpp^aDnuovLQ9yj1en>L`TZ(*u!mF%V} zpotYR{MQ??8&y$BVMG;?ckr(H+>K(|ZFKf|Ol87mU%itjMGsECiz~lrErW4#W>3Y} z&Q8M|k~$d5$~C(k(zElsBRKcD<#iK$h*}_U&(zobrbsxz8!l&@BgfNTH!IvDGmi2( zI6{D!(p68*Izt-yd!dqXDeV#l4Vnd0+ZR}MURQGLnEoI#Mi^+LfR?b|BaY9`u`h=b zJ|13!uMH-5{#?743oOI-R${{}9MaYZ!S6zTd?KM!5La(GJ~cNEF$tj{#h+YH5dD?_ zXM#Qa%c%cPct{&Kt%x%&Hb10_#OO&4zqk8IciH=(?P}in#(U?0%?J!@v`3@4iJDoJ znvvj$5b9W{+8-A>NEIQoL8y-Fpw8;t02Wc@Eq3}tv2guSQ)hIrO7X%PkfFT)=lcak zdcp4u-gEJ;>G1hI2Xt-NQqxdB5}aG?s-1=`OBIL|fk>Oi+pMyUeys3SRM)Uq^{5eA zrtVY9$ig3R_aO$`5b2pJqgEhJ0 z07jl4>>|B$nQ+|J*WVf{tZ2EFT|Rt69#^8`1{cMCrq|F{TQd2oAfcB3U=UzRVPwKz zjBi5ELS4W8UgeGLSKZnrp{8iU`P9y)vc?j&vup10X`d#u7W}vSM{l-g6U$EU#=4%Z zpBkSBA_ylb3yp{;&R`fg`-zjD?!#OEE`aEVqPMSDURLHDhk^999Pesxq=YMlAxkCg z)t|8`io~Drb*Su&T7Ik^o9szzdpe{?eapGL>>&idp|s@VyBUPqbko&4F%OBf4Z5(K z0`q%CpC(c$w1|ary4r@+#=Q+TWj1&18(WrjbHMq_cd0F%EBK~s-Z%liBA@#pN*(~d znE8nPAKauNin=;2toVQ>^ zx}MfvZr35M6&BKEL`#oS#B)FxG+JNNhpVKK<@>R$5*2at?WX~tVzjm*~98*@hOv3MqqVMH583U^zHkpJg`|1 zE7oHrX60pHrl#Klrx`{{tGnu5@IQuT?0uBRR9o1Mm5DE~H^siTql5ovW$+{v42io$rf;o!>1i$GLz1#5F#isB(qz9^BPr zk*Pp-;7?$EAZF1U12<-94w(aF;CPO!HdVPUXVzi5gC(0^#FAqV%A_3wOH!1=H!`^3 zPj4fERi0MiX#gcPkZ}_9B}N1>{zBlgG4$ZR_Ujwg(8V0W+bUf zpOhkfGzNNP!o;iKF&Cg@e9<-8(+b`2S8uy%$$<5rTvyvd;PeZ~&40&XfUD`PVYn|k zGTUwR3Sk}>QWnLm&z52%DJ&8LpZIfIWELj?_1NGt1{Bb>*YuMGm31wCA~4kB`>MeH zfQD+E-w<``AHLfsYKExqNbvuD{!SjCpeCG)P@rO;uXq1Q9!6bR*({uIxj8;HJd({c z3|H<|`Jl!GdDg8_;7hk7G;YdBp#YiOu1 zgK?yl?q)0}f;d*B4G~_~;(kA{4VF-Y%%Lk7@#~iG+-R(0`v*}WNm9rm;lETCUZU!H z5CissU#gy_cACdQh^~*8v1psZNonkblS1x|?}@i^gdI_6;8!%mezcE}Gl_?n!}v9C z-P0fa{l>FhLN($xE92Kxjp~FfQ2eA<|@!eR+BgvRf zve$6aHa!*%w41x*fl-Lw{Y`mq=1upu~hfLCg09zh+uf-M3w*1$|>=>z0yh0 z)=YCMD!{(7;Nf`U+V*zV+9xYCrUO8^PC{E+Eh5 zaBi!%(>;84@DnI9(mJ)+0X(0W8a8&!*-x^t7YWZ|%)D4~^d^;r01>-$uwS5&oz&rM zcW$WjF|Tu9ADA`692~Prv1|K{CDB4W5U=Wru0aRCOX$Nt())PmAH0CWJKy~B2XMnx zw=^6hDd4^-2Gl0Nbh}44To(qONsy<+lA$1BKvQ7WAVe~Inr8YF)m_&Q9W+GbmcVx? z5P46BY#i3!ymQmXZ#MYA;U6-YA970)nraXd3htjsG$Kc!d%zG@6hZiG!$L}z^~tA; zw6z)XRD)=)HD)ll0Uh+0^V}4_7jNqZX7eI`GC-M0iz`>BX=vn_m(A(c<_+eMiAD@L zq)q>lTf9D(V!$q0Km}D1%Ui)-yWy^bO0>B0srY6ky+;^*$f87Ry=!zceaqp^;0>44 z|K=m(+2(gH0Z%W4`w24Dx3sns!;kK5a>PJI`9SH*)9UoT8uirc7})#JyJzK>{OC|S z&lyRk##Mkt-!27Gs3>T#S*i1 za|qt~VDroi)m@XLMd#-9B=*)*xE>WZA9@~zF#ID!=fjD&->bjD&A5U7&CY|s0xCM_ zoCyfI*Om{7!oBO-`^ozqT@M!~Bf`+nK+`w1_GX&C<*i{N_^oAZ?~P{n%3t&RZNd4_ zd*D3&{o+=tCLggyF~^jqa^W`85Ii-*d-k)(&YRXPm}Bqhom%TT%aWaEn2S)ZatyUi z^o2XeX7Q|Qt@Kva?*9olQ{vM;J<MST+aMPD3hh6xZm`w`S919Fb+j zT{~aixSOsCI-^;r2cf@h4g+K@{XclT?qKU@=tIUKJe-5WHrE>%_ltF7U*EIm6%D4n zIJ*axGjpVgxp9 zHYBoel5>Lrs?|cC$Xz8d%Z01vrNd{{Mz8}imZhwY@oIS_S(SfAI4kk?JU{&TI^DhV zwT9Ej<1X+xoF0V!4l0m4v+Sp-KbTxKcaK?~OB%!Y0+wLjBZJZVU~91QMbYGt1pEBC zZ7ldrazictb?#=n#xiVSsv_Xl{c~EJ^n`kptePmbRahN!6mCnS^#fGs*8Jad-I*Z+A5*sD8bx^we>|!O?kfMaIs$!IsJbwr~OE2$8 zo;n}ayf^(@0BVDR-oAW3j!Rrui&duoxHS`ii?lm5BI&tA}# z03?u>?&0tT0-tXfxw)B3PPYX`*gsPH-S7M3YWe-9WZ!DFQD;C1yJ~a9xw~6_{)2eu zcSGaW^NMMWn#bJK83vCeRVAWawVhi|xRj5{nd2DNiyy;Q;%rf!!10Z?4TJ?rn8Afv z`)t_qx%#;S@uv!s;k#gXs32wt%xw%Z0r}GIc-o#wE}y@=n?&c!lfcF6LtwB*_&kc0 z$00lvEk5&SQLNr?SA_WA7KJHT^zkY@%(tj2J1gsMZ9@tCtDFvd$j@KW_s#_gTe>mt z@?dS=@M^Dk3~y~mX1xdc!(GDqGNFv>kxc5oZ7WM8hLB<4>wa+{YlZLu=5o6%3pCSK zyNPw0=b%R&Wu+PDZC{b9$GEYXc(Da{50a91X*1sn0F`p|od{5w3VawRDj$aMq>IQK z8X9cu>>N%Gh}U!oWaVX%EO>{6&`5;?@aEc(m?hc_8#>BuABelirmF;BP~H4*Xw=UI44oVUze4ObtiZ2@r4|I96u{|Dciclj4#J2E36YWo zj+;|9g48%q;sS5NS}j)lncwXk-TB2g7tGWTa=e_U(#d?H0S7oHwZm5#`grX$UWoqw zP`l}G@Y6!ecsc+)x5g%Uyba%r7p#CZ6S%kS8OrJZ()*Xs=l;Q^wrLKX0bKF%gvKvT({pMNn8t9u zo|Mg!Ifc!9zV@lsC6$R$Y`4_>XV!NF)jzlFy!P2p%bC>7kmJi=C z9h!^Ve>-Q-bD+wu`_4wUaGNzOaAWps~GbW1&O!aL6z59JNw3_7E-Ni#zkVsEx3}?tnr|1R-v4x<#qk-0k%OXtc z(v@!88sPJPZ0ESnoAn!5esVVcz=D_TXGiTAdwgYGJ}ij7gmSrAoW|p-|36*uSdD0J z{UR#|$I$sXNS ztD0u7i!jda8kud}!-J2|KQhrUvs@Csu(8Mx@h)K|BLe}$W%CKr^oB)Ys6?uCOH%Lm zDxoU!SL7s`6o|%irs;(6;ogZVBEdCo zz257xFKUrr@kMt5y+`^*_wM|f76}LytgyATH++8FUj03gCReVZ?PmO__t+~scQ`Z| zOepjA1dohq=9Pisv#UaOvaIZPQTmd>{JLIu)pdB#5RgkEs=kG*$wkkqhmu)|KUASu z4*x@vRvWIXPcm!NW$a95ZX&S2fiadR-U07H-Ir&a83q#%T0wQ=@XH%rI7v4}dFSUr zbm(t?y$1i-X|7rJat6E=%2Z8(rnssKxw*MTQ%ldt$ts9cLR_t9@4Hu|-jaodv-&Tq zlbB=mgQfcFKq5`sF(iKOF@%$ok?YP}xp>|TTIOz+zCUM{f6Z6j+~P*nLR`d#2rF=7 z$!TXEM~2i^YT8I9Tmq8KE~0$x9^`cTE1P$Rfx(ag<4fa~#3 zH{jzE0k^1~b0aG!=X|n=Of2X*ShdEPnVZee9{1sZ?TS_g(4tk=F8N80`*BG%bmq#u zXB2@OLjLiP5Lx9&Z7Nigo!dAsqRao;NL?FU*bX>RP#Tvq|h&;%!6 z0m`h`C6`v08y|?6)kKcc==$;KBgW7$(obkG5zvxIq2SPTV>#RKc=Hn`Ckz>_X|39i#w)$9=>`!%)%m#$WU4sYi_;#LfR$hX{n5kL!09D zP=AL+(GbEo<&|fhG73aWXblLUY)M8B97o;FED2OF4pXBm+2)tJ1;~|0xR+L` zDWz0AeEO?tMY{6j@ZDy>LXee{uWfAvOiuU=6%4XiG9hw%*dsKIxA+_`W6?bOh2;Lt zs%uNDOiSiEvyEMG+!~_G`op@6@_@Qxe^Iu(qGfpR)QOb%N(510Ve6-)0OqfBpW*3u! zPjD5htE(ZQBb~d`jb}|*P-Ju18Eko81eW+~=Suy&n>{}|ZwzeMZMuC#{_WMCuzicZ zUXtGG-!=7(1;#`qr)8f0iI9Y^#OF^Er2r)p7^%_eKbfk4dMB9c5F#Tp&&b(2Id_97 zO(XpS0f``c@yS8&7y;+3ZH~uET$zlu>(xZD9pgts>UU8xRMNbAF$wd-HKK%tNQtO0gv&K?^h#^wY zQ9{3TWStJC?0UY@=^|EWC^=OtPJ0yrMOgn61q3jcme3 zb&|~J#t50wG>Ga`*s6=(p9D1{R%(}g&*HyXlF=l1L=b7tAhGRd`5Bs;g-o*VzpNWn zeOH(^3wHG`c&2f!ED|55aOIDq=b zk|Ir&mX^NKQ8#=yh0}m(`C!;_d8Z#&Znqcz*rPQ%bd>`tAo!f%RWb7EH~{Qy-vBaP zm{cRna1>?6RbIv~aj}S+&83Ud!Gvb}4A+2}C3!re3~ZVtgV{qGdVe&{7%siVusBNVwy{n?5`klif|EwKFx+Bl_c|Y=cVq0IS8iPOf%5kj_v&>Bf^6Tnz3d zFeD7xClBqCigGWZ-OQt0#~YT#CWUYex}VY-*w`v1<1TkXt%O>BULF1yZ8 z`f^osa(l_9-PS6 zbSUAk2miGXMN#zoGm2*55hPVIe|^rNCL# z_fwVr-Iu;$%U)g+$UdjY{&VP^NvVE;cZnS`)Ekp5S?le(wDg=J<;X zyJr9PB0mq;P6;+hk?=@JEbQ#!Qk&G&ITthS$GXcEQrz+)vsc?1R?QV=DBR08oCdV1 zCA!szec7N+-Lv;8hr9E&iv@NJA(Y>-sgPv9)~s$dbltk9g9Xv1_t4O=mX@BG zw|jU~EIV}xH9QO+>VYrvwF25g6LdnuzWbz^d&yv4XHoczO*)8MgNf|Qvt{pTJw2>< zUr1M78R%8W7jx+lP>bR1<&D!CbSocm=c=V33HG;Nzl(+VEiyrY1Z_GCzZ?IWMQfc1 zaNRUoY7%WyE(`iS+qxUrbzt8{;kBIm`s8NE(kV-13DJ}Ca8P+jCW8gEiSw#R$jC+zd zWsg~eC{z3tJEfuvMX}2+T2&iYC@v`V4( zO5ZeXWmSjc|iSLHqmzDk|^yr&6NRPJFu&m|%6L9^_&Gfl_9HW6= zQ<9h01euxx4{`T|Dp0R~0e17yuYX}w3uC#;r^a1tcRzhQw5YgNxWMcA=SH>VrIWqO!zdmV zA0$P_1p7-+!U~`X(E5gkRT*l^h~H#^lIiPo-43u@*#DY${D>(&Uv8Sj7#SM}741_k zE-q>6YZ-Z&hl%*g&e}Wh22U0)*}!lmgH64D={7lPNtK8{xi*cb9sjPk+IzkZwyD{0 z(o4H49&i4g2fxUbM_#KrxSlh}mov1U9aMD%cp0*aIXg*ou=sOz+OAihTw`Kpl|JX6 zmlIamIy9PYk|v{!P>%MmSFlq8u=tomH8c2iV9OZ=RFfHPNeOwgbno-Y7Z0iLM{3*q zlMk9GvLIY@VqBGc`Ep&b!&CrRKv&O6jYwu=?y^l2R?9Ekw!}Q9F9Qvoto+Q`pxYI==HEYjsh>P3idC1XnzG>v>2x)=@e`X>JgKTs0kj0&u z!esxYy;&d&Hd2{04GXz4g_)C@*}a&#h1$rQFfEEXv8Y2wD!UyQBraMSJ*E^>qnaR% zokTf_t;`wdqz5vVfCXd6@QqtVWPkW#&{%VNV~Sk9ng^%pXq&$mOv1(EA+Gxb;UTLy zVom}$eA-sOC;&*?EbJ6_5?3GI!0HfBzRxS2LickFU{ncW=AtrmrLQ7rpZQAX+>}VO zK%sN2N-^xTk@ii7z`>jKcl$&FxXaSbn`K*ul#Q&OC+t*?99+C}j%5?m=aGxsPSC}^lp5LNZXb8S3+F# z0CANA^`{$F`bgK13X-E*T|GTyU$rk>wZ|kFJ=Mv$ zhMSIrM`wCRTl_=!G&8CtxaC&arf0U)y?n`CQ8B)ukmLVyj#A=dy!YhUqnv2$%A7by}Y z$irI?l135b;E~CR z56c1ji=#_`oKc<;B$e5i`C4Fz;^LyuaA{^YUp@#RMoH|9hNA1Hr@>{GrB)KlZ7b-I}%5)(x$^ z5JZz5X5;t#d(9^z1JvS}NXpS+n=(e)F(IRGLJK%ytMX}ty4LP|;#?oxrb>ke4!muq zTUJUI9lW_GgV=&k(|i8u17Sf$7t&0>VA0L|?cNVc>Ny)N!~J zxtETJ*r4&?Sp<4DdwCp^U@9#J>er1<<*I7Xz8vQnt@N6=op(x?`xf*Y0$ zdZ^5DFD_&dHS8;ZT{-B4R`*_Uiy8@Dn}VXK79|%y!HeMD`CJVSzfq|FO1QIJW_1JOYYKDf zN&U(_?3Ni!0GfgdN(KV#J9OxK7?dYq`Y5vDF(m|%s@Ol~`Vgmbmoru0_=b1|`ljwVInxIwBk-!UIYuN8n@7Tb(kKhAwnehhM?EUt%dv@MD z?X+abmL=;<^YE22Tq@RXO{pSl7Wr9L4mwJfvll3xkBJQ>Zd$BWD#%hS zDPdQk_Uni2lsP{ZI5yY{AaLy8tRwutM!pDUZuZfkJL--WyV}ln;FtG0eHTV@KAm?} zzqXsq!LgQ4g?H0@m5n51VncRAE0Z@fZ9*?wog!Q|*UP;tvxZ4K>b{tKw^GL1mvi_v z_j19J+gbY)R(e|<-~FiSV7+^ZB=Mx-g~DDlb;1fcRztkO)+ZSwPRMctvfb~4aZ0Ci z+;PG=5*YDfD3$i6hOt&2in1i(O+uAv?A#MhS%3M;Fd#+9mrPU^kw zDM-{)FwLCdY57j(GgJvVAe2h=5_BR$O`@y|w6B{?gfFs3-;U~;rqbZ0!(Yy^U-LNE zUt^#uZl6sbG>I=wAlp$2hG`d{siLIWv`ql3QqK-o;Roldnc*^wHSL^s+fMH6!Os6!-WmFLcn{z{@olUK; zb9`(rk$Fi8To5U{7c=2r4AQ;!N$WUjoz%F(^vZQl=Ly&JT}K*b+}C~n?zpE}Rr%jc zl1VR5VgY(gN$Yu(eq5iO>=8QH;_?{^?J5C&RTJ2(c~UnNLJDcjk3l18+{eq$K8F3a zZHZBLoI6rFI4Hx@yl(f=RW^?x8Tl%wLaCzjn=$}zo$>Et#=zIl3jd9`dnhac;Ncvv zxT*S@AW}m+zqqO{J>cCn_4JL6EV~jPK5Dph`*j_I-yQ2*n%+>B8$HtEDAP1Pf861^ zf-6`$9+d;CCk9opd1_akf7GsToCsJeIPwWMv=H4Tz?>JzrFt|CGqr!msUZ|KNsy`n z)e#7_1Y7^-7QI!V*15v5;r2-Rw;cov5MOvj9BHgu6gE#j=!sN->`ZbL@V0b*jDPgd zN?r}(La)|Vr^uO;T9~Fpu+>%T%2jo7mvUVfINQh}`5f`I&pkHl$J}e^AQgIi75^6& zR)2ke_}#@AakBgW{t~DQLa7{>91!TE^I@N_-A2!Ne?Ol-DY>gY3;9*~S}SnfVx=m6 zK9^PG#eRM_c(ipUl6b#J*%Uq}1}>I#8?LLMdUk;$!%J_ktF`+4JKjdg|4qk>`*QKO@eKkKXKMogfu(IKZ#;?9;f`+_&Ox70k{sdERO@bsX=9s|DT%9TVw^ihvgY*uR?@XrZmng|VkUcd+c^H?xV4&ZJi$3L zZ9lsP{i5N79GcTia&b7~LRQI}PxNf()72E0U2y;e^_?a6Hzc$XpUuv4zojj=p7vIn zEFa#k?{3!?E>faB7#Ej7dq01v6tX-KXeR!<|JZRNo%p`8x*Cy>xN>_{09dqz>*;L=U2Ll5~Nk z^(e+_?K;~tc1J(bA9_173xA9k;vcDUGACdX*-Z7#encApS%XK+NgL6noWux83~k?| zBT`Fd|6g&zz2M*U26oq30d*8Wn*eE_rk+u~>UT6~XWq zBlbzV-n}Th5c0x!4m>7@pHxJCb;mWcL02AcQ7Vu_y@@{@4{OLT{EJ0i9>iA!hFeLT z`hB21FOQv*QsH;z#k^Qpl^E%ca@)Rz(XB+NQi8;HF(-v{d^fLjF<$eF9K(?dz*gU9 zt2%IqbS_|4(2h0zI|sLu{vw6j8mDWhdk@jE|~5JQB%_sxv^)4xX$Cj>EFkBpAC| zK0k2BcQd%{ZvV6=e_oB(0mZR;_#&rezs>fMX<_`PSYMY-YdRbbRED-BfVQw{lx!lV zdslt{@qg%2U{e}p932i~<%Nwic+>=-_3%gtC}50XvR8am)eJI6p|j0=Thucj5Up_A zs^W_mLYvw#$AeIN<-!L~sJ`o9suIhqbP0ZLWQ)sYDBL-ZRLBN`qwHsSO_)RQ;Clh2 z;+{*qp4t6#+pIE%ULU&3B@X2N zK+`5JQGzW|{AiJkRFCx1QL(zOyYD7G9h}(O|g`oAI+F)|0+;}ALWB^Y6oLTXcYe`_hAbhpLg3Tw1F4aECWC#rjYE=FwvS0|;$ z;!Dc-a=yEnBzf2(aT1V+&Sx4`=9rq$SwKUg_tCIaEBC3DOD37tSbm6g3jMetyTjqS zpM%1E<|(KP68UmUo#)qximFRDiH{_#2qAmi%i}eAHG?WP11koY?4AUGlwjV!eEoHW zxY*_Epl1`X@o+JgpwiZsg`HC-gXz$WtM4rL-L?Wh;^E8pm7KiNa_*Dz9o^#;xz94( zI@ScHA07>gH(~-U7DeNg`Q{2b>jq?_O2$Y!i1LqS+X=n%v`D8bDG)ut0UrZ$S;YuU zQZ5u6pO29Chv(+qXzHBbQ!?Y7utWj}Y}E=Y&kg0&TJD6r-OeTa=&H8wsN$mSuUClC zM}wmg>(B&O2Xd9W&3vEC`&RxDb1ovNKzz-pyrQCR4865|V)Qj@!q;`yb3|xGwXvf| zq3V0SPi5ccT=f)X+cHO%b*q8QHgm4o@JOFNuk<=Ilh_(KU?%% z9Sq@4kJi26mKE9vS{r9CFv%8rtX^ZW9prCMhre@1X(y84O_Y|6-?MEYlKRN!xIOgl z`y@*o$)v-dBgXx3;3%jiaiZ?)PE9(ycZFeZUl^w&(gbUm9S=hPh(+DrkL*%JYi@n| zc|!5EE4}yq?_c_3irfoYlF^Ee0CZ@;rU!Z% zsjUO6o_EHgnc%}~p=lueaJ$q1-|ho3>LTfCowEl!N9fBcy1$8aWd(1*Y5`NUMCH(B z;c2*emh&N7hGH4OGC;YBXeWJb#g>ORymd~g=pq`)17FH7#AnuYH6?ty#xNv2$_a`> zs&I68Lc_o)8tLe{)30N9BN~rf4iVn3NgdH3t2V`;MNeXN1>u;%%)>&8|BfI zpF#~a|DAVxw1xnpJxP9!DKIeH=Gv;av}EXLn^vm6Gr>2BSiF)E+{@`4N4eI%VJ&gW zZ$z%rTYacROh7qqsuY0W#Z+7)Eb#FTy569p??@h$`MfUnm}uw2p;tVsm>dJ&8%&8q z^4GrV6_>*BCcHp-U#h2g#FK+~DL_`r;m=s9U3(`4?Ak6Bsc z2&+p#&6P^f)u_kczii41nkTjoB>fTdI;m~KZ`S@X%o1VGh3nK-;iN_Jk>jgR;{lbc zt6o+GB0gu22dWmq=aI+lseIv!3lOG^i>;@+qujfV@Kaj0zlhC+arpu#A(23l({s`w zaS8v|uol36^iL?mt$5A=HGCioV^xjgArQAD&^mqkOo&sOIJExGvc*wyCgd<}<(QE=<#*5AqHcvF+!f&w`}C@Ls^G@fsCxaJ2fI-L#_|#?X=OMi%&% z@8_4Q8|>MnhC!+QpIkK>SGbdg5l2f`&*<=+sM-FEg_rI9J1O;0_J|ro&6?YshbcVW zZvjlH4n-Is%-YrV)N44(#!RM~XsC)Mq=1dZ1ty*H0;uSzsZzEE)>(?fDA0-_f7(^9 zIrgcEUf0yX2UtQm^KdWfw7@UaUpv)yf8SB2RWMZ#X1h|RH#niy z9Z{&4fPPMDxF;2%RH8vwR$+W5k=%vKiDn6t?N$D!|39RtdZ)qcu@@H_(gYK8f(caH z+17V_M}0GXds^YLzHR0rU+xUsCPQ!dP#eIIcKJhDTUs)mk1CnMxhz?7L0osVsXAxP zx+7P8$69R7n#RXfhZ@YyHQZ+9{;I&V<>LET2a!onu0mz{J%BC@ErW1B5J}J>H)qXK z&-EPG87}Q0FC!BH><9xIV+9|2!zKhJA`~ix<)_}@l?v6dur#W;=fPh%bwZs#Xw-Cd z`_^cgsBAX#*E_gF0&eBO?%7LIHsQqCqc`$#+x>)cwU5m1brSHVd-EQhEnFG{D+p8S zS0M4h++!#puj(HftCd5Pvbwa`!%rWInEx3c-c9IHwVP-f-fQekEyo|h^mjaho!qyK{}+nOG;9Z&H?F` z&Y@dc8YxA(L%Lg9X&4x~W9W|e=)KSXzF(LR%!hO4oZs4euf6u#*{zKkS=kkX7<@Tv zRYum}o${4|7RJF<6z4 zd(SZDy_i`$(K0+_(F>HjhC8|?l7w8f5auEXZGAm*ft=Bd6F*-=NLUg-UN9}ZIoceX z#b)J~e$iYQhO^wIXC?c}7+(~q#_$(FZ(B0=#>u-8$a=j{2 zH=79w{n5D6sf1zFxeK;^1EImPLa|pbP=ETo@xuyekvUf)Ba;4f`%g(#$p_WXa*c_N zuWP6qiv^nb^9!c-<)KK4%7#tqEo4G<&O>UG9^)Q(kfs<09q(MsrWf|q4yNC9Llzo) zhB0)(%;jyNx4j8yOlofksi)(w>&)w%KTDSP_H&7UR!y^D8YXJihU?R_Hyjv{yUG6w zY&lC@NH)HwVjpR!ZBQVUudcsSwEvGFook-9XGjXQsn607cG;ZcAd_-8O^CnSFk-H-+Rpt)Lq_U+FyjAW9Hva@mVYb(}Og_ACg;~Gz0 z9oRtwai+>yNb;X`BR{UAZbN(amFB(J%O6muB5>scnUFtVa!EP~)5_V*BIClG)nwF5 zR&DE*yGkcyi{I$%{=kUv#DmUGo=JY0V)it$aO4%46LDp!@?i88eVjo;Z9H!IeYtswt|Q{Lk{JS!E(ASc!FAEUAvi9+>Eljlj`cftZ=vmYyyhf<fQC2JXg*DAT7zXfIKcw zE$eODNF)3Ia9KVjyqCQy=q?gYoE9A1?V@xWD z@BV?=^sp;;zuai1oD`Vyv5e224S9!br(&%q&+nkgmIyt?#H6;3zv~{+ZiMdY%@`lP zP;=+OOV4H^nwN(5!)w1qpJT!Oj(IHWGNOD4-_bZ3lyjZ!Qv}8P7SW-tsT1)3so5&i z0xj5RdPaNTTd!fx2T6nF_71qj7=7kzM``m9-}1DTsO~dZ3;3n9B}RTOV}tY?AGv@( z7P~Yt^{7_MS4?xp3TJCZd)aYh$qJv>bvE| z&42>`@Z}UE!K^Uc`|8;Q4DTM-_9fvSpduPHE<$Hr?LT6<@Y3`RLd z2@sJpEsQTk`UK+v!iM_&0n8&)0R*{ZC3B;mH@)MD3Efj}(!Yn(Htt@xOJ9gkXPYvW zI?=0cjiiDWy`RRPE@TwEU3<$KxgqmKGnvJ+ADxC6MvxUf=^~X&wah?4deLj0toSkK zQPahA(w+s+-bK0ME?obTPr)TLa>P)F+3>%7xSp_*j-~lca+I>l1ZQLI==y;0xm}rZ zOuUJ*#vYXrk9AelYPSue`f+WKHp!Pgd_|Sq?xXOakWEMm+SThP|8dvf&dt-s>rXxwGgP*HrP6?mTYuSWm;*mdiPeq zaKuBU>Ya+eWyo8)7mm0Gr?c6ktG~CJghi^uMNlbaQ7`#mqXA)+l}YPbc=1Z5^Eap@ zX?VF*@?Rmd-7no2nKGU`4)%+O$z=R3?{AMCn@1C_OjeX=T^tH@WC9~;qoA(7H_1PG zVV`YbxUkv<6n8Hm9sQs3OTEiw7M@dxr+4^@!LC(gw5pPmpDPF0s|psKa$Hhnd`O5J z`I0Kikoe^0#$HF3pM8B@WxIiXr1VJMT5kTEcyg3w!=#OMw4E5xv(NhDYWw4Fuk^|w z!J||rSeVuO21k?h6}*3q)qXtwf);Kak%X~Ha6lxjz3>=mt2AMo^s}I$Izx$H*SiQW z63-58j$w{sPysy8pB$SFUF`%B*LqO4m}CMb7JiYu!n$CLaWb-%Sfg>IDh zBf>?Ol^u%t{(|;Ku$EWhg>EOGOs!l18pL<24{OI#wuGM%OBYnr2B~>NE%aTp^0TE& zAT7Vvbn{|IMgj5SV{(ETP3nPe)wdwbJ*+fMgA_21@l!boEn6^{Jum6Oc~B zV|Ju(fo7CY_VUb`+AZMB)FUjj5;U3TZHXKmlDZ1sq=91#J^d5>Qv2sOi-&?zcohr{ z4GW2cByc6}A~4wXvc~T?3}`>&eEt#Qma2%T9M~fNpM|KtxOw5NPC*UVwoa-Z1Y%|Z z4k`J38lE&X;?!T*Hc=H4+B}zbp1&1RlP_^lSscz1SyzYJFkU>z#SulkgL}l&s4`Y8 zM`nfRKrVk|1gmZ5f!GUme7Zk^uDr&ONgCvvy_B{_6}Xd^$&J=ZWEXsQU zto*tZ|D)hmSWXf->zqn4x5Fz3?sOm44UtIkBN>2IPw~uX=+VhMYj3y(J=({!R}6Y5 ztL)Ud-%^dWx-2j{Qil)GnjBYD->J|0GM9JaZi|yDj#HT=jB^V9Iw@1rp5D>rfPHH; z^Zu9%*#kB|6ii1_&`UJ;mGkng7{tfp`ZlZ_dfe>SfUg7Ds zIa^AHU06`>;wuFgd3?(J;KF#kQ%1l4qqR}2*!bg^k`-AU9cr(4sSU1=x$AxgqK&@Y z0{40ywLzxNS`R?{>A>FMsmX3By3q~-Z6mY;j*mw~tQ|oa2+~X{ucew5kyV+PY*y>G zb2qhaUT}D!tP5&I+r7a0rp->Al!A#qJ|}-j{aLK<_6-qZo1AaV4JzK;S@&-h)D2V6 z7tq1+R-(NU$?sUh^x`aG>GsCHQ}LN$4nXeY+>jV$yWjR-=g00wlquK~8zx4ep<%z= zI?}%4J9hj7MR{yuO(9#U*TV>l{7xLvWK&rI8W^-WsgCVgs(jK0cM3|iW0yW8#g>RU z`tt*^dgRK7eafzLbF+<@2FN?gpQ-EvIP9OOSUY%>wd(+i<}6y9Eu@bi0zVCKA+U7v z%;~yxEolaUa6ihtg-^iEk}Rj#ETgo@SdDbhnH!zCw8O~|K#o!9ZO#Tp3t*9n zo`O0<DP1_Nl8xP()SFh#3y$*!UHd|I6~X=TB=KQ7L!aCVUzuNgci zHFR*gv`R?WN3q~6s*l3T*f5mY`2S*IP1_)#%a|%ZKCbqAreHD1Odg1uXBDv3G$R`l zGCbP3_W4tC@xJ8&TqMdYk;gykFdPU;g$oE+wVZV6&QEBmgh$bk>sjaMS;R0oDyTjq z810PsYuS#;`FTAQEhfYi*5-S zxFAZUJxpc^q2j($savj8mo%J|&QX1lmgk15bk0%AmqVSX{yUtWg zoa!%?b)S$Jn8O2k4~D}YN4e*(kh>ttU5fV1+ss0JMWd&tYzh^Mh83&ptggREU*j^T zT?B2Pn&K(E)?sXHmNN|YRGU4t=KIb6s63&E8Bx-S{Sd7q^`1gDBFrnoRmG{F4xV8@zM*E;-cOJ-FS?L$iv4T zrpfO)EY!E4-exJF(b!h3(QJ@SuAAlIUhn?VdGY0p^J|AIkTAJx9mcSyFo#e)2aNk@ z=@T>%tIHDZFpPA2rF+2D~2vmGN6w z5EL9Jt;!Y-T^-=tP;xAOu7ImY!d=F6C-=>TjE-IlJdu;jtC1mfq@ViCgxxyC;=shSzR|kr6tJ~TYzUxu}L4Rp&7omec>P%{7 z{goZdx*MvQBRh*dd$ZmBD^ThIwePJ8jaIAs_I%Ff0^^k4uS8dJW2mdU;;&(H9q|_% z-xIpLUT#Bs7e_SP7QL88DPBo&$#?Twk|tBfi74m5X{B*#%uROry!C|!UUwjSI}AIw zq|9Z3eZl&~#5>Mk`eo6?oo%2WBzEqn^)n~5auJqTH2nn@vnjjxIZo;Aa8f@jY@i^(~XeOq9`9H*c88(k_vG+UC# z$p>3EYA6+UU$Ee`RDt1ozkF%{kp&wI0dGd&mMz^MafM{zYCyC}3bHY7#3=z(#(H0V z8muNWBX$Q<)az1h2##Vy} zAH-y6w_rA?-lb%FbXNN7798@a4AQBPv!(pim55|A_j}kLg2^qT$s_QP31@*yku06H zs29V4cRskKvXY@>sykA$d=Q|I6nHU>o4pVbBjgkLmrCS3U~JHtlgh8nPG>4FxoMW2 zyNI$QYb1SOGZA#1K65jB{!E;}bO_+SQ*no}bp{OFHhSfn!)q58DX1kI6a8)Bxm298 zXM3}uN~Q7x-om#-k)xpzn5fOWSCL7Jw*Q)d0 z$SjVSJcA#icm&3qQR1=<36ET@zP`o;`Y1)Y7~rnG)jd}chd437DpFMg(H(^t`7}7z zEYX&TYz8R!nD)<$y|QMC+fN#d~g1IfxSm_})O*9k8r4 z;8@(zULj7oWNtfYUXG^k`{m{i&ZJPd{&vEj0a+fkzP+{9513O7glj9qlZ6#)`%}kV zpC(@X5dhx*f3gf0`r@=3!a*h0PoJtESH;OZ9eLV0BNE0$-}FwD*@7$ydy9DxvJQp# zoO4{rT{WYN>Ck2Y_cynUSSR@kW2?DhS@8h8!6p3JI8~(51)SBaFx2>7#!{3@hHLGnv`(wYjn>y&2xElEF)h(h-;HYiRRG zjHid@JR8A^WV{f+n(h0IO6^u1g`obxbEAKZk5sCgNTf~N#LXNNFZtWU=zRCL(Ax45 z=dfHc|4Q{uP<&u|iQKiUY}C=7vz=4vk9mgld$Zf~S5og@&O;|Uyx&`WG?_XNt zU~S+L+6yn)i;sWDN)R^WTNi^0^=;+Ka;#j93!zdBoY9Jhhqv~7+Y=J#l+Npi3ZleH z=3x`2L_het>LpFh6?+2QU)^uM&j!sM&U>zw6Wuu9>Z(&|8TPr)e<7=vyu;6Ntc{sK zv@4@icxgA%q*3r=k>mg6dQ`8A!GP$@m1U&-@Tlfz`%vGg8*P){uR2HLVn|Wa?z)YS zQ3a0mS6PxNT9o0RHr}ZR^>Hc(`)6N&8n91v?1+gYFTwb$T9re&DbTSQhO1Asf!!xd zH`?=FXj6kWiP40Z;(~bncN7;2*Lw$yp8z4lbZOKLow;iLbYi z$wJ@%?^RuPQUHz#dI3HQ7<>r$b6kovrl;rhd`ZRL_TE0U!}J|@7QxIZa}RG}=}X5L z+qkReBYBHoI5x#QvdUs|FJ#{txFx^|;wL1>4aU%jm<@}l2dnRBwOeYxRBmyiLW`td z!NiT?hKGCKXT^itaJm~c(N{(QXUE(6r5D7zJKatRGY~+;<7X?E38fP%)PD@qKO#xo zd~+|G?fn6O8IFH2^O%Ek@QDDA!OCjB$ukXN>^C&~*a}pWYeUG646n2XB>Ph8QpSTgx_Dat-PS2QQSbI_R z!vH6R)mCYc%_ws&_-#6E!eh)r-%yQop#PuPxiZ=d371sb z#6G4Z3@OzUm~8TkzD=PAykMcg0@^BD#$4AWZh68Fose@s`yxJ3G1Y3H*4jZM`h;Fg zTbQ=T!XDqm$AZj`LD-#9r(Kt2C5zR!gszLA>I>KXwnZN}vZ{}JY|7K>Ys0CS>}`m1 zS_W<(U2|O1h=Y+M2@Dfg6G>rwqoC%!d?if6^j>H5M>bP!@CsN z1(!rVC*pTrDo*Mbv#BLo0pGPBo7cLy8Kyhrfd|O;?*YbAU`~Crm3i}CnX|QabP`|# z$PSc8$JDHSwm*rSuLhnH>ojE4tsNG@Lqr5)eKWTfEu>~Zsz>B%6_A6Ul^)cmR;hdI zwYH$Ni$%vNe~}9Ivbbjbh3D2o*!QpnoSp#;6Bu~@2~Hc@HHt@=G@UWy`g`uw;33s< z?Yybn#iH-GSe5N6#-~tkt;d$qaGwo+5z5+0L%g>4)icZu+Wm|Qwq5)D2Xpn#@Ifst z?+*808Ft&^F{o5=vXSA`ei|@6;^G(YT14-_+a1T{`JvxZAP!P$mZ^!a-)j2MlH15T z#-qS2Q|~!#3#)EOR542Wob}&um!Zw0qSI$tKzw8wo+~;OMtliQav^49Ir_80CS@7VM>6y+ zhOB<~3|~#bD|uJqnNjFl$2^|uT$ws9$|PJC`oA(nLl!tC8PsSb%hWX8R&eGR%5}N{ zND+w@XB?+Uc(_+S1RAl%?UI$;a+~TRwW5*_s@9RRAzV1e33=;NR6X*||v znnQn%r5lxbFQhuT@I0m7ML#E(j!cdmLkl-hNPni{`FLoSKw4VXLx+%-nrDk<&Gp+o zdj+Gpu1o%F#egVSHT>K;Hfigjbx{!5>;+JO%L1^5H$8gV(FdP^mZ$+mB5R*v{Kf=DB4bBhestw#*t_u=K?<&}jA9=&9B zeQALN)guQZQvzKHhB;*+{ikpPnbLBcC7cNt(?r%mmTb$gXgXy6r?=_~vaswPPW(0A ztd>(pl>GH)gg%k15pv;%*`E)n99`$BRNKuqmA*`CZxoQ2VFY=*Vq3syN&TFl&e++t zRQr2?9H05OCPOEH3LvuLfa!k2bSoRH%!1|Dg=xfSN?`*$Rg~{5T`QM%MWqO{szR%u zrr`N7Ein1IOvl6P8&k(#j@OTtxOBQ!mQ`5@v(B;2x6fUDUABb}g(t8uImH6r*iZf} zhYwV&{+u!Ng=$>0&=k7&a|$8?Y6r4Yz_;5+N+FH6Rsj-3MvGYd5)ECK<2!eUS1oN= z4qwg38P4&!JuGi%T`c(w*gTS)`W6Wy++M%5^F+$9~=P zN2>BCOaqRvBa)JH+ckt3dSo=LhL5k-jwfMc&^ZyYn3-Y(J6%%v7&JVto4&PmQpW?2 za;Rfi-i2#DAmTvWu@n5PB2u~-z)b_CNz5NX&nOm_3Zktl=CgiIk<)$Hz0ZP=q>Z~S z(e%&P28Xz6O}eQ6o7VVOZl(-^<5gKrzIXKuAVUCOkn=y%mTAOPzg(&|;gx;?44%Q zC;`n6El-d;g7m=F`_Ao1}3o9t+X2aBnd^>HGmX^D`?hyxW%7AE7$N#n8HM&@&n zs{tv8rS`?Z?%HHWJbPJ%aGhc1uY4wC0>O@(6jtQ2f>8aNd7A^ zosk)fVv$nOaV>znolU`0kY^+>N2wEt2AN;C_JSdV#||nU=BOlUo^o0dPcf3Pwe2UR z@SHHd(xuWh=ub)7-zyA*eW&1cKz6Z_BreATG>85T?{DoB5M#tljB<9?5A7crupWQn z5$8e5KQ3!wGEL@rvHcP~+bVt9F8lJiDw-?KSB1g-V}Z90zbgm{*gwVEAvV;y=nvR5 zh7H*Y>;6QlHetrb9<(@NsmAfOPrvXxpEYICN_0UYD=N?zj?@hI{+>N5MQQ?ic~I(A z!QFe1LO9F%P9^zAR^Kja<{}k< zNW=zzIVW^mb!N6k1|8AAmKL)cU`)-|r)AQqQOg=9o^_IbGt5a>-hwc+POmZoe=6jU zAtSm@&{k%lr+X!nu+Qjw+Sbq{DO00Tb$$QcHwCGjJbGqD2}S@=KPuSOL(a@b-`9bK zhyR+4hJLlH9x(F$Q)l7^e!W)DwEpGS1BHd`qy0QFS$m=c{N8sQV)+WcZIUPce9xe$ znkvYu&UMcqKj0Ed+g&+rbY8gZTqH)(0tFZ1Y_m0;nvl(w! zMcEarXGkdx`igk+a$c(%Rs6X+QNjzdBQEtnwe7$Cy85Lh6Wu16q+zZ~d9iN;L_|ip zwrHIx?J3cC}s^4KW?N~>zX@TWsVKv=ylX6*_dK7esqnIjo|vZ%P;d1 zU_Dd#Z0%sbT_nG{r z{X)@df%+Cm?`9S!l36$+*FC*zGMWn3j)3cR|5pv8$B}LIxx>edDc3c%j@&(hfup4M zm2?op>!nJAX3^X`q8BS87KuB}X|rdY+b1qQX&4P`og+2Mc3!P+==H+~578!QXV8oUR4xGw;83cr_eV zjt74_kXLf(7nrd8&GzA?cxBgR7XRSVAZyXY57HC3bxbHaP?Na6hj14GOsd(=fd$_y zhpvani9tTWnb>*vQ?Jj?9Z}b6+dX+^PM+;CT74!C8FY7nu{h4`pCV27=tXs#gvH2p zml2>4ly-qD`W+dP=I70Q9d?%Vb}&!kn0RFO0($-ageEh`=RAM}3Pj_<16F?g!b(Nv&n;FZVWOPcAD`;qcI$I)Q-DK5B%>~31(mr8(@{J@!SbGkQg*?9Ya?sWZ{zkz<(9D6S z*tmAh5cMp@256EdHv62oi5PqO;Hax7lvJv%v2RGcevvG3^TlZc_+`moB*OjMGilb3 z|4!qTHr~O@FV-)`f3W&+MZDo!hxn6A!ElG)x?GMyGtwMBVed%ZmetC|TsZ^IAZNu< zzi{K}w=ui6t2XIduCvijg?az4PWO~?nJXA}RahDpsGzC*i^4{XZ;jD5)9frZT(a`* z!^F$91g|@V?uZ9|TmG%qhb8s!D-yMbp|IH3g8iM5~>Dsn$zwOPUTsf(8 zS^H~sQijiQBCr(wD5vIs6+)8PvX>}bUO}_|zlOU115_qR7-_nFU;u-4+rbgBgE$#0 zy0mYIof7&XIRS$F!)>KT1ySAuO`r&~fz_BjVS<&a?jS9aT0;+t7!pOkiFLBU0j0rX zjX_}5+1*5sCZM&aqpvp62*WglWU-u5kQmq}uM(so$FTJFzZjr`0^R4t4ekd0_pcpG z62I-pzr?4=hq7!pb54!3jEzT4>K(jVPunw`{io3jdH{#XI@EoYM$N2@AuiKWU>VCOpqu{71$;>Kj9 z1{fy;AA4Vrh0k+#kxdDWL>-&#c)M1%M7`fxGCJV4a;emBTprNVwz6EQy_N2Zg|W6u zblq{lVcds*F2xm=3{F}=Q?+W_B$ssui4L~-yywb2U=Ygi0@1Zw_08~9-;oktiA0j$ zzNw5qGTh`cuWSE&K5r{ySec+{=e46wqMWdWWpi7|J&!bt`$N4b&e28i@<|M@`yO#; ze+|a&s);3ivPUg*N!TykRIpek*lzDm*^Pi*RC}pR+~Wue14Q41?Y)Z|%6}WS|Kvqm zQ4&i9IOUS0>R+=ydbr1K88qqwHvN0ILcvmjcIb{1w8ZrW15=P&9(C%d?y?qO6oZ9{ z5ZHsAC>4pL3^B7a@JKVuu>4Zu&`b%)t&T8jmE-*!Ew{UBu7+LCbI}Akuk9c3ISrU_ z6$5d)kcvibzV-${=CTrK!;Hr@YuwnJh1{DxIS3B*S!RU0cw(O~unipz&XP6?)Q~GZ z$w)ox)#9k;0hb&^(l!#q!|EAC68gwxMynl^Kf&0#%h0U+^J~l3aUflyIn#@&z&beSg zXPt{-6#fAaA^aN_Z}wSCNprL0FeA;)F!|(~)ZY%;Zs5;I&s7P5e6LzSnb7F)R4WJ# z8C5xs?FKYxX7u(o390zp549aEdj8hqIcYVeR$Eph#~LG0jlnAxU4KeW4K${f7hK3> z!AC^1IXgf@{#v=tD}^yaL1<-x7?(w!RUUqQ+PMG|A!&{|>Y}10#oHJO(<-4u?ZNBXJ^V3Y zr&;eiHq6`08DnucY)v}gpB#aYOBs3R=YI)tygBN8#OS~<-=lT7#D?P4B4S#|oWXdJ0En@%B_Jn% zxJort;xYL=K49#&*Y0M$WrARZadOCC4~yBzT~8~_n?tC4yJwyx3qt_t(}`%Yg$yLz zBI_XH2sruq$$Ly`t|{tyXr_o2@4sFrI<+`p5GKN8e$8Nz{z;>(m9nzZV4kRPbAM7u zXV((K@a_9(CwiS7+m~!;*b$lM|JROsGMoa_%;w9tRRwc(^jxkD3dZhlK-%4i7IKC3{Vg$l>h}@#JrIeG8i7gS&Bt&1v#n+PV`Yn8GKk9^(RPxk)@#MK*gG~$$U0Awl4n_> zaYGwi>#o0hg6fq9Lz`PX>VLO7xi_!l8NSXFg5nH45{dW8KB;BIPpfpLhc;uZoB4Vu z^53X^cURD}r<6gv}M`k`6h`?l|IA$Mv@XnIvDe&nk5Zf}tiI1G62KxdM%& zmGuf7I^rVdzSrCvsuJL*8}-{OF<)miRgK7>)@8{yAMSmhFtCneiTmgH(VNHu#@8t_ zeMV&xM5!`3&eBsc`eqNjG2AugOf|3V$qc`%l`vC}{d#A1rWUg)i}5`=>$C1VIXDbW zG0DV9zogV3I35JmmTaL+P@I!n_doB7hqVnw9kSjzL`?hb!jFALL6BREBsev{rFVYF zp%crf$NqXg%!%2H$lA+!btzdz>it1#_2ftV&6#SzdgJp!!0A@*Q(w2-1ZrSWw}o3T zd%lBUY>Qy3IplzwB(9^OKR&9YUv$Lea9^Kwuwtmmg-s42D^dqeTm;AWUU(;h()H<~ z?qnH!V~>mXnf4<2Hw#NdsdlH$2bZyg<8l%2uMoug5~9NIFAVWc7h8z7pCq-^I3jVZ z-`v?-A>eyh&)DhtF28YVclgpM8vSXJ;+G}y$ndRuAyF}nch{hh_|O+o!rTCy{xa}f zYSVdTH^ffKjnG(=o+=Taj=|ap*{hLKYg)(>0aZROl$6Qf&(EK1906AP(VCeJZ*@S7 zJDn4w@CPKujyJAz-3JxGc257VKO;)>>Gtq`{J~h?WD}1;cOJQ9ZCn2DHzFDmO9cC< z=A7J8`cDWvZy4XMU$0CCxCD}%)G{CQ&hWLqk^ZWf1u3mbm8hEx>i^U`3qa`rldmZ1 z!?we~H3#?83C&4so#*~T7iU^&$FQwE^lipP^KE&NNR91D%kDoywKG{8ESQp9{ z7V8|`aebnq37-riqWzkx0|6S+JOXK*aE^$xu z_k=cXuoQfhQ5C$&D|mS@d#!sa6T?lG_I`Xr$n%dc>by&y@V^%6cvwRbf4pPC&LRJ% zZfU%S?BI!ooB)brnAbY7g-A7><~?G}Eo@@_tEOYo!+DtYsy| z<>(n4swYGOvRawrv{;6nB}#c$=M3sW!U?Et(rv^?nbgYhMP{CGzwbGIrI*<)qX?!8 zUlFP%kWz+6(z#L}zuqhxs{QHYsv!U2bW3Q9`BT(oo_77c&7)_7i(I*DeIAj@Am7yM zu)4-hP2Oj{U*aQG3UxeZhwxv1lB4$Mqy8^0nXNFB^CD^{L)YLY#GkWN1MUs{FOM31 zFJ{unMZv+*hDUWU?%MBf`;gcFw9L()oR@CfoI677Ly`ejpDxA^#OIxWmYa*Q(t+`? zCc7HX5$d?Yl8ZxwEls(1c_MZqCYuF4OuM~5?&3ZUr5P-YK-SreFL=2wmYV12*7@2e zxDYW`ZEU+^nJkj@Rn)BH&|AL^u^W}wx)>uKb$eWb)x&0G%m7f`N@fIX3rHLTp)nqn zpc=r_5EKu#XtA}SqTI9o+^1i2&kw2(`<^S#C#P~EL%ECZEN3er@FHTw6ISYcTdK|B zpTo1tQk!)Job|MbP$QgIqc^fvo)gb^D8*Jf)jL`uy;3iCBT$Qox}PA!wRzOh`?DZz ztmAMZ6g}x!ty}%*w$iwI{d0EPc@B}0vPHEg9UCDx!7Slv6yL;zc)1&1Ss8SJ(ZZMf ztxoZx3E+WtCs%jDG()EOVssiInwK>G>|X8Q$X468^wM-3?|x=3TBlu`JYN57KPY*` zYPi!SJTNjBzl=20JZ_QQIqWbuS$>c-J6p}OD6>*THEW|x3A+EjMENhcrUzP(4m=Uj zQU|&MktEIcapHLtIDjiRo0%(bH&2q+j_iv<%nYiId_bu?dQXinHs3acJ=Spu=}ct; zp#%%w_%%ug;3>4hkpnYE_V?TN+Q<1Fg3u!wDk%F!y}YMf9s?-}Wbi4r_7`DpQoLF1 zAJp7@S?r9uKd7;(E6eNQk+yNR;iWs45^(a=rX?ImtQnuvTRpfOCT9)Mb=BMgUE7-* z6x&w@>q`*SU3|sqCaQiteq^_QMjjUdLWx``nl5k%6}l^EF7EuqpDuMC?sA{LR6TFy z-fosnIE(2ZHYy1O6>ZgRRG_Ov?#dMwHIOlb;1WKds_Rwf*P7gf3m1v<^gGCs;aT9 zJB@i5y>tFXeKi+PbZo%F?`Fc$Uzd?fa9^d+hLOQ-pvreX9^HAbX9wrO= zscKsq6IDU9djA7WrPf5LCYN#NlMAMaj=RJO|7Q{L=e78U{p-#RAT3&>i5NxGbht`SxGUwVCmettNTUw{`tXjG&`SHSo3+6#N`JhL z$KM-MYB+N7zdb{X32liNA^Xw|R_?lw3CCphsO(uB?;V0K8MQB!wElh;jt@`od6trf zIlXT+O}pwE@X>Z8U0tdlC|?Zs2s+4|sjdqsw`XL&>-?+a>*Zjcdp46wj33AfgdBY#8o^~N$U&H61JqkNQN$IrOTrCt4>NbLTu#EXyn z#D_melU)7C)x47yu5Meh$=nXkzcV-a;g27aQ^$Nx!iia*qg!C2@m_dmL!V(n8vges zIm`&8M}&3Dir$}L04S~Md#U>TtE%HZu2(f@-3Kp;@*@N@?_IQT(~;fCxxHBiXy2p6 z+u;e;L0FS`2CKbz%Erp8&#ViIN6uMk8@qXQow%+Fg+WC+#G`M_H+PjnFUUJ!3D!z% z^f;^});^|)vLXBs;@D-XC?8pFHtxE9kI#QDSy*Km61`-4JyJ`8aPfoVF(M`&Tm+iA z8ahbpopRW>uABGLqmRG8o-g>L1P-MA)faRb#M&t{leV6cVh-SyLv>k!32EcCC?gzN z4pcJN*zfvv=cDm+wByqeaApOJHxwCvyBNw%oja1j6_)M&IIAZ{PURml54mQ0_7e~F zTMyTFc3~Th-3V32VIf{rhj=dwlLlwZX$c2cMj+q=f z7-~lo8HMnaQaiP6KW2v2OhK1FY+SuL+jIjg2|)q1VgMPp&*54Zl9vYJ=IenY`~&MR{5p%My6A7dm3gFypbY3^G!7WE>b#Fr^*<+y zQ1#*53!KzeYOQ}L8TKagcOzPU+L>vtH6IVEXW!XpH`E>>FC5lk?7@-ndD`O$lIYOv zXp6adb2Nn$b{x)Z?~@zcg{D zmHctz-7BQCY5bmC*Ca;%%~DWP%7K)h$FeYr)?>$~ow%>Y7ss^~i)<>Myko~2N!ov? zm?TZ5mu4|k1u1PYE`X(7fDN=%-K-~3O zzM2a8+Dfs6qfqA3*#N5sAB5KX*6(?t35oY`-u@tpq>sk%K&*Mcj}AcsaOCSM!n)XF zk?PIi{mOY}z#~b=!#R%V?P1;C#umTb`0O^;y!{7Wj|)*%0XXH1Sn9DhL1oT3f zi=bL&a?&q_Kbk&sDXRrh;`jl+-BDtJl>$dDv^3$K+QvgUks=8=6zbuPnSaZU9~l1% zeu$j1*57Q5G1s?8C*TRCQcF|OX_n62qIa?T85pf-m6L|{G33e27kRu{Uf|aUAY*`Rt1|gr&jQ!5}9(yV9qYldq^loR=&?|sCLiGWNUMb!*bG2`zI*aD=43jy(+Gu z7BkzlY+cKo$8+!1l+rZgtUE$HxRKyX{Q(Ed*_CgLOAq`lkl9gqNX=UqTU;c~#?(Ev zvyV)cKNSwbTW1;VD1fwZtKhLGAq}soL%HBi5!=5O`g@^w`%2=EM*_ZQV~$>TLvj~B z7Dw%|yYbkyQe@HtEplhE#&*L)pmX2O>bR-u^Sf88l{cHrCr4#KEx@-mA>reHO)TE& zLcZPqReQ_L;SS?E7NngbJ;))Fzt~beqOTq08ScdcvDZ<9f4=#uacUZMk~!c zF1!8z4!9p)H65a6Xh?j{=^nsMoCD>(YR7GOZ@dJ9%Tmi-?buSOL;I}7rX-xN&=3X1 zS+C~pI`7HRK^80*{B$C9bCrFzz(;-)i7Rv_egmbz%6Lj3*WK>nj=6UYn8u_Gzu}bO z%_jJ^q|Jd_3YTKaJpE}j$UU0GT1&EzfNtO(xEs8&ow!CI{Qp7vo8`~{Id8^y!6^ms zhQ}YsbAJgup9h>RP<9kOAMPol-hvKD8o6qY>xFxj_H@rm>1g_BHcP>mOHaJo{*Tb} z-WBerhsLax_8^W*AAmBsXzzXRf9uJoC5Ms*2*8)@QZ;TTU1k;g71W+&MOw>?tC!R$ zA(NXf-<~$pJ3NKJmj1LY+gkOvrkdk(2|6>2fBsm2KBeo(4o>K0cK0r8PF;_-U(AC* zVZF@ogkLZ@en@pCmOE`$k9}~|iDDF@QNY_~HyQ6SI0xAZ&#@Slc~Dk(O4N^_Ep4ND zMBnO6X{e(n;!8Pah4d9~7qE=~B_A;V-8Z1(kq&&b{)3})^B~}fvE#Ne;4cj*Jspj@ zWOOydFwx%1+@j5!$alc)-KRxGV6rJj3saE&RBu;!#ekn|MY}a87X&lj1gg>)m@&Gs z;XCr(e|Q^*o3gG2l60Pee=0oJd|facGOd=ct3bLkAnf)A>hWl?_Y=p1_gVZ?hzT#M z(u+JH$pOR+K^Hf>;CFvgwU>ls@Y^U;9qc*`5gHdr-U$#!dipuo&N%vJ$~E2mSU>fb zPtE?aqZ64=xOSxN4;(6cL-0I1C#G521va)95Gj1G9AHC^S73vJ2_VvTe#iqsflL?ikiI7R6 ze7M$yYa%&%1Jn`$mhh=*=~SoK`oj$HsZ<3BZ$i14z579>{qLQg-c2We_9PM5Y;W~A zaC?s4G=B=F6QLO!N27*gU>5~`a5I8KbX(E^56Wek1l%qmY>C_S_<(c1&bwt#-#dt- z*VV!f`Niw;l=YOuQ;);m{PUwQMGE*0RdTgeQlX!~@%os)Tok zka~^<-r@nZm=50fxTQldThBaM$nUanOUJ1@?b{CK>!8`t7CYI@j~XnwvmAC?8yU_lsJ z%aV3q?|OyXQ;a*zFj#o?YO#>sX{_s|!+03+r^;4Tqt07irYaguF+KloL2tRQ*;I*v zuK$O}_ralWe{pBI`aWFFJ!~64hgCga8sA=IR(;0)VN>Ioo6QaiKs**MUU;xO55OtC z!9AhIJ(h2aYCGBU9v5)r?g+rUc`7(*73k3^8m^mPGmLiv=s|4fmX*q~duGm2m&KX9=08LSMo%;Ius%UekRLfN-<<-LD;p3p&Ht1d4LBRw==+7(`%J7{J8`0m~}Et?h09^QE45H(kvRjp8O_6CH9-~1(PKE!y~ znq0GF)S$Vr`IKahRPqJ0VJ=0W44T3_EYusJX^J>6}SS7mXxj}iFYPI_M9ukMe0j-~8Q6j-4&}_QUo0BY#;5eUXBglAm+m*7|Nfb2 zmI^Cdbgg>nF?#6^-g+`6e%CgkA};$P0H4-l32r+tS=!uhT~_iT6I{ROt6GDx>3z$* zb!+7G^eQ$*?{;U$QYf5$cTx&Sc9q0(y3)Q>MdPHg{4Ghq2fPTM98JVL5{b8o6&u*8 z!41&Ou%0mbq7c}$wH;4Wi|>wUgE(W+_jEkTF?#0oyuaIlz&GRXehNx;rw#N0kXPro zWWIN}ls@MKIS&`4V)vV***-Uki|c6z{;t^bKr5$P#cg&}@Y1jT>r*53hdZ}Xx5MRA zc$h_H{^1~ZKU>VuqKW)j&q{9wNJ~@L_gwp?hk8CGOB_>BF+#j1#gA~_ln05NgDv95 zXR^j#YvcuP7c$*ta2T(B*Q~6e=Gyy~;9n4BqXPOZEjc&MgtUCC7eYdTb=mDmo4cM= zw~~r0l8%hwmhaxkz2SHe8*ZD;{;KJHDN3We(-L1xauBryPrj{D4EfWdve43C$E+L@ zyWH7;vXg?LW}HE*1n4eL)JlkBLg*<@EY3D1#mNr;($NOMb87<-KQJbTO^iYOd9}O- zSL6ArYYKnZcW)l`mIs`xP7Xo82mKL1Wyvv;Ba^4x7nD>%RsLMcU>iP~F)~rZPiVMX z(aY@SJa@?vVXQ>?yGo$E^gVi#Y5)$mN}tH88|RO$5pohz-yYkt1iKdEPD{d+cc@{T z!4)+w$ba^1K4KQ#9v)s{KGPs0hjXG6Mi>|91mA=E!R1Z2^^1ih$>x1D;2IoZ&Uwa+AYLdPoit~a`YJ{WiRx=nz zgehN?ClLq2;gFwc5x#fyoAZaW*h(@%UEWE5+6mIC0DZe+=(_gi_ z4A#d<_U3wS%=8KN#dXTHd>B7J%mqW;eqqh4g?YiXI=O==pJtx=A*mK(ydDl#HCjJ7B57?{S3zcxYIr0$4 z!?HUK38rUFtrCQ~Iy*L#olURE?Qft%6!ou!JOws3{QoEU2b zf0HITBS}SYSoHLuH!3+NdxIQT>-9@V$J0iqXVpeyZsu+Em4H`#2)QW%fEq< z$l3+X21j?46IDh5M5_A zmN17#TUf9|2h4d|Vs5BTJ&qxNM(Tsx&XsfR7M_2fvvX=jHSJtE;Aab~-@u0 zN#SNAD@P5D!W%>^2GfnK)Spr9NqcZ(d3z+D{s+bSKB`}BqKJ{pNS_^rik=|Kh1D47 zG&u_I<<0eRM(SU!s&nCQ+lj02c84MW^Z^}Ha51@obYs}!+%Mh5W2sQ9cXV=eZ{!tz zmpK2O`4>UwI-Vk^-BCqB7=9^sD5dDyTDc+u{B#r$w&x`DJulog{J@`rxV7FajceRg z8!u;QGl}25SqW|3^1dW&X>bsUAQv_AZV>uyxzffFrZWtvyB+2i;2YWr0XkZc5Q%jdaHD|o@kaF>t;>AozErITh2#!oBL-$o_Ac?(3ZS_bw?alcwP}dS`jTAV;eJ$wy6hI|W z*upckf<3wwGav{ohxSabcrGPNjsEb3uW22Y<0;n=lSn`UPWgX-;G5DDBF`ru_S0y# zH2Tx;PB-l$fx1z1n=qh6u0tXr8^kHeli#)w+gV$1jRw*HM0sa}J{ z3;k8s9?J>{ike5h@8ZdaQFX$>qUiSg!0JCSP0dZIXEZk|SOV^@eeKnVH$K}4qH@QV z{m!Z%)N)gnJ(u&GxZ|vqn5tl3r$&QRI1pQXNUt7bmT$NG4;PVI1VR~fvGXXN!sokr z%v~Gn+D39|AQKoiJV{5$MO3edbU$*-PA<)^HP94!;$t&|-IDl-&M-OlyznbpB1DC* zb&GL|X#xt&9^!`pNSZBNck#&-AO~vGCtUrFiRU_C+&jifU9s$;!v5bVgny)bo9N5Pu77O3Z-Bpsx=P&fCcf93+l@#5-X}&?eykLhyF82oj z*MbdLm=lMteJ~8ChfxqOW%9r&o}Mizz=~0ZH7$EODjE`% zrI1jseOK(Fu-}7FC=7NR4s9U1)u!tlOrCw(J2i)rflqF@E{ zZe1-g@@17IcxdI-(a4+zEk2V_lq2ZL^wp~>kj}Y#v|*L>(<=u-kvO<$NE9kqfLGB| zwoCd%^4Wy53=5A4N)~jb_(SVS3dOMnAK-7~`~x_mO9}+G)i|!7c3!FTMTD^Qd<4zYixSnFN zia22zl!JYFN9S8WUYR8ktKQ2LzhEQwcaSQD*CF!dRVS>?fA*^K%VQNX_p$QC==FuCn%JRj8 zXqwZ_+db6GPREQ*)mq3_7NbKj9Eske#1lxyXEe8X-G5MINaiSn8O1QSG7cJrMjy~M z`b_h?nl5znI8GHaTq0K8CC(cqQ(uR}%KjIY5Aj(YnpdfZ3p=hv)>h9`CvuQ8@7#xv zm&ydMZwU&;4V`W#-dMDP953$TBZ#I!EhNAq4J|VoJEwDtDSqSNULDL_XO6`P=3y@| zW2IYr$Bp%4BeC0`mGDIR=-aY}jFb6^h}dJ~PTqJfZ%%!!ol}J5Ums7xsDKkFc$B~V z?Zee&qWFF4DZt>JD#|TMZAW?iJbEnr$BHf7Gr{*_E4#Ksfcc52yeG4j z*zv);Z-zr^#FxV9Vj^}T=Vj)DbN7x45%4iGGis43F21$8@gl{Zo%mO)iMz-rZ!uuc zT}{%$5>2#@f6u}t#F+~hS0|yV^lXu1*|l^+Q#(Oc8;v4S!R3nUA=Ym zgHw0tVa2c)(irL_YK|vyp@&-@v@R8-vmB^cxp>O5f ztI$jv{5~*>-Ul&Ou!$w4Ke%=1V0XR1q(|Nj|875wLM_W9!8?R1 zDBP+a$fCCx5Q)l7Cl9K_B?OmPW7@E(m!{2w8~`S94sQ;GX937A98zE18)0VsO8Cv$?&Z2*Gy=Py!F&LU^N`=;&mM!*x zTFAlYpNBT!xGCkmpk@jJKR@2plc|Qx5Kv3w=E)R6N_km>r%(5J6GCS`b>|vFX9B`n zHSP{occK3>0aGw5kwiK6J+DT&5w1|{e8=CFnLkQ23Lxq2^FHibhrbyIw}BrE*aDyG z!pl)`iwA?Z>xRHla4G8AUGR0bY)rlMgKW~j=h8-p6y|BNS@wAUNuH>i9ga--*(69S`O~K7uRA2=Z8*&qE z?M_ZUKQdRD5yY=%Tr5<* zuR?m6IujySzUV2Vc21PtiWqRADzB-tACPYh0kk6_hIIt=`8j;tL#e4Wi&%rL~ zaX%#u^zbETJ)dD+1)I8UrR{VhG}GQ_!}~E}>uDoDPzhNSl7tV0@7h6`Z$Wy({GC_>;(QBe>b` z00E~T^%Wk#z?M%QZi-n0@QL-)li0=Y9-RUHbRFsKtt2GEKxy!5oa=QsZgfFqsmqE8 z0m4r7dxyphr3vu_BU{>!WHeNUs4yJvb_V}g!pGsYcYGJ%gxIJ=8)+`gq;&=29AsGw z^$!f$@Osp(QRLtRcrT0cgYWRM5xd|`Il9 zxy*9~Y!%ldlwJnI3kUV6c!)~-RPy&G?I`v-pj>O>2H4FP)TUnX0O(0Rrzd4%fWE#u@P?R4$$ z?H!C{RD&w5*3?*X&>j5TGr3HRoSHL$(Mh_wV`{@Qx=7s7MJ9Ll*-bQp(7pqpb{+PO(z_bGyfxFPstnkD)Hic=N+aGII_D6+OK zt|E9diWmI=4_bQ^+qeR7=fizuHRY(_<3>P4$~35%oYP(Wvpd;ap>HHP^j%vd{k04v z=qKpHtYNFfKp1QVG-H=KYa|#bikIqFlsg8Z zZiAEapWSe?yg&F%&huS}A#XwYueD355v^}M47-LL$z(c4WMeykI`VxLc4YOm*#ntc z3|KU?dJOr$ZoxHQ6kW)bS=-gwn?P(oh8FkGD~xGjs@?O#KzDnsD@_!npVrofT~2CD zhB9D{QQUI|4Qc%Iuskuf^3+Ut?(1Cj53A&2QyrDOL+_DEb~I(Z({KT(wS~G?op|gq zaK&osocva3kR<1Gq%yZ`FG(jhN#G^Z^x>;A!x)&iZulp0Bk;upFR=7ZHz>MkcgoI-N}8DL=Xn?ZotEK6rQZ556rk%2_kSx;h_4^}886?cM5dvtNO} zeKP-Sbt}ha(@MYHN;>{-ZH)H;V=dd~3jtG5{th;mTLsoG(X_FUI?cJD$K0>a3v&_s zKb)PA-uK;+eDVE2QKs54ME;~|@BLHj8U#mHJVim1v}Rmu6WXBGd;5@H@q`!-bcWar zI3CCjruU^f-Rlzw3%T4%SI2z>WcLTo8pTvyhsEzf=z>3EVCPHM4$SV@ z!g3&c9$F+$^~1+|oR(DPhxWh~-PR3&ItPJmXW)1OJo`hFrIh!hpR&hgtBU*WBXHaC zqu}boquA>Gqwi?b<7qbbeX z#(qs-BWRssf%j{ zhw#^^O|=|WBA0>li+8;Lz=~RXiPY_Ezrm+H*aOjYZOU5-#yZ);ECV>^P@%I(!k~O% zkac4Xd`cF}W^EO?IhAO52raB&eIafN}n%3ZD>znBOq4 z(RQYA#V80pbP{(}_B(H|Aax3A--QAP1s%-wP4I`w^qT`=I}Q(im)qnQJLDAO!U8|0 z#p!m*6Q!&#)pl^J!bG&jtyR3^xWtr*$$K%xBT0UdI^KWonu1bmmxp%y8DYX$AAPT_ z`nsXi7UeHN&16)g$alLb2(2DTM;pOMJRCBeEwpE72-bS$hgBoZ_Nj{foRLkfiX$*E zed&>#hZ=t3__v?N{6zh>;gxRe7bDSYUEP~+dIpKd&Z1)qf%#J(4Wbn z-m@~qSH)D_b$gqpXv$oE`KphHy!X7{#e9v{Bb2d<{^4L$^+{thT#O!8_8()hY0DG)s(O1i{GwQhS zW9vk=ud{5FTEqCo?l^2M@&_R&KI8^kEWP;vYUJ_u%pikw??WCpTKavoe3*F}oL^t4 z)Ls__1}Zr>=Z7bIXqN8jV!l{oKY=t7)@WAOIk-Brr?YIfemNW2Ou8k9HQK2>tcbxH zlko^-5CK0jYTor^DVX0dtIEjm#aJR0?GJpD8fVg}oc!5g^QPev<^y^>YgfN;RRV9~ z*!5nzYgEG!#sJ!m%qMl}pAU|soLNmUvgXw|ZvQ#g+Uax`(P3YRg*-)PP10{R2yGIx zs?@Qj=GU6yl4Lwx-P0DWZao=vqa;hUTfVGA4*wJF8=3^g2&junT+1uZ#1Uxv{qM#O z8^-lUn>)UcxAx}`e7!|$b@rez>p^qpzkaq`X_*}AA0Yc$)f1m9^RMk82J?ZooE#yQ zRD=lXn{PI_Y2@|7C?@=s4otGhsDRET5?tp<`Ieit4-;;c7lw|Zt_xY-d+W$WC=z|< z?^eZQuV{Z^L`)Zqz6GABXGBryABpsM?LMAO6~sypyJl&Kw+8ckj>d@9SV2uKISwss*K&A;)?g0KVg_AyrMPUFKV zW27|#qH&_OxGQ$;Y)I#=?S#90;Lo)x+h}}S-3^b8rFTbxv?4J;xei>p2dIzM{~(MV zMNC%b`HcV-W%k>vBki)qJLI)$?I3524aAknk52jDFCfe3{c9FdUdB==S2xK81$;dP zRQruoE>?5Ud62#W6%G%BWwd;>RVw;#7XsS=rS=I-iAI~>19fmfF(Yig;fvP-ZX~8Y zPbw|Vz+`B3%vf&J8+kr0{IaZ`)pzdP$gb2%YvG|L?Rf=d?rxL$wC%Uxy_HGo^ff7_ zPhUXMH*WmTwP0F|z?rv`82+tsqH_dl#+gC(A7UlAx7D24CGU3Rry_U^O-*qy=)Fr9p zWn=MiV<4DIwgCAW%HMIaO0H0^5n$ER!jq0hK`qnwwIa)F)(5x>bfNO23>`<+5Pd%O zQwLrBKMsFy&$ztriZLV$%i{x|h^hBQ{MZiqIMh@!8^2YrF z5R&^jN4f2${Z^kYmPYx5dB%WI`Bv!hMSPs1u#NxqQzT zX!RP=@_gjAcB?^+q-w)SZr{IJ8NO@?-GLogjok7DQuAQp=jjVj>ysTFl$E*|`N>QX zN!+F5i&N~}#mH^8{4ZCA(wfVNW=5dse(*ky9y?a)O0h@Pjf|iHO~SQ^oJ(mh;@ET7 zCUgh>U~2|tzLTXYiAHJ|hFrEBfAYI8pV8{%JU|GDrA<#NBVWikXU^1S3r;Pgja_^! zBx)n@$fl;pMm*u9*^7$+%EGnEe-1ujzh|tn+uu+;B%RK)SB^O%ITn{Ea3LFEK^uq; zJ~e8Aia$??mR@d+I?rHYq5m0f72ARcvRh1fR_#UVJxqD7R;c4UN;(7xr0s14j~eQF z%khux#%$Uya13JQr5ja`UDzcj-M_soOd1TYWzVsySDp}@u3-Jcnx8aiHDjZyfor!4 z*%jLJ&X7DW@X)MvEx`(Qj5zc?zo>dqpYHni(ZFrJ)752R;GrSVVE@yuB;h&IdlYU- zI<`z;kxh}+{ z077G3y;rd6yU#t1f;(Ho4u0{KMY_a<2U_(^&9+)<*7h`NY5M5eVBSJ=a2HgrjY4ZX zlAu)Iynj7*4yNA-i@| zZb>XAX?Y)Cxxe#%NME^allH`pIwO03&+#JPa^K}Mm$pUrSypZb#FUHy*#*?y}BKLSGHmLR1{^j9= zq?1q3%NVd7AI(Z#z^pFjmL1WgvXAO*6+{&e#c)P;>^EV33$Ll8zBD5B4|gU>bInd? zznU+DMd;w^T3cdx=TU}u&a7)I5Zf?UHiS`4Tl+cpdADrf`;rXJ@d)<`25b{&X-_>KMO#*X}rUP!AE5 zkV7I*rb%9%9OkPTUZyDR9J4qjDm{jm-%sTp+X^G_W4q*#w~}#4oj80;p@v+~KjZoT z-pWwGRDd!q{0v!)38a%hvDHOn-`{J_vOfe*>%TL4K!*Oti2QaFM z$4YDvcmc-$;9%q_nUFT5=}SvStAx@`%SDbFa*O%N@HM{cCX&=9*;|~gtXf5bIH{=Z zLyFQ{nZsOHV6Oeg)i`uU`*o1n?6Q#p-Bhp2lJ68MvcG=~wWo8i51I4&m0XONYl27E z?Wa3#!t*3l<7kkr&1amkT@GJ2v=nDRtFCNk%bU5d2g@BZWw%T4NVzH)0d+Wo0=-o`%G3+{H1DC6T=kO*FG{TSW zxeP*x5sLpfX+eYQ7FRs$@N}~-q8NmpPH7OtuH)#B8hP|Ln%VLpcrcw#% zA}S}z5n*T+lRR@<7&Lzu}Wnf$cy>+Z!fG?^b19Hhvz{~6Mr=w~X zUkW{OyFQonW10yv`3!YWb@pD*2llLR^8XDfm2Os||TKnMR(vhw)$+TL7Q|ZiZ`ZE}% z{&OggjZ{WQCCzb@#?<_o%Iv=Tf^&9VlI`@q6_}lM)GctKVd~nRIZR~PmJ^(*=BK^~ zpP^drKNTaJYkfVBpi|NKotY!B9X;KiCF?CAoMT6U!Y)X zCn&X3CO9XV+0ITo9w=L{Quhp36jL(wF-oy6%cO(pw2Q@8J0 z%{A=m&D59^d)2GDT4I6iaWRbENV9Q7K$1J{Y;~K^OsDxIE?sWJV3&#{n`Mo4qCL;2 z;ckwmO`lW1pT9IE8n`X#>aX6TrsZU5K-9AfZ|O{iBdig+^MiI5_4a=!>w`tz&hL{* zK~Pt`WidZWqTwU%j=^VRNFNm(2cPoEKMuq_^R?C_LpYWys#*Tqo-GcNoE9o_GTd=o z=G@6fR)N~e596xQPD!+3`;bu2%s?8k?2V~1f zTPpmz|^0u0+RuqSnK5DfuLwfV9VBLkVA#5IDQ@}v+aI#t{o};qmdzQo$^*!6A-WaP|9fo3KVJO{x{3PSSUHd zK@FQgg-^uX`38NhI_2sL?L}Tu>&@?_g0Oj)k*PYuSr5?^G#TVs#He>!$r1+ytv~cf zrYOp98%^_zP!8Ha=x6SD zm0u=spYfB=9I?x3e*{`Ms4N)`Fb3aDWku9tp6`{3ueI0(9#2p#(kvfcl7dJjhd@_n7JqN?inICkq#^m9A;DzJ zJeFLkgXV&++T*9C`JRTG-=zt4$j9#eNAWcAH0?ynCH7(t7E_Mg5vCD zY3OAAZl#E28`kq_(pf{&QJ9kcz*%yEeg}XhDx@3)@@SAi>P|2KP)m=W%vQ(H$O{4k z_Q!(SaHu7Cmuo(6)mU=bS&4sa?mpCr=uhg!WJ4<9A&UzEUNqWD9L4`(2n^E$lE$G< zb&0AVXNRGqb$})jPly_kNHRREAcj@o6I-EQ*2yTOP8N`5(f3N9Hk z7$dct7Tx?)Zy!PV*Hvv3jXSbg257o?BOktedA+G`sQg?ql2h`Vzp9}HD0qC{0o`U| zHl~bd)&98A07XZX42OkGGw3KeOobA3N3|vrHISp(lYiq)?_=lj4#cw}nL_-L#jY_po>F3;u z*qOjCY*|fBvy@m{XcE#u=rY6ah@|OS*#8pN#1e{l%t!(egmAl+HMt(Bq?2b9CDZe2 z@1fT>n^wkIL+9D2zE0!9Kr1e)XgZB4CK>;Az!Kjwn*A$H#i(JvquXahtAq7HtUb6% zx;hot96!jx(Am#k^PBPx1c-CrG`d~{cuI>f8?z7H`yUfKm2JkE&~+38>)#``*c=X+ z1_BCx=3A_8&R7UaBN&D2th^-Ak=Nz1eLw-M1_H9);Gf9 z{s;ldzf$F2)S#GthAWM`sxb~<>+pBEj*i7tn>uN)>O{27cT!#H(Mf9#o}J`;ZG3)5 zzJO6n3zV&_VIkZ@qSQgU4+J^~;Tx=MN!1e>Ma@l20f>b89K5R|MQdbs0uN z`)Q=(zFYowT@V!}*8enNP;q?MwD@x2=xt5oCZEArszKq8h6}4_3tPzty28FI9cV-B zoMj6{Ke?8DzO1+RN0u2b&(5$Fn9MCmx1kaJSP5gzOosM0SzI5FLaB|IE|;+JrqjDAd+HRdGd^J)F_C0_i3tj^7P{Dws# zo0dv@;2N4#IUMyC=y`Lm?YG)#*(spE$&1ET`49ilH=QbpTf=*8XHOrwf%jVh^D{nS zx8uPxNi<0iz&qgtn0R>Dey~~mjQ`&r+NWcsH{NFs7?q{dJ|xb%llDGF%fWIe1Ikp! zpt2JL_uCDnh?%;IBIQBQsjm#mE^0a+k6fIvWqs2|qax*Qtz=+Cx-MZ1jtUG>0}GQSI1z$B9BYN~&ONMOu?xtGUTyGuvwCYpOgRX=6;f z?}qD8Fbogpyw8kIG7C>S9i+wDSb7jO2(XlFHVigt`nBkCla0T3qZEIaRa-KB0)kvTb-aokEstHA)>fA-CW+mj^e#xPQ?g+VQ}fU_^L`ZPjgkk_;+74}HR0bwQdG>qONqLLK;bV9ObrJXr`X zcjG2t6apyb-*2y033j%Tj&+YCioxi1X}i?T#SW@GlVnNkIgNVe3*~K!x?)PFtaqcY zdljbc&>xw@)&b%Ra58a(f{hY7$9GfqLZY^-My={$in$--;y(H>)<_<6LuLa z&r{O53tbID;nACZD082PC-6(U@#GJO8G{mPi6%FYh-lemvzGR}T(zkWO6#1|+6LB< zIHpl@F+DR&u$QBt}t?7f?i#zV@bFQ7IYWHxG{=EDi0MU%JN5TwYLw zY1^%~(a3>-xkJGxeXt!}HDL$ot*@7Lxp1iM&G|wzn_jQucdW5UP+8x4=J(DHf0>|v zCFZ{a)uTmGQMdx7W{BY+G5TfVe1iwjgIyNVV+dt#s$$>SiEgbSHDv?~T7#YB_*$w# zuoMp+bcqhI)VC#;12E~J()4}wNn#{sboFuHp~mI4$xlX#Hyres3$m{kb|4GVLz$E; zRiXKzkXQ0YLNt~GABx@rK51a(xt{FOUb*eYC<6IqFNT!j6z-uJaKl z0122K6jw^dnJT!3&TB0^A4>5Xv&%3v;BU>i%g~_9OQqKrn{^{#h}nV-`EY?jZKeq-}L zJ)n-VI-jD9`@_ zf{*6qShwyU$Qah*C~n{VMk<#INT$777?iV1Q%b0Fc5(B&hgG)eMOHDUuebyO4zry+ z;Tgc;^jH614eqP{vW`V>-T2KkkX}e~TIpImLsgKwrCig}YGY^GGi$0VJHIraLZDK` zg4LED4iL;v0}j&_o_^)S)ufygM5IbY7Fcd-5ooSmIec& zH3-GIKDWk+`{RaV?cX0gGPYdhkcfPUVxLy%m-CbF=gJ+BJ&wPXQ`>|=0bcR%U8c*} zwI9^ZuQ;jDiPl$MkC}YM-I{MEnJEhkQ=bg}X&s~`kPcNQU$~#V5McM#CRZkh1WY4* zBTLvpHI)01Irk6p{AV*_xs(98N|m7MSa9u>2!Q2ry(!&@UnV)a${H0cPJ3^MnW5~b z_FvLuV!f#3qf-*8o+hu%nqb32D(}$~_5*r7ev?BGI8418dIoXSM(>9|syiT{d=2&D zO*dKK3Rit@`0|I7G&2JdxdzhT%e_Is_f)y{FI0+_lZ1<~e(Xz71TY_6iaqelNAH~$ zrw#0G0=tBetqVPE#i>Xotav_3ot+O>dacn1FEyo~OK|_9L+aN;Twu`-I~OjlrurMZ zFzNp6^@Y^*D7-2S{^#eQBh*Q>bgrJ%{_^wr?ugzO`#~A!xjEJA1uhDf;BxK{Z!1y_ zEeY-|jWu&ZX=0zHg$6JkOic;Den#idDH4E@T=vU4LP73;4@O*Hx;* zD=@XPk-fU^8Mu1=sq1 zjR}E&-gnE{#}rZXC`bN?ql7BN%3aY{U>?{1KMmJ-Pp{h~{*vyA!s8b58@alBF>^&G z{Vh)_6Rxe9F3jqoH}t3Ws%eayfEouSEMezxtZ4!Jwzf#$f(34v67hMj``F-7rDv@H zHz|QC9T%81r-vdoF?~39@iiWAK6P0ZyT}r@#$*{}d@u&;JYtqx)b-VHJ|TsMT28&C^izI_Ky+hpX>j zsLfB>e!O~)b_^jluxiAtWrx0I7|=`61g70sdBZJTG=7=EGZ(>Cldf){77Eu0g>xTub$n->QeHH1s-525e0S;MM?Xju@HEU! z3C%wagQo<=4FHb+dI5=>e3qkc8j3=%BAwlbu2$b1Z&+6yldLA>ePxs5n0GX<4MejO zE@OA0O2j^npX>K1nyArV<` z^@mrBQragKf0{}+{RGS0QYw0yrCsVK?_SN8pj&5(E9MB+yN12=mhZnup)`>O&ayU4 zrN~8>*IdtKxXQ{&p!AQd5?}Y?Gfm zuuDqH3W=C_KV+`okOMO+r`#o%Y>btC{4A#*K7Oi#dmhb2VOE`0=7Q+KK{Lx;ig7){A2G0Ldjc zHFoMlkIS_GCbjZl5X8WjYwNWCxkTu?=le25Bypc9ApJE^{)^L{EYz8uFqEXPDxp6* z@#5Q&$H$n&$f>Byn~}8YMO2q$r|~BjsWNT&d3+rormz^4yQwvrrS5pzo5YY%p9o4PE^^t0AS{IV1>#plqh z==Euk=Ay?RUW&E>u{}t1S}Lnmnk)PL5CmDusq31Xp`3f=jg}Ha6OTtHK_@eS!Rp2# zQ{uGMWb!-4WI5*Anqi^yn1zGEGrxHMS_4G~RYESThtc<-A@%>WH4E}O-5)oNuf3(Y zuiCcnh*qKeLC#@d(Y(d>JW^=cOZ`)uR=0!1TDWes{5FzAc$!4 z@|uf3@@GPxCFPl=btqgEwwSD|Zdr4d-%IY7s~7W;8HL0jpL(hC7C>K}e_uS+Ime$I zM*!~go~mhJ0p6ghdWUtMM?JBH-=5m)`Gesh_w9c=|FWcvt^bBP>z1~Z z*D6|9m4{b1YP5g$ho6tYN|F9*RNg5?t4%B1jWskL>^wMND-tvLo#-t5gQVt9+|?@tOQ(?VW0maxUyJuaMhfjv3FQVt-{?RIgH{#t(^?o z@_n1q{+N`XF4-aqH3VsphEd~j>Eza1f?(p3$bWpedPVv6+=*j0>C`4YCe62_78D!r z4dyvf1+#Gl5k>k%N8SK4H~)>lT|-#=j0Igroe8sXmh?VPzuIN<@ct&h(&fYs8Gfec zD!|s5Sv359_Wj6|lijNl$Lf+z*5R#B-#mZ)xQfM@Oi?a;|6_l@sf78hl3s(0kcBQU z8D zush|Lff6O`B9{h*^SRb_DveTVO7MOUQs0J^v*(yJX9WnwNOQf4ie+SyT$svj2tSmJ zfJ^Q3?!L4uftjk)#mnGJCDWOCDi<&yHM8pr(>@*je-GICw^!~cF9`><(*=~%s~$57 zss3;RjT9O%ZO!dohM7%mdOfQG4-Q?YVzgx{mZ04pE~H4CheM8rXFA4s?|6Zpx!XC- zTBB)c4hhQz;ag9T<<|yBrso8g@jYJC;SMGB$*}DK7i_xq%mk+Zi?1{SdFsngK9B=E z*9~B{gCjmc88Vh6&|Kt81@n~R6O!@PLII8N290Y8h*)*M+?890Hx3nyOFL@;h@&sC$zOS)5YVp0qY{M*wfszwZ^Z~{X_mTmPs-V2mg<#t6-=i z+_owp-QC^Y4bq*`DJ9*VBHbw*y7SQ8-62SKm$V>_;2ZGX`~JY0Ip6GW=UQv~mAWpB zr<)opERgn=igA!H(6e=PTa$JI*v=;{fJf=L@h|+x=G&mVNDq%B(oavZ9{vjR@rjun zDuKk(K`K3xwZH{>_*^21gHt~~H?n+*%((%|#e-r5=j?`#k=3vulAKy_6=x%?s_rmH zu$N}jm$^vcF_Am#l|OiBgXt?6{zFszcSqB;zAO-s#hXD`8(a5Zm~X~3ToDs)f8Wo)z5e&DDy-Sf;^m zxFf2D!S;?;l*`)ph0J_L`Rb!e;1IDTcoxX-QtloQ182I}Iz1G#^xo=X`R96$s~^VP#FV{V<8Ce+n|YySX{5;IVM!%KH9}C7BG}z92G>y#3^_@> z(v{CcSZ*YnbU)Ixxgoez(G_qZv>GnC20&HXlREf1uPFS!*JD_z7&+=f6#juU*Ydb~ zwA`XH{*|7n5Q5gTnF6ucqeYVQ+qU`@$yw$R)jJJVOuIEt)T(>0CJ+7Wl|c9f`KHWA zAI~r8Q9?L4qy>c@W@tdC;a_TXyT`hE4fN$Dh7;fcFW;|OH8N-(Bp|t0)MvfnVsXL9 zSB(`^RU|MLF-$Tvyo;jf)yCB^?L8=o4i z>*S`q)vR;gnCAYKqs2{wEeVe;4=aOm7HVsH(?&iO4L5;U*q%wB;kU?5~*M@wFG=&Zxg)^=PPTxK;hxngFZuBoYB=Ks_6(XK!ecf?Wo4DIZ#Zel4!wl-=XOT_xcv%=o zfjpzLIl;~3exd!jfP3hwZ5yyQ55K_HI5`M{PQvcyQDT*5IoMPSpNi!wqO(RHyRW5xJ$X`T1%Y#qI) zpsiIf)^F}T9L~zI?Jio;A0K}B%d^YoY%`xWHY6R*#;Jeo_;DS1^lbd-tM9q0d$z0b z9+Z}vO!y()in56m(v9=iPl(0$EZ>jfH{>F!EERtM;E`cH_^m#4ISjp&v0k|A``9Eg zNX4_RDVOETOhPGfM@_kr>olLr;2;0yj8(7wlp$@WX0mZYV9rFFeB{X4qWCI%u?@MP?`&UIa zy>`6RAuBZ--*Z7$HZAQZe?2aPz^`9=f3z}NH0D&d&@cw)p53?2GRR)RF*f|lANN~Aj70yRURt>gf zFd}TC(l7~mN2dZU))*W#cyOd+t3n|)v4%7Lap)W(Bc3USv*G@GQ#2C#5v|o*d3j6q zd*rz=5mFF1__(Jl`xXzz zvZMPJR|pfAgsh176=^?}sx`lmpS?G|bQ6_VCQB>x+iX`PsftdSXZ7?Ehw6--obwqh za5pQFS(@#SIGo{_q`X%<(BZY%a47y4WF8yAfZM+=W!u6j_vvPLj34E_%c zZ6pvhbXgJ``4FZg6DkEHP;m(BTQk;b`zuH~0&g~~p*1d!Tf?X=B;(p?hNJkU)Zrn% zra_Fc+?==aTI#}^#XX7v3{pZZl^Ai{?b`2zTKu$@1QA2*{mx&_{S!dj$w>l0se$~q zEe$`!XfKqW*oSlTDMbK)SP;g?!~iW8aW;ow-7>BNtBSr-rUS50on;U2Ox()Mz_nTj zQ$)IMrD_ywVP(oFa!bpYmHz&Wue-K7-{w4X{&XRyut#_0C3u;ytjJUDSYKUt1p6pK zb8fC+W=TlgyAL!W`zNy&{r9vTpeNchSBQu59YsO;`wwHDxKiiC_54bQ^zTh5l7;?- z5xV%>iu?!LA-&7Hv2lscGT@mh!Zi$U_3J|DpPw2p_|&wb_7^n#%a%3PtDAf|hOX`EsKz`hn9l0Pt}WZuvB zVJOT(&F%%qagHP(@ijxV?pYf_UMauhT`jtDh2BD(=? z;=re2bXU7hut717(Z;aM_lL2dE^$uL>2ZmG9i7XXq`(hbrZ0w#J>pXN7rQiKeVoix zyKl3Y(ODG_Aj=70u2Y?x^rm^pA|xkR4XQ4pMiI)PcY}b$*kkwgzOi}=xTD4pfxim0 zkl}Vx@fIl4$d zEpR}`*y=CL|3s;r{^0X#@LJE;ME|a89^i$gf10V)FbC^vd<1&s8l!u2-COx`Sv^X9ym(-$}v182|F`y z4}S?>sG}Yc)uK*Fh);>6rZ;>a`W0gvpsuc$F4{0hhMD9qtr!Y(?U)ZM6R^w}t~*`g4WMyZ|lGz*La zyq5sSON{Q*@*{9V(hHI$A_88VKA1juGPH`UACukapN%t}_+M&;m==#%Oramx1 zp#A+yL9k_}9Msfi@nACC*DgbMRDkm{(CfhFDO`}*c+FJ!yADJ!LKTEl2iebN7X$5= zDbT+p2ic$}t+|{)2tOWSovqVLhL4YG*F?<}9(i3|eM5nMF#S85X8N}Qy(w877bb|Y zh_5R{K#%Cm0T=n1>MhSVe-M3wgE(Hy1s|l#(S+9>43DAwj7CQh^AHn(mP!j1EuaaU zfr^u~(La2q{o-R@(OfKa5sAHKP-MJ9_z45%1^MJa7!)D~SXhN0t>>a^PI&UD0>MkZ zGj#T$5eOBdEWaoCPBXh>(ad67Q2uZgc<+%V@NVJwO-Ld!OD?ZuPs9?xwgA zz#xQ|9dTu-XEMmFX;_QMSfy*uxjkBex(aMp)gV_aJJDC+sTY_uZbJaLsOuFCn24F2 zt%U@U8jv}lFcZ_i^3N2mi^t@=l>SD>L<_N2NHCb3Y&ea-QEHCFpfJo!J@-*S?3*$X zG`;Kp4AN9Ic~XiZpM1LqzxIw`PB}`x*$3BFcf}QtG7djVXOUy3RbB0!DvZ{V3Sx=u z*xb)eIPc+POlJk$!M;{%lw3%s4l4K|U^L%8-bJ@OPlc;>CUOq_#+B%l(!xGU5;wBq z>uSQfcJo?LRED(^6KvEd%TuTy!HWy}r`gXTSOn6hA|+0YT1PkRVuED2;8_@ISs(A2 zmW|VKl59Zps?Xn?Gkz}e#3B$?jjE-@B`pZsopBT87RECHoi-l|sAn&LKUir^%5tj$ zf*4qq+_vX79yQh~&910d?`?Dzg@SBZ&WQP1wk0r@G-*qPKkpi%JaX?kA;!wGq{3*u zJuMtii;?E(2wEQn^?q+Dha{z95toi+V0*1$i3nbTvC*s$ep|2E9s!za?Ol}VU5v}s z+iU}J(!eB}lDQ8nMOYC_a)=cc&h}bJUkwohiY1gEABf6p5kwkrsMh5o00~aZEunPX z=k1}n;Fh}9DTTBT>gk^f9jfcg!ERk#jH$B(iOVRyRxye5E)Mezu%fnKR@>8a<>+;t zMLpXZTjs|Mvzl~;b%!eD_&RE2Ol?*}ib`{poV}lX+onFbO(%OyzT4L&KG;*IlU$;& z429KhCb_A?c|4%lf|-I0lhr(fz?OGOPL#%oqY?A6+!_?c>Vo48bp83q_xqYpFkF>` zL5ye$h#W;IWQycJoYp>;WbyJm50(C6(-8^OBfqBz1|bpAm`I>!YxuW%wTNp)CjHZp z*<2^Xo<$HYSr&g?`s%D1kSI-EV|@_cMGDg+CM8EKj(UOzb~0G<^c>?z5Sgzhz*{gg z7Y$D`^{9hP)}G^O&lGvTrgY)&|PaU^3`BqBmaoQJ2&rq8u9VpDmH+|Djgc^LQ zPd>Zej?C>9{Z+r-b?O-wHc$C0jkb<19f9L`n?h6!&y8m>C5p3W@eT7!cKxYBz$(6e z1ZG>yHJ*I!a8P%c-sA``_0JS-dVu8k4^=Rjk6G;nj66wpN1!gQkI9hfTRw>LX@0f6 z+w=h;m17OBW^ZGk@jLJcazQ7LD}1J5k6P(GQi480Q5f0HsQIG5YcRs!j`UV)7s=s@ z=#U95On?VD@y(;I<0pwd3Xc%ej08tT658_~LHT~b)qAA8M$oAvku}UjIyAJ)E8yFa z5)#K#V?z={L^zwhlMP<6TF+$n?lx>7s~>9tXLogo6@Avn>aDIH_obC0$B6xwk2>L} zV(!gUM%AQN*k^hBqF-k-W>(mGghPK@7C|P0!dx;u-MO>>`CFx@x;~;TK7VGb4aC|d z*e>Po+-FVRn*z5^^UBJ#O1oQT*{zGiTu3%vlb>IVQ~qXBv{U|UCUA6cswPg?VuN*L z*4JXnQ7qb^UT`r?cgAF9u1ajy;;U|t8NSMppM>q7F*$gRVXvuDUr>3izBq&@u!_J( zip?ih3A(TZy}=u&>LhpBOJ?zxC*!`Tlx|y3b8o=Z?O76x1kA|VM|!`$IjrrOvxeGC)%w`^>5%hiZKp_;sNSbx&vy~X*qYt{oo_?4V+R=@R|gqxJ1J2+sS>ktyye)44r%_y+b-kX`a%#N zbDg>ft3A7sop|pE228@1m@Eu!Ugs0rlp?eGB2PZnG1eVrT!I|Bm5yz`gT8C;XK>CLXWBn>PYtz*eHiJZ-1!v||3wjPeaFmy zd#u~d`lD`#Vap+7!9!&J+(uH&MrN-e=hEL?1V^RODd?IVV_>%A|5C&uL3 zxW|=3M6#q0aqf@SpSbES{U!Via3a>LtJ0yg=bK2mZf>W**VX#mJG&V=bsg?FC4T%m z1AR7f3=`2ZJS~6%uG#Qnm)XxRq)7H-j03I-%bV2=tMOe=i!n*>P$-n|_l z--Y3gXhQw(3Ln6M5id9Q!`?6 z)gQixk&PGzU4^6nEDhN2of8EGHY1pM4rf z@Ickc$Ag88F&Xuf)2LBNlhQ_od7XPMlF4YqeUApBUT9)0(XPnifJ^C(CWi z-EXxvz4k8CH9IUx{6lscKl8g;ffRWz%C5hm3D+KD8#k(cnWDuDUl@hbe4*B#2s;5(UQ27ty3|$&KY(hn~W@QaS zusco7wz5un5r~kYZc@uFUrlqTmCOJ$A9X)$Kg>hTbmY4B2*-NjNavtdXMLA09T78L zZj7l6EKq!nt2lO6;srfv<=A+{rwei|vS!a+#>vxAYzF@>cPegh(5TS&`+iX07&!Mu zk{&_}-(3CJHm66T@pCF8_L?jBk6(J3b-ryTZxu$BSi-NYnlCbF>j`7Pto>Ux%{)%z zyJS8Nw0aO3H0$zMx6ED&iTXj!@h|N}VcG8EIC{Hn0(4L8VRVPgqT~xYX(K^SQrZJiz%>1kGy~|K4!i3Sv6k?n|n8Mqh=c%s19iMkWmU*j882U+CE17 zDKiL>;}8=Hu!-JamlKdD(du9n&`Um*}f$2qhA9l1FeYpJ&UutTwmjQ{9^sZV?+L%#VaLi zwHT_GNH=hOj2~Rz~UAk#stU4WjL3^xbK(GBa+B@WgwM5-Ns@ zd3DkaNoJMyfhVg(i<}M}PQ*U3B`QK^Us5TwTuSsM{-d*q-2= z@GC}qJ${5%M5^SywT!hlo8t2}WS%l)D?2xi#E!dVt+#&U4wP&PoF@AP-6wF(e~ThX zkg>&fEt=A>l<5WRMTvv)y-yT5nbXsD?FuW8?~eIzLZO*5II=8~w-QAaT-=qT=hmpj zx(0FYjL1I0Lel?0FyW8wKn?atN;rga^cdr5CyNKj;Ibn9Mlo423ry4iV@H}+wS`e1 ze~lwUf8+w;LJt*3_Z6B)-jN9OU~qK1c|)BA)gZK;2Jaqt^N$ZqM{voA4!nYgKW;f# zAz3ZBiJW3qhAtVV6R_>Is=aSx?wn+dl`I*s2+~2P>B9Ncc~Uzal4Fn8yGXC6k=eJ1 zy{2KiMy|V733g3g2@i&lD?%z?YCU0b=$I3yKG4`9b3<+JJiOuLL({eU-Fm*8)e0gC zEwiQyf>d?lrG~X1TPCMXc$s6BIYBGLAa?GbeG}+3{HK-xP)kRVGq7A+Ff*a3?Y>p zUqqn59F0xEJna)(Au)g@lf~;ZPL9oRxSM&{(W}4Zn5gm^6BwX|x|=$Ewa}SZN{7>z z1nHNXVI?z5lAU&SJs8?^03dl zP9hBFau-MC7*(@Dap|j%vzSQbfcuV#55zeQzT9kCx~^_7YQ!d@{;M8Pw5ykJ%$w!7 z)M{9DMs4Bz-vt}si_CX-Bt>r+`Hn^89yjEEjZ%6 zsPshw3WbFFsQxD2e2*llp0KJ3yn0Sqgaf|S0Mjc&_D;uYl%fdl!fjX}X76!G4yT64 zyjFe!SSN}v6#K}8k?S{7Fszn7@@zhGSsA&cLoFraT3;#K)FMTVu#qCZLt#pwT!-z# znE9XEM!{pAV@jFJO&c zGHgO*(Xk_S*8SAMA7!YKpVbyXPxWg zc(1jp-M;67p670Wov$xcZ(?k9G7hYEh1WZuCTe!_%VS)TDYc_tJHYCa`jO0oT7W@n zAzE}0)n_gFjJ^E}iy5LDwWYHm$9AV%8Aj_$z$-3CN$O`JsS{3qM?Hcb_Ge)ydA5<{ zfj-MXK2gO_;-j=Ro#Z<>Rr$Ok<1znZLJVq%mmf0!P1vcUpS}pKP5Om^h&yrZSSf7y zmO~wyyX889T3ZlRu-_5umI;D?GEP|6dj$4}9anb67_ibY zbYfH)u);A*?+UgU6c&9?*gerI&)%h6I z`Itha6vjgn3uQL8878kJbH!NGNw&p^YhGwQvE=yDiY)k-wT+qIvsLR;P)5h%82*(P z@QR*%@2?nYy+C&`7XbB{*TZ)mwc$iEwdF&y*c51b5j)cmb0RUx)#!p9gR*3k69w38 zNIt8KklWc_zK2lm>!i)u?2QGnRkms}c5dy|&1tisdXoZhhDiYK;6lKJd2x;Qrq=-P z?1+9t&9Px>+eYVQHZW=nnF$qW@;!vn^OHY*a4_23pF`CoQNk3f)- zXIQzd!{b4>O~1(L-p?7-%Mr@UL*B~^sLLxnL4v-oKaQ^tB|^ooXws$xWo1dI7UK+b?&DD>Wv*eWATWfJXigXZLnsE+W7WHlP$i)K6U-MVcA5(E-FFY6(589gOAbYR zCJD5Dr|N^vkTggvgfMFgXyL~Qw2mk6KEOnctU~dEU@W(Q3K7#bWa`%%ZSSHtOec!y zt{_miXsvHuMWsEL z@i2V8C1kb7IhHf_6)PI@2m!{OJ*PmliN$^G(zr26%Im#@Z#L8H{ zpO6>=j@3S=0=sFe^K_`>)Loa>!Bgb;O|H&$i*``KkE!l+zH8K_(!Lpd?Fiv7 zmx&t9Q|qlhXT# z4TP(zs4Hq?Ux`83EbAbQfqZU4ivIsj5&A5sO?eMKKxG>y_MgiPo?qO*QKyyCbDG;$ zG5M-lxX3ZS*kEM`3_ePRltVs5%V4m%9C>l=;3cI=nNQl7a^H(X;W!dytj zf*7Rd`e?+RrdPYv)4*tQy&6VTBQ(94pjm>l;Mm-SZ<1(&D<%6Nrm{-Ba4==(I!$~% z>Fa6LFDGq*tJc2cmcYU9UDkG$g~~bSTb7a@d=%bJ<0ZH35KqjNg(#IF51Jtl9Jr|t(>FluGZN5v^>ob@@5Zh_U}hsr?;um`-0m)6mSE&#DN0hDsOq|6 z6)cZet-;+LS_B0E5)Ih-43+epcHaYP#Yj!5j}2DSC`X6AK@_3qIKtx8YeEc zX-2%G;vi)fy}*KpB+Q83`rIFjV8HkWO7xsKe!n|Zq6oGBr2?RXK8A`72&Gh=Gdgq_ zug}ZeBKoG)+ZXG($DDs!u3)KQwhgyC>k3$44R1F?d7hvVpaw-c)%O_C2H?26KZE>p2`;am&T$C&qSApbI7pz41hL!N*{{>-xh z6t(}XN*}BMqoGq{1^Gy`dG*~rU{Bvcps8Z{xmG$oGeAWF`{t}Wep}V#l(>tQhYV`A zsO*caHDFYW;_hcD^sQ2F9W^kK-prb*AHHb?G`*sKBdTX$nFo+be@h_FVE4NWxTqrz zM>gEa7g1YXa|A;h5&A+pOB@%is2nG+aFA zj4#sbR0UG?UuLP?h1ax`f#gidvOub!mxkY~3X~Df8ldDeJ0#(&=6rb-W%?(X;`u`s zvhv6n*wOM^Ig|RHLfIs~S_QB>mR)oh=I9`)@o^wiN-l{hkxfiePu9;t_RlWmMWUfJ z^2V0;f*HRNaf3ZM}jOB7V(KfI%Cv*ouIZtfsz|?IqYsv9AK~iP~;+UJc`a zbsWW*%zD?tqb;E;E1yOWrnip{A9%DrEI&6}u`J(Y|j3f$>x8)WI-njShH_B=>cFbeJ>d=v8hF{k&3J(fc=9J!l1s z5lfs?TfmTNz?+%sJUd?C<(e}~P-&dQDQ6ID77SszH&5Na!E5s{b*;33Mc~VN-x#|` zS(>$poB#13NpSszNI%%;gZFYwXOIVQ#r|NFOHyqw*))RqVxZldk5^=3s{p@)C9)OS z;NlD8^q6zwmU|Ji^biJ<=QsbZc$#2z{m1@1T}nr(fl&hfs$2Cse|P~QPw{G}_| zx7Zj2WtRZI2kR%Em6b4fJ~E9SK)mC?<36^vYQ3^!wVy3UbD18&P>vDrb2PZzE4zBh zb-u*QZX5qD9IZC+<2H4bOjaiu{~!NzM3n*rH*V@=N5j9H$l&zQ-6& zfa;<)f&aAu>Gk9S|G9AfqT${M{|{OEXt9Q9u?7i+2PF^Nu6hmhVQxq{W<&8Bt;wI~ z6OtURJYCI~a_hHeY4=qwJY;?uiNnaS>JXLN$Tf_G&waYlZ!O0okc#BI#?se_w!Gtj zvr3GIor6WA+=a&O2QIPKUCa-B%tsyXkJ$5~(gLnAwQ%Nhmk&1Vz{qqWe_mv_41fS5 ziOp=j8=L4k{Od&MzKm$2dHa2CZls1YwaWpGj@N)$W;aX?*qy-p6*YjlD)6le6_(vj#m^(Ar&I4{1!*;bvGXjLr(Iw6ZnkwNKa^e~d54Dv=iksrx%HoBh9XY=c#%8I3fnW^}hUTuKQZ6jO- z9%%W|tb=%On8ZBKJD3a_vhCLwe6I;g$M5gmas2?6irV80ZzoN37N2R6(nha(aWQ?b zk{vtA4dr<#i71JM#S|kzC-WVu;d!2R<@5?hOp>i3BD))nBt3q~zIdv{+e)!u-MOOq zGepG4!U3mwSdNPMj`ySD_4rE5G58E@10FrrBl(#kKN+^i zSkq29^YI<)sE=;L3ke8lMx@YD0%v4m7{QGVEZ3v#I{+0N!fJa(+(L-g8dG#FzxfU3 zYTcVJzR$Vwbe~O&`yJ7JE+Jz5>|6Ki3Lvd#pa9BE!DZ6Htsm|NNv;M@XRvyHVfLKp zi!Fg}QY()&t#R36CmQ0c0zhv?21_2MleqC~YA?rCM$75rXefnws68)}P05U}C*x^o z7e?j?n)IP7XTX9E=3kGwmpU6ys-MnS>nU4hKDE7X$JkaUJD2X|R}~zv)-z~nq}hLI zO#Qc`lNzR8KLKDXhM`RlSuHQW3h+x&4z&8m7jl9OUk2EbQWh5zid%{ z;NN8e?s6F=q|A%FOLHNF_2olB$%1NsD~0D}%u;y zxyq@7BH6wFGFeS=?8lAF$|vMk6=p#6^wrLyd>Qs+7ow$Qle17_4deRlFMy2?C|9Go zdn-^4Rn0mn*;pVC|$`LkUWnhm6OD z?o;^PAB`;F5Ggl;JRzOcPU-iMuxptbj#;=TB~|9B^+Ez&Wp21X zv7Rs8ie7P1bKQOp2A@ZkStppN>lHe+BT4T<08L=B^9uz3PpL1AsQ;eamvZLGF4%XB80EXl}wI@W-7o z?CweQnOfKdAF1cPeg4*>)X*#=WiHd$4Q3i%yg^|Da18Oc{SV1%vA_5}!16rsJh)Z9 z`e*Wg{<(wkCsjodZV&QE9eeFnlt7m_#alp9DsRxS9$>sRw#@G7%{3co>AU$dCHE!S z;i`!AlcVjgs<3YiL|A`*M#o>}S6^8jG2^^%ZX`!kB(%N$oO9%=m5HV2SS_Sv9rzoX zecunrHT{iOa}K*ciXhCI<~p|Udg zm>^59N@f?4{61KGU4DQbz2eoA{6lmR!Dvq;^^Js*T6&2m6Ky@^xUfmoO;>`i!Ih(x zsF9!R%4x)};d=7W#MQG?BTy(X@=?L#P%R5I_|XC4at+2GQA+~j8vO1Nk9oT$_mg*K zYoe#-GFqpj_u^>Cg_}t{ACm8u#(?HgGqA}<- z`dT8FEez$|9m+I~bc^-+i(Eo3WGFmTJ!hR?L@uxNd%PHhyHCGM(aTV3m{37U$0z&CV#H3H z4_UP{6Ft6GaxOE+0T!Z_=KYruS_X;7P_i`onl~vgDxCj?P|FOy)7vsmz{oa?XKX;@ zTLAjD{H4#_t{#k}X#09vJxI0#G3eKj-<7}1p&suaOfoEHT2^~sUNMf;ahpO@o0YWD zOHpwQjRBe&l4}w@sHiOv1Ti_}s)A(6n!&$inNgewI@s#hh&}>i2>^*_r1Tzm_U*fu zLXtE}dd9tf2L$*hGD}BZcU1a&4LKx`?2x)y^`olt<7zh7MA@FhxePv#g6E^k##m^% zXsI)pGwW&0!Je$?YglxM!796!zyUigw|fhVa<2?pDl)`oR7kyH@3(MA65R`wgyU|R zVO8KNan_uY`x^3u>-hi$Vdrc{x`}y+ETwyI6wvYN4}_-Cd+oW z3up}e*C9B9(2|dL^oZx$Ehy#JOzQhCySp^H(l)!XCvkM(M1VN`&A zz~U3uWNsIAk2$Tx_R{DHcZgmmE1`tnbdfN!h{j{kanFlN3nj%2fq0f*^uv36fUgcr){YCD?UCGP7T|47y+pLLC6>+`(1IDxbML-?MjL z>|BDv(I=-GC0&y1(dFC*aP(o+?J@g10kQV)t;$UZ9q3y&+x<>(ox&8J4zwEs5p`+~{!WW{oB(@^uU$T3f9CI)n(s0eFSPd^%>Nq_MT_IXhM zWlRnPRjQ^N((99_Fkid9XP!MUNOK=xSl12X4-R(kq0k0%zK;C3DpRDi3Sy{mP)E$_LNx3%_DgivSrm%oFO;%x!=6AJNMx`r1C{Su^>|>T$uD zw`M~ZYAc2o?hDC%maJ(xXv5bLYC@-pY8Nv(yQiA1Iv7AAOPiWPQ332A; zrc0>nh9n7r^O%ux>JA;3^>@i;Q|4RO;9Je{DiS)#3t&X9O=|i*R+0xbQ->Bpf!P%y z-=OqhWqBUlUtJWe$%o6ic}!(3K{W1230p^RbU@@d@fe zYT#$k(WPog*nU6!jw9>+C5ESF42%R!zp0mG-nsNOVT-}H3HngPcb{H7&Y%K45%3l} z20No4+(WHmZWm1NX|?@Akj2z$dzT6ruwpX-rCV6yDtN0ZBxXZ|kJ(Zbn|=Mkn4hbZ z2(|D-YJmBb8?4EKLhmBAZPahufbTQ1VUL9L){6N~L86+FZNlCk>(Pb01BIJKhxaqI zsAN}Rb?qezey8i*|4sVvNghm1@erf6k$A}a6-V|qiT7U}*bMt`@CQRh7JwB~#6mGS z3EcPgE>TWqDM}!`e${UQQckpe$cvIuz0L&3g#$IxPuD8cL4hJ${SQAm4`-V00<9wi z$^%=J?RHc$iNy=yl~BJAUnhtQ#|`VUO~_uzcC*dAn#JNP*TYDleyr8G6os!s zS_k!=infy^k1HnY5<+1~D*M zR0q?yj(Nt0(lPM<6a3gBalA2^5RFbfy5Cju1$9@PfVb4o=o81h1i;sw4;*1FpoW6U z(y+FZd`RNCB`n&Iidr zrf9g}-*j`x6hMlv^b0}RiG*&=^QQTw)sHjyy@nd@#RxU^ut088Q7DqT&jG91sJ)nE zmWKgp&D1$d9sLosmZNDIWqajibJZT16ZCnnIz9;)v*Y{bKZIM5#b&13iyaJ~&cJXgNdZ#2VfoV2LPzrTW`Rrr4hBY{@+V_reB<8)Y>PdmpFvnd6fY$bqelLWo|;ovq=(EgIc8MsdCk^KMv*GgeYI_eQ}>@ zq61JiymaxoRMAa%LWkJL?SNn%#)pgD#N-}MTO4q>}*#jaxq^HX z&v#JyN`ECUPdIbAeZ*t&oS&*0CrXzs?JW7Dt33=YespB6j2A&+WsK= z&mnN$09cWbi*CotQ0ETg{%QxA{M8JU>534_`%TZJ-_F zs|u2$fY9#0zB0ZVx+m@;@X+77$?jh0_MIZ&SD#%d69=`v)3# zLj=+XMn|*1p;O^%j&i(w7aSL%{;s%t?=9?#Ka4bv4C-dBo;1%p8*DV>d7tdZRnL=^ z-?%sH)=$5U&z&D!fS+D#DxO)}n5?MqcAad$Ia}%QoBZ+wa)A?0l}=!4uFUmzd8(e~ zQ>c?)V@SND;yo&TJdIZ)-)LS5kW z;}Dy3@!%6$C+;NY@3Y4fB;EyKS(m5yh=_+#xIp~dpItYe?-$Oe7;LH=+Q;7<3*Z>0 zo!0h11;`J;Kin%kIHs!zkh5p3j(t*1pvat8%1kJb%O}YzQnXh4&VKYcJOg@-apwE# z(o{(%ZuDBQc{}Ul^9=4(d3DL-V@kO`Ll}%|rHrTe1SYR}m|aA>bNKr1o}pvvX8BNE zS@F{uGKY1ea9!!s1nVdFUk#@%?4k6o>G|~ZvJqNu*skX&AF9@h{w`yeR0^e8ofzEZ z5BbH^{`rLiqKs)XkXR|oPE&tyjQ!p`Kth!8(|v0`FSmd~hL7_ZgH{G~l>H}xB>s|5 z{pnk)Hrbw$rWmWiHYW6#vLe|0BP1`LDtLrfKVr|v)**SMpfsZHx4SRq6ojl3Z(hq~ zz*e;PmHqk|UYotLvZ_;I+dPN4CaLbl+&d`E<Jh`S0JpxWGBt(HRpdlJ`;Fzn`|`EK8~}2QV`(drB&ih#EYoHD&Oz72LoEA zI^y1$bCL82b}{rdM2Qh9?#y5Q@Nhy`&M&ueMbd-~bJPryoYmrHo1-90BXs+HPM|o9 zxsRE%x=L_;El|EX{lWeSBNAW}CRGk(}p zd=$b-jhdyT*ds||Ww9fSXsWEFl7Y3|FMI3STTL@NeoVwYlTjy})BLYc{ekIMlBw#^ zxKM=VF&3iu#h-o-SM9)|)EW6x%7C2mbkO3ZEu&SQ;*6gBi?m6}-U>$KJ>FE?OUyJo zn1EHSH35cNL>b|2G~xGESTb`}I}mD7$#9G1#!BO}9&>3epIQ=@x2CqOcjpth{Ih`O zZD{IwS(zJsfSvY%Kh^CB>q#;yR>sErC4FUCs!_nU+4}HraaMf-r{;!sjekxAIa5*{ z}$l^_;V?>(hcU}Bl{i^E8ylg2ov{m$d z2RHX^-*cZS>-$R5LD%=lOp+XzPol$rm{K>WVBJOawPO$S<|ucqxUFbUppY>KGx1Iy zV^GLcmDqL?OvM?0NSrU3VN=ktb*GHtm4#~@1pcd~e zSlUhZDSK#QIWJD|L8+r&?rA-Hk0f@blXI&x78#9q*{UwA5?aIY`PLnu`#y$jhQX$h zbUvEcbOpD;AMS;NULr2E5T< z+Z+-piA>Fsv|!OBjtb%O7a+a!0IZEyG(qMMm*$!!SkU)r}pyB~RJY)6Yvp)6{D?I&D|q z`49lNDlWf5K0<#^8QNx0=;@f{`(kkxhw-_|jrXT)x{Ay`_Ujg^;*w^b1sxaj2I<{( z|D&Ui58U=MUrw>+lBf%eXV;oC1SF|FglNJXJusDU`h7z_j=djh^TW!52Tp;QF1=Z9 znlgKMnvpc3M5ab25mY*jrl3PDQx3s%I4*RSArbe>cTmRPXh;f~ zX((0D2PXoGk#JkxaCMbp?5Jrm@Kw;a$F+)&a<^1-8te~xr*0HBqWI~!jP%p*>%6wt#q`Tn{@4I#Q9S`ealBI>F^w6I#DTfMg+A$l*-MGaPs-V-%y^jH>&PAm~! z@E`L1pYwZX&*8jzGjs2qTR$_q?@43hMGoRybwOA{eYJ1SSY011%>9D#NKCaC7-7YHhQz@LdC3h)Skts1=7Nw;5YD|x&lI-V=TLjWxa#gg z?=I`^rwQ~yl`PVCz zDDxU-e(kX;g&zLv{i|%vPwJ`xv~F+~$hc9dis^=kQbIjzC3YO7Nh8tZS#e=SiEl7F zLqv(a<@9*RY&3$+;}JiB<*`Lh0D)Xg`O6w3r*-X}3*#dbR*@{x9Y6k|{+S~KRuPz9 z@ljSogGR=K#Qd$8K@wAP}W5TlbS|bn-?PEA6F)rKau;SlZ^Gt zc2deF_i|rh=Um7r$45Akb-jtPFd}JAxiBm=Y+e#pT{d9bQMBbNWd6Fx@|Ul!!H%t$ zG}0+x3l-0}|NOCMI94JDoZVK-F$)-Ls4$AR`7+R_8cF5olU7D_c}NgfVq>1jk7H>rr3iOLdctALQ=!oIHYy19gX(#pZXXPdv zOt?{E{txpIN!K~E%jXphLfsr;Ok-Sd{^q0iKMv19^o_eW9nMsDoc~T{TBakk<38|TSy&rU_%v+=~L^rM$xqz zFk0Z6XM*A1CzDWXM{RV#zCpsA z-m09O^K5;+;QOzX=IT1XT{70yXKusuG%lqrXm&Xfg@3fZ)qY~UEQEjA8gV&b9lY%- znrCce=5;YvKw7q0o)}rw!bDJgboRoAlNp~+mc#h*3ZpE_{fACR%V;oq3Sd{`jHT)a z5U!=#+quFQX+vRTGy>I66ZO7ULt=b#(Cqq%0;|u9WRb7`_VpJeDXGbhlyHV}9Ct&drq=!6(DYi@(jx0TI*m;!Kf{yVj-7*(gXm0zY&Z1=o2;#LCfsE{3nn~|)F`mRf zag@SK3$t?mHMUN@Np z6mw4JleRQ8W7Ag;Eq@V6rsCZr-@?!MN~==Cf0YWPJ5=-5vhdm}^=BFt z8qs}b^R;bxW?7mN7TvO?Uu$GID}nQFs;z$8^g>WMS3O zm!~)~^J|k$>_>!h)kS(!PNWVf!5FQZxAF!Z%1XrLSsxvgR7hEQwUu=@pS`s-d&5qR zP=+rQ5?kQ;Bpux16Xb-Aa-!59(F{Sas^~)lKyF2gU=J!42ewGg;`ZYSt6~l-kM=r6 z!FL98toZtMHAU;mcE>k=HZ4)2i7xWWvM$e|8Wqm`o*vJgckI_LYX&~j>FCZ$A*4p@ zMyvn)US4NstxUc>{ydP}*ej~+9FeVG(g5*4yk?$qp|Njc>t%X~U~uSQ8?D26%p1aO zf}c_uzp-(gW5uRt5wLGd%AkZ%A%7Nq<+@t4%m@}IVO~9sUsemQ*@vI5&%(cja`jQG zdk~yChrFd-@zr^uECr)KRFA)+1w;no2vwAiIbo%=sn-SiVe)!`u_3g!j^EPztAce< zusp*=Rqu8B+XVt9{Eh%L1Oa}G;@T2yLrQY?uPu=$l9ZkO7U{$3lI7bv5=G&4^%$kw zA>_V?@rWtu2xPL}FO)@HxvkCL=UDM~9}2Ea-Vt>yL(53zC_|3+#3Yx%+6g&i^=4fdq}2PT3Gi%IreWT=qQzP6P%N0?gUx=8oYRocaD&k_G3oxClGfxcgs*64sOYMS^f zOw46AkjL|37NXjZ3z)YgBev0J(>M!;rq~JqbNmb|>*Twpg-}@sSk7v5h5VzG*Ijpv)D%){lb{_0)3W`EpyF|Jh)Kq z6Fe2XIaT(=JVr&q+33ea`*M{pvfT5CTI%-`BJz+~al!24^>^$*7##k>p~>8PQN^vr zqAL@d!1)Byg#*5@mLp8%6r0Y%myhD-3B2}=%_7-B;O}NK>-cggicnO@#ypKxw||UH z5i>rV{pjPf^s$frTFC9UjTnQszTDzYgsDaP6L8(C&!y!+Rw>7WQEf_ zP}XO^j}5>C4&%6zHg@2N2zG?%4@#US(0(rS%*by5f@=TBHL0$D_uU8VNqb5(q9eG2NMx79y%!$f+V?7;m zW}N2|R-KYK&Z!aW+R1uC_8>o`P#=zAc}vx}-Kdm6BwmMe%T^1n)$?-8OCct}&_b?h z>YKl!wLr^HTdU;hDEb$39Pjf8tF&xmGaL4EsHQY4buGv%jV$VRU8C*3vPw1wGdSwJ z-|U<*m6_#cGcvMpTBg6qXrMIqee#0x*n(!UQlvPi5_!STn-^hquuRnD^<)??oTM6d zEDx*#f0Y|%S+^q{O=Aj4AQG*k*Yn2;SKIPZryEs*Ph9pTr0$G+$}KOkTe%F#sf(w7 z$;n~w|F%V5`I()4AH%|!Ayp7KACL{gJlR^0!YrLb*cB2vk0e4Q3?*9`v^77?>l*V` zw3foJOG^qQb6Gl3Rr}Hn<304RId7*sJp4gTSjH@Ln*(8O`FDranAEgqaQD;B?%Y;k z5~=9a%KROq!`o%CqVI>D-VHiC*gU-3bUe3Rq(S8Y3s`_x2|8-H`i>&p2?ct>72VY1RjOU2 z;lmDkoh}NdPMvjcahp}*T$xtWlN48s>ORjNxMrGfI5pP5ga6E%`el=8YIQ%QNl76; z{K?m~eGDq=;k`52@h@U=Y?6g*u0steSe6QtT>L8}m>@g7O}GsmEpuK?o{0uF`EB95 z)SNXk6hiR~5OSB!etT-7N`0754IWP^Qvg-Ds@LNjc9@js3k3tVIT4lq88uylrEv4gz8$W04x^`Deuz8l5pr!O0K^g}a9D8QtoF%x zm0qzAyA1Bi!!P1f)AK$Il$p06i?udSmZ;;J*#L17>Xasl0Vjcl0SCpY);9Ox?sMj8 z8-3R5r+FHNr+iRemrUgRTo#e5gHj2r)`V_Aw27TMXBEO28vY)OLo9`pS2?0piDbDK z#Y1V_F}l}Fr!YegGFG*UzH+;o_irk5exKh4XR4GHM!!G-WgJyp zes)xD%sMT(=Cd{V0eyyn2oN=Kgcn~qxt3M?`l^kb#cv$6oA)ZlK?pt?n0GG=wVJ~e zSChqwlmVo%e0So?YF$I7W9LLEAx4VPF;%8c<+gA$^tN8{Q@Ht-;WxN#@r6tH`|1Zj zbXoLbJQTk;^B>dNY>S#abVbw){*a25{_Y!FvIVCVd>C3liMmxqP#vQ&3u8iSf+JSn zf?(BniqmCBY<+LWhI4Vm{Gl0d9qr@#^moSwuy@-2gP&cso<4|W}WRG)~ie8SG)LstESjBqM>9l)%Xdk9!*nTAqZG~H)g z=YieF4Jr;VL(C-1J0$@GQ?xUM-R5BR6fBC0g<<^E2-^1Ty>GlAm_ujj(&TcA^oz_w zhH_z5^ykl$Xe~mF0k-Q>`nANy^FgLzS}v>hvO*_YSy%$oy})3jn2(k9(MVij)Z^x( zw?;(l8=5uWL*&`0%*l~jbH<+JQ6Kd!`!R9mUv}Rm-A-JM?xA{XdyRtqB7rH?!91!b z9L3ZV3YOE0WbgEZbyJtYHA(p zK|Obh$d5{V)A>IfDOBB_I=2|2Qcub97i_lT1Nec>q|bqzgbOq2^x8^w+=-DCQ;% zaY?c9$r5eh`MOV{eW8z3i2}Rh(kTeMyW`FYyq|W7@p@liSJ|`xmz8-rHPQj*9di@` zVt5%v@cvho?g6lgt)HSL-x>O0Z+6&QFp9sQZ(vQ3tJ`ne>A1a)$?Pv-Elke@1ut*O zo|s?q6e?TyT+e{c!QM@GhWSa-gVHjU1Cy4XHAAVvy)(@5FRX^q$XL7e4s%k z3(hd`u8wB3GFC-3)m;S<;r&T7%wz2~j{Mvqe1OwB_HUfYo>lq>eU3IHsFchz)-QS} z+~4!jaHL_X!FGlODO1@p?YU6AL0zXOa8P1k(XAaS@#@Yo)o(A`*(D4XSS7%J3RFtX zdbc_a&~3=^t=1&Rp?kZuJQO94k(V`iWXXM;F)!S#aqV_Hv7 zQjl{%ScFbU79Sb1_M?`)Na5z=^HLMQf)!<3?1!`Uivyqvu}sHdc}4AKnU{8sRi`PV zd|o^F0G%lY$gQPW^mF?JD%Gq{DE5|oO820Z>D_QC!tLk3{YZ^r-Rqlwa30W)RgGmG zmqTxZD9qgyJA9^cjSoGvEmH}W;}Eiru+XYjAk*=6rDbjuHGOx?j^Kb#)~ejro{+T6 z{_;VtXaSL|X_W`e8WFl#BAnM=Gqizt0Cuu?TTfX*Vv>7>cm({dsnfFBfAGv$-8+z; z+Q# zsCveC%#c0`GPf%lW*1)O0EK)x z2d4+v$C*mHAXCWOiJVlGI-f4bp|bLh#z~vUHUQbetly3+{Poy2-2XS5!ByVO>`8-E zPcN_nUhw0alb%fV%=Do}y(9g)G9vw*&anqU#vy=Im-_}8^AGwj$gu+TvGm$2+f*fJ|b?88U2Ttnr? z1(dQr_uZa2re=j{P5WI9!HRlwT@!eg+Vq@Qy7PnMOd72yPiEDfPeby?0+*`1g>P1o~F|T>rH15Q=AaB`e zRrG7huE1n^X48u!9;=xYE-&cI8-3`Hgyz5|=yD4+>&1~HhoPo-nqX_$ z94@8d^HTNf@XqnF%$i^(Jl2k8>DbDK-#lF$z-P&xioos(P7K<9e?9dZ$OvA~#}bh~ zY53q!SIMGdn_Pe#jkdt^oc=ic7L#B>5o2+Eyf2M$7Ob=CldaZKm70R6D zz!MuIE8Fd-t8z>{{xU<@sBp*el@7v8YFb#wNIt!ecpBWHT_AwjuGt8Y4J>6 zj^=?|^n$B$!DO5ZqWW!>NaE;xd!K4d^UmNDwtmQGU}p*3#s2&sQ?d0s2e(7e+N+Af3AbDYg z;nWp_LD2m=os*ceT7_~4EabP!$GIfflFQ^YHK=W58)%*U9&>j`w8eh)tYvUvT8~J2 zS0KTxIMKFm!*jY-IuZtr?kIKy6=VW;xXDkJXfxuP*gx-R6(QpgPF6-8reqp{Hj07; zWxbH8q00b|kC*E^HEo$R=>p7!BqRWA|AZHnx9y)?9S?$M0(UiW`!6Eg+f`B+2Xrcs z@vrH|%HI#Ww#w@iXW1f(2EBS0n=PLkz$OX}wPBwS_1Z|jFG*po`I@bubHT)P@o@H$ zdmNe_n&0=|fT8SQb}b6?@^4)5gh)+8PV%1Tm1UAC_zo>L<3HN)tAgiWxg^fb-4c#Y z1l>nQcqS}0&N+xrUUe*aDJQ5aik==N4KkKAC~uX=M`}_$rDL>-V9;U}R+|};bXPes zYG(r)o)wR2p^i&fC5075s|^{qVb6mqzaJziX^`0}PaKq)`L+T+kC3N@mV4|KgCC9iwIDX8_4wLas2>o0yTT))H!Vl+_}K_(Sa1tV3eRIo z%~)BWZfY3QHAk2G`lBnGfypsMzlqgFEpBA*PYl@H0)~(}B3(F~Xd+|NLCm|`RRJWP4}cKU^9W1W!)llw!N<6`;_pp|fv*iK>)Sh;)WHN7j@(1XB-zNG zk;#p$6tN1Eiwf@4bZMKk&9p0?JAu(QNw+2})o@+jquJ4&1TJ$z#3Yw3g45U)S1Ru7 zZo%RP%~}q4@FJY|Yx`o}&ea`9XF}Z7N;zGWq4-=4V0I9Zzv5bdD#9EkAX+Y_Gl7h6 zQomD{;kI>g%mQ_NZPl2N`~6+j#s{_O0&Z4DbCWoRJtwiEiQDN#pgd;%BJ~__H2zTR zd9T}dwC&U&ikNEVO*eOy+mUixhY*fe1x;+fp&gPb@GNq42a-ZWoW2ISY})CJtzu z2&-M>2OhXYaaI?luz;!&BIdALIQ*Un?iWIF3h6UQaBHqlAyJ5L4$#k732}F+)OcLc zo&FmJtHgtU9lk!FQi>>is|%11;4F5K@BBN;+OhB2Ta#yitaP@@+L~P*O!(_=#u*Nf(n&NsLgkr!WcR=HPHTOs!3k!HxWLaS z^5lwQ5GE|CK%jA|b9AP^z=6nG$`xhf0iYLvJe7IFNyhH?oUAp1jzv-697OWhHbxug z=-NhQ)ZvGIL-D%`_B_4R!XB}?8`F0cMplyg)h4(ZhXh4qUfYcVe7YGd}zZg0Y^z_Tq+<< zOh1`9^Yiela9$}u1F^Xofv%0S?A=v7-MrHACp$@c$paDdH&nsOx9@M0vN6kQ{l-QC zI}s>pO&4&nD#eJbpwpe3rvrr@*b@sa5MXw}2+k9!ute?r!ICLr?TJ&xPDH~myJpob z-XHIzPS4-TyPKi`PbI-kp~y!$nXvlXVb=?zT@RMb7s|Kh&W(r3AXV2`?Mr^3HD8HxK`CRg$L$|q`)gHjHTJJj@`9${iJb$Q z*|`#-!oTy88b5&(3@R^tn-O{%)^bjkHpj;{UmT*HbHm#Z6e72JRyN!Nb_i z;ns;i06sQ^-g^GzK%szhd!>gk4{kk!i?UvmayC}LRn#VXLYX95@ZGL;fWhF#dqH6FX5 zhxzM}8KUc{hd+sPs95a1aj|!$n7hU3N|rN?_+irO9&?v@Ka#NeZ`j@pCTWxBR8)mUV#+q z8b(cVQ_*tvqU#g6Kohr0@v9JcTtv_y9k)eWe7;m5V`d12R%oVpwT!vf)jf-uw z0;;n9hl3162E6{=GDK6N*s`vyo%)^LwcdsNc|fbkKdbINWgR$ywca1ki?>&OM<;+}q1e^!k~IS7p0|E9sa}aQZDo-h%ZW z?uuPm0nK26T^TeinLQX1rt=N;it=H>}A$p%V_v`eB|VVVDme} zIdZshIIfsk=qr>a@Jrzvz(Di+fVjIK+k+F4XB$>Q8a^n1BXQ$J7}dineF%Y8Y$7-| z&+s-8VyDVE8_~J&T=-mJh`79Whz}$9rJR8Uq#`O8AELThz=S4lwbhOUC9cMuUSE7kwUeoWlV^ik@K8`g8 ztuoRM{7c*q=(YRsNoih%Li3MQPZk}(ux_HQsY2(XnyU}}-*?~;NEQQa8q+afrI)WI zTzD9~Q75C@j$kvHW3F(FtE^0{C?7X6C^Gg^(2)YbQmKAkxI5nihHE&|dg_MCNXEU3 z;@?k;$xV^y3!C1_%)`abCU9%d`&|4X%Tqj4Z&D#@ zY2}R5%*K5(=}GZG+-yUzMjXwT`aQZi@ULUKHslBFdF2(<_n&EfJDlXZv9s0RlqVl1 ze5xl1s)PbNwobuZ4OueNM^DONLsCRl1mNI6f}4%Mbd|>o9^LHycjr_~FSB1>r*L3* zdwV7*M{JlvVh4>ga2JDmv<}<_3fVOA%~HI$dB?*W=l(%rXN$z*_2bNt^^sVN9FZN0 zN242Emz9}XsXjO3Fw;f)b}mXpQb?7d*}bWsSN-qlt}E4y&CKR~0_g+Xz?|dxYj2al zJlu9XVJT{rC$UU?1?{Yv3H&$b{0p4R)K7kxR&=J4I14CuPzHhqfrbSl35h}BXCt2( zT4_$IW`h5F`t&i(!?kEYlq#wN3+|()^l#{B9mLSu(6CCN`~4I8N^rgoJPmRJq3gTJ zt^(}4=nLz0j5oL4))q!-x#(y)*X^C!6YHz5Ydr87>3xZavUC_S3_PSgA!2CN{dZd( ziuRhsOGv}9G_xz{?9Se6-?o2KW;Nf(l@f6T_a{D|adxt`S^n2ym@Q~Gt4~;{5`Fs4 z$vx8vJH;CkH@gpm91DkEt*MLlVIEU0wX_BN*J0ihGbejSSa0NX=>l^}(pdK9c?jE@ zWanbey0Cus8=1d3fs4Oi{JSsaQ8{7Bi^9U!xh-^B%^7lZJ(C#*C^)h-r0X6)p&lhe6;@5dj#j$5##uZ@4DqqJ z;(_(LIAk`UaiTAdh(2`ucR!s!2eUm|=%uLefUC&2vQES5l7F%rH-lqb5w1VhemJ@r z`1;-nYwkYczoAMZgj7z4y#4;HiqfCNqB;IK`~QZ9G0TVRTHYhLSA9WOgLcgjmG~o70+4_a7(c5d1@cicOz_Ir0=YI;@@F%)0NGo~E zs4Lw+5nFPvRJ-ZIY>az}1+#QPB~xs4!!b;V=Z~qcEfmy}-~AC$uQv1ia`!Q^b1N~k zz00plA^-|+P*GHM7PkhnZFvScqD{A20;dLyf~7`1BiyL*p2{k=q*=#pzXy-^5&sR< zdFv#xHmyHI+j!~jG09R!(16`_4hd2!Q*s#2ethH^`XGJ)hBTHR{xjc4H)c?P|FLLT zO3E!>`jz)>^_BA=N8E&&+8fH6pOC`VeqTmj$_%QwJiWTV)X{V2pXopeZlAc;ruBvB z8Ry+SpXhA(;Q}^RKqt5MsamSOQR5V1BjcL8e_cCsKKG(-DA@EXwf+>H)1rF6x%DmE z8HJ7bAL6#Pe;kSO))jnLP>!ja;E^t4=1nr9S5Om!&`u^g)BkHU{!9(4 z-A(y@lo0>LMi6}dyF?D=Luz1L@$NzXV?phd8D_ahOdFBU z+bKq$^WhQZU*7q`8&fJ64)mKWz9wALUf zCJ24{_xYW@ORf4y?p! zM?ax^cjMFxULK0==`O{(yE+X&#DxF(@uu_jt-qp6DgUiX{QTD1_bc>0^mi{WsZ6u( zQ=Rm6-p9Hm-7Au1x~6UXL+k8t$(5;a-w4syQ`12A%jcbog-F65;(>ES|L8k!Cj8su zz0_G^gxs=+&4U{JKLomi)vtW~<;pw_2jrOEUTZ3stiJrjMpCqBpo>+;rKUyF20%5~k8;Fh-5b5)Q5+{1bgMI?CQ zKoe1LIvD7QZugIh&EOZ?YwadDf4@4Y!|{93w#{3xTM19JmRf*cV7?xr0RN{p(g?i9 zscHAoyOp1Ooz1kZ|FGHc6UWoZmu;-hnB@~V`IVx*JlD93)Ma@ z61Ae@Zo=@|{5F#INTo$|r&5$T70|MJNan@1w~KCq{TH?r(sd)Y^t(LGOwR@wmVz;+DN59>Bt~x5egM{D4yO!X%vzGGup4a{Qsxfp*)6J^0 zPTZglg1-Sv(ioRwJ4AO0kaFf9`u1twQk0SkiF4|v|kdp)(A=f~)~ zxb$LjH==Wp__gM;I5WJ{PI*%5FmkNXESl@aL{R0HE@U$!eRXS3t#cPMfM>4`DIK_r zEv3@3Ew}UIe=cX%zKRL z+1vHJdVEQ~)MqoO@w)h2akA@wuH}usT{qYA`@3t#i<84W?O;%U6=A*eWCs*0l_^A9 zp|jY=Q{0bY=WNrl02+ES)hUd*c-!ywaqQ+IW&r)ktvz1i|GlK9hvNg}NWJ(7csQl*OxDyILR%gRk+fmiX=NZ9jKPB`_{OSs|G zNqFWo%>nf%A8E7!Y%+g$b4_`7Yfb(B)|yt%_L?pV@1yJT74FQeVi)xN(_%DmLFCot JDxR7K{~sBQs{H@} literal 0 HcmV?d00001 diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayoutSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayoutSheet.kt deleted file mode 100644 index 7197cc7..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayoutSheet.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.prismclient.aether.ui.component.type.layout.auto - -import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet - -/** - * [UIAutoLayoutSheet] is the corresponding style sheet to [UIAutoLayout]. See - * the field documentation for more information. - * - * @author sen - * @since 1.0 - */ -class UIAutoLayoutSheet(name: String) : UIContainerSheet(name) { - override fun copy(): UIAutoLayoutSheet = UIAutoLayoutSheet(name).also { - it.apply(this) - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt index 315d789..7890eab 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt @@ -6,6 +6,7 @@ import net.prismclient.aether.ui.component.type.layout.UIFrame import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet import net.prismclient.aether.ui.component.util.interfaces.UILayout import net.prismclient.aether.ui.event.input.UIMouseEvent +import net.prismclient.aether.ui.util.extensions.asRGBA import net.prismclient.aether.ui.util.extensions.renderer import net.prismclient.aether.ui.util.interfaces.UIFocusable @@ -78,10 +79,8 @@ open class UIContainer(style: String?) : UIFrame(style) override fun renderContent() { if (style.useFBO && (requiresUpdate || !style.optimizeRenderer)) { - if (fbo == null) { - updateFBO() - } renderer { + if (fbo == null) updateFBO() fbo!!.renderToFramebuffer { translate( -(style.horizontalScrollbar.value * expandedWidth), @@ -97,27 +96,30 @@ open class UIContainer(style: String?) : UIFrame(style) override fun renderComponent() { if (style.useFBO) { - Aether.renderer.renderFbo( - fbo!!, - relX, - relY, - relWidth, - relHeight, - style.background?.radius?.topLeft ?: 0f, - style.background?.radius?.topRight ?: 0f, - style.background?.radius?.bottomLeft ?: 0f, - style.background?.radius?.bottomRight ?: 0f - ) + println("rende rthis fudmbfuck") + Aether.renderer.renderFbo( + fbo!!, + 0f, + 0f, + relWidth, + relHeight, + style.background?.radius?.topLeft ?: 0f, + style.background?.radius?.topRight ?: 0f, + style.background?.radius?.bottomLeft ?: 0f, + style.background?.radius?.bottomRight ?: 0f + ) } else { renderer { - translate( - -(style.horizontalScrollbar.value * expandedWidth), - -(style.verticalScrollbar.value * expandedHeight) - ) { - if (style.clipContent) scissor(relX, relY, relWidth, relHeight) { + if (style.clipContent) { + scissor(relX, relY, relWidth, relHeight) { + translate(-(style.horizontalScrollbar.value * expandedWidth), -(style.verticalScrollbar.value * expandedHeight)) { + components.forEach(UIComponent<*>::render) + } + } + } else { + translate(-(style.horizontalScrollbar.value * expandedWidth), -(style.verticalScrollbar.value * expandedHeight)) { components.forEach(UIComponent<*>::render) } - else components.forEach(UIComponent<*>::render) } } } @@ -135,6 +137,7 @@ open class UIContainer(style: String?) : UIFrame(style) if (animations!!.isEmpty()) animations = null updateLayout() + components.forEach { it.requestUpdate() } } } diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIPathDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIPathDSL.kt index b49d5aa..d030a77 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIPathDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIPathDSL.kt @@ -2,6 +2,7 @@ package net.prismclient.aether.ui.dsl import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.renderer.UIRenderer +import net.prismclient.aether.ui.renderer.impl.property.UIRadius /** * [UIPathDSL] is a DSL for paths. [UIRendererDSL] utilizes this to apply the paths. @@ -149,6 +150,21 @@ object UIPathDSL { x: Float, y: Float, radius: Float, startAngle: Float, endAngle: Float, windingOrder: UIRenderer.WindingOrder ) = renderer.arc(x, y, radius, startAngle, endAngle, windingOrder) + /** + * Renders a rectangle sub-path with the given bounds and [radius]. + */ + @JvmStatic + fun rect(x: Float, y: Float, width: Float, height: Float, radius: UIRadius?) = rect( + x, + y, + width, + height, + radius?.topLeft ?: 0f, + radius?.topRight ?: 0f, + radius?.bottomRight ?: 0f, + radius?.bottomLeft ?: 0f + ) + /** * Creates a rectangle sub-path with a single radius value. */ diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt index 926acc8..fdc8b70 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt @@ -176,6 +176,9 @@ object UIRendererDSL { // -- General Rendering -- // + /** + * renders a rectangle with the given bounds and [radius]. + */ @JvmStatic fun rect(x: Float, y: Float, width: Float, height: Float, radius: UIRadius?) = rect( x, diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/background/UIGradientBackground.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/background/UIGradientBackground.kt index 985502f..6097eb6 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/background/UIGradientBackground.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/background/UIGradientBackground.kt @@ -2,18 +2,19 @@ package net.prismclient.aether.ui.renderer.impl.background import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.unit.UIUnit +import net.prismclient.aether.ui.util.UIColor import net.prismclient.aether.ui.util.extensions.calculate import net.prismclient.aether.ui.util.extensions.renderer /** - * [UIGradientBackground] is the background renderer for a component which requests a gradient instead of a solid + * [UIGradientBackground] is the background renderer for a component which requests a gradient instead of a solid. * * @author sen - * @since 4/26/2022 + * @since 41.0 */ class UIGradientBackground : UIBackground() { - var gradientStartColor = 0 - var gradientEndColor = 0 + var gradientStartColor: UIColor? = null + var gradientEndColor: UIColor? = null var gradientX: UIUnit? = null var gradientY: UIUnit? = null var gradientWidth: UIUnit? = null @@ -44,15 +45,18 @@ class UIGradientBackground : UIBackground() { component?.relHeight ?: 0f, true ) - gradientWidthCache = + gradientWidthCache = gradientXCache + calculate(gradientWidth, component, component?.relWidth ?: 0f, component?.relHeight ?: 0f, false) - gradientHeightCache = + gradientHeightCache = gradientYCache + calculate(gradientHeight, component, component?.relWidth ?: 0f, component?.relHeight ?: 0f, true) } override fun render() { renderer { - TODO("Gradient suport") + path { + linearGradient(gradientXCache, gradientYCache, gradientWidthCache, gradientHeightCache, gradientStartColor?.rgba ?: 0, gradientEndColor?.rgba ?: 0) + rect(cachedX, cachedY, cachedWidth, cachedHeight, radius) + }.fillPaint() } } diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt index e1e85f7..9066422 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt @@ -4,6 +4,7 @@ import net.prismclient.aether.ui.animation.UIAnimation import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.dsl.UIComponentDSL import net.prismclient.aether.ui.renderer.UIProvider +import net.prismclient.aether.ui.renderer.impl.background.UIGradientBackground import net.prismclient.aether.ui.renderer.impl.property.UIMargin import net.prismclient.aether.ui.renderer.impl.property.UIPadding import net.prismclient.aether.ui.renderer.impl.property.UIRadius @@ -157,13 +158,15 @@ fun include(dependable: UIDependable) = dependable.load() * given to add the keyframes and modify other properties of the [UIAnimation]. The animation is * automatically registered under the name given. */ -fun animationOf(animationName: String, style: S, block: UIAnimation.() -> Unit): UIAnimation { +inline fun animationOf(animationName: String, style: S, block: UIAnimation.() -> Unit): UIAnimation { val animation = UIAnimation(animationName, style) animation.block() UIProvider.registerAnimation(animationName, animation) return animation } +inline fun gradient(block: Block): UIGradientBackground = UIGradientBackground().also(block) + /** * Type alias for a function which has a receiver of [T] and accepts, and returns * nothing. The block is intended to apply properties to the receiver [T]. diff --git a/src/test/kotlin/Renderer.kt b/src/test/kotlin/Renderer.kt index e736fd8..478c8bd 100644 --- a/src/test/kotlin/Renderer.kt +++ b/src/test/kotlin/Renderer.kt @@ -30,6 +30,8 @@ object Renderer : UIRenderer { private val ctx: Long = nvgCreate(NVG_ANTIALIAS) private val fillColor: NVGColor = NVGColor.create() private val strokeColor: NVGColor = NVGColor.create() + private val gradient1: NVGColor = NVGColor.create() + private val gradient2: NVGColor = NVGColor.create() private var paint: NVGPaint? = null private var activeColor: Int = 0 @@ -53,15 +55,8 @@ object Renderer : UIRenderer { override fun color(color: Int) { activeColor = color - nvgFillColor( - ctx, nvgRGBA( - color.getRed().toByte(), - color.getGreen().toByte(), - color.getBlue().toByte(), - color.getAlpha().toByte(), - this.fillColor - ) - ) + nvgColor(color, fillColor) + nvgFillColor(ctx, fillColor) } override fun globalAlpha(alpha: Float) = nvgGlobalAlpha(ctx, alpha) @@ -302,13 +297,7 @@ object Renderer : UIRenderer { override fun strokeWidth(size: Float) = nvgStrokeWidth(ctx, size) override fun strokeColor(color: Int) { - nvgRGBA( - color.getRed().toByte(), - color.getGreen().toByte(), - color.getBlue().toByte(), - color.getAlpha().toByte(), - strokeColor - ) + nvgColor(color, strokeColor) nvgStrokeColor(ctx, strokeColor) } @@ -349,22 +338,18 @@ object Renderer : UIRenderer { override fun linearGradient(x: Float, y: Float, x2: Float, y2: Float, startColor: Int, endColor: Int) { allocPaint() - val color1 = createColor(startColor) - val color2 = createColor(endColor) - nvgLinearGradient(ctx, x, y, x2, y2, color1, color2, paint!!) - color1.free() - color2.free() + nvgColor(startColor, gradient1) + nvgColor(endColor, gradient2) + nvgLinearGradient(ctx, x, y, x2, y2, gradient1, gradient2, paint!!) } override fun radialGradient( x: Float, y: Float, innerRadius: Float, outerRadius: Float, startColor: Int, endColor: Int ) { allocPaint() - val color1 = createColor(startColor) - val color2 = createColor(endColor) - nvgRadialGradient(ctx, x, y, innerRadius, outerRadius, color1, color2, paint!!) - color1.free() - color2.free() + nvgColor(startColor, gradient1) + nvgColor(endColor, gradient2) + nvgRadialGradient(ctx, x, y, innerRadius, outerRadius, gradient1, gradient2, paint!!) } override fun allocPaint() { @@ -380,8 +365,7 @@ object Renderer : UIRenderer { override fun radToDeg(rad: Float): Float = nvgRadToDeg(rad) - private fun createColor(color: Int): NVGColor { - val nvgColor = NVGColor.calloc() + private fun nvgColor(color: Int, nvgColor: NVGColor) { nvgRGBA( color.getRed().toByte(), color.getGreen().toByte(), @@ -389,6 +373,5 @@ object Renderer : UIRenderer { color.getAlpha().toByte(), nvgColor ) - return nvgColor } } \ No newline at end of file diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index 1c3612a..65efab8 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -1,62 +1,139 @@ package examples import net.prismclient.aether.ui.animation.ease.impl.UIQuart +import net.prismclient.aether.ui.component.type.image.UIImageSheet import net.prismclient.aether.ui.component.type.layout.auto.UIAutoLayout import net.prismclient.aether.ui.component.type.layout.list.UIListLayout import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet import net.prismclient.aether.ui.component.util.enums.UIAlignment +import net.prismclient.aether.ui.dsl.UIAssetDSL import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.screen.UIScreen import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.util.* -import net.prismclient.aether.ui.util.extensions.* +import net.prismclient.aether.ui.util.extensions.colorOf +import net.prismclient.aether.ui.util.extensions.minus +import net.prismclient.aether.ui.util.extensions.px +import net.prismclient.aether.ui.util.extensions.rel class Default : UIScreen { override fun build() { create { - styleOf(UIStyleSheet("btn")) { - size(200, 200) - background(colorOf(1f, 0f, 0f, 0.3f), radiusOf(25f)) - } + UIAssetDSL.bulkLoad("/prism/icons", imageFlags = REPEATX or REPEATY or GENERATE_MIPMAPS) + createNavbar() + } + } - val list = list(UIListLayout.ListDirection.Vertical) { - button("Some text", "btn") - button("Some text", "btn") - button("Some text", "btn") - button("Some text", "btn") - }.style(UIContainerSheet("")) { - control(UIAlignment.CENTER) - size(500, 500) - background(colorOf(-1), radiusOf(8f)) - useFBO = true - verticalScrollbar { - background { - backgroundColor = colorOf(0f, 0f, 0f ,0.3f) + private fun createNavbar() { + ucreate { + val navbar = container { + // Logo + container { + image("logo/Logo").style(UIImageSheet("navbar-logo")) { + control(UIAlignment.CENTER) + size(56, 62) } - y = rel(0.1) - x = rel(1) - px(10) - height = rel(0.8) - width = px(5) - color = colorOf(48, 48, 48) - radius = radiusOf(2.5f) + }.style(UIContainerSheet("logo")) { + size(rel(1), px(118)) + } + + onMousePressed { + + UIProvider.dispatchAnimation("navbar-enter", this) } + + // Navbar list + list(UIListLayout.ListDirection.Vertical) { + val layout = UIAutoLayout(UIListLayout.ListDirection.Horizontal, null).apply { + componentAlignment = UIAlignment.MIDDLELEFT + componentSpacing = px(24) + layoutPadding = paddingOf(8, 9, 8, 9) + }.style(UIContainerSheet("navbar-button")) { + control(UIAlignment.CENTER) + size(206, 40) + background(colorOf(0f, 1f, 0f, 0.3f), radiusOf(9f)) + margin { marginBottom = px(8) } + } + + // Navbar button styles + styleOf(UIImageSheet("navbar-icon")) { + size(24, 24) + } + + styleOf(UIStyleSheet("navbar-text")) { + font("Montserrat", px(14), colorOf(-1), left or top) + } + + button("aaaa").style(UIStyleSheet()) { + size(200, 100) + background = gradient { + gradientWidth = rel(1) + gradientHeight = rel(1) + gradientStartColor = colorOf(1f, 0f, 0f, 1f) + gradientEndColor = colorOf(0, 255, 0) + } + } + +// navButton(layout, "Text", "gradient/bag") +// navButton(layout, "Text", "gradient/bag") +// navButton(layout, "Text", "gradient/bag") +// navButton(layout, "Text", "gradient/bag") +// navButton(layout, "Text", "gradient/bag") +// navButton(layout, "Text", "gradient/bag") + + + }.style(UIContainerSheet("navbar-list")) { + y = px(118) + size(rel(1), rel(1) - px(118 + 235)) // 118 = top area, 235 = bottom area + background(colorOf(0f, 0f, 1f, 0.3f)) + verticalScrollbar { + x = rel(1) - px(10) + y = rel(0.1) + width = px(5) + height = rel(0.8) + radius = radiusOf(2.5f) + color = colorOf(207, 207, 207) + background { + backgroundColor = colorOf(1f, 1f, 1f, 0.3f) + radius = radiusOf(2.5) + } + } + useFBO = true + } + + // Footer + + }.style(UIContainerSheet("navbar-container")) { + size(px(236), rel(1)) + background(colorOf(36, 36, 37)) +// useFBO = true } - animationOf("someAnimation", UIContainerSheet()) { + // Navbar animation + animationOf("navbar-enter", UIContainerSheet()) { kf { - x = px(0) - } - UIQuart(1000L) to { - x = px(200) + x = px(-236) } UIQuart(1000L) to { x = px(0) } - onCompletion { - UIProvider.dispatchAnimation("someAnimation", it.component) + } + +// UIProvider.dispatchAnimation("navbar-enter", navbar) + } + } + + /** + * Creates a navbar button + */ + private fun navButton(layout: UIAutoLayout, buttonText: String, imageName: String) { + ucreate { + autoLayout(layout) { + ignore { + image(imageName, "navbar-icon") + text(buttonText, "navbar-text") } } -// UIProvider.dispatchAnimation("someAnimation", list) } } } \ No newline at end of file From 4b6e8b37d2245e7dc3a8f63090a74b63a5be4df2 Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 08:52:59 -0700 Subject: [PATCH 02/37] Fixed fbo animations --- .../ui/component/type/layout/UIFrame.kt | 23 +++++++++--------- .../type/layout/container/UIContainer.kt | 18 +++++--------- .../aether/ui/renderer/UIRenderer.kt | 13 ---------- src/test/kotlin/Renderer.kt | 24 +------------------ src/test/kotlin/examples/Default.kt | 3 +-- 5 files changed, 19 insertions(+), 62 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt index 73d348d..c40131d 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt @@ -59,7 +59,11 @@ abstract class UIFrame(style: String?) : UIComponent(style) * frame has been updated, but prior to the first render. */ open fun updateFBO() { - if (style.useFBO) fbo = Aether.renderer.createFBO(relWidth, relHeight) + if (style.useFBO) { + if ((fbo != null && (fbo!!.width != relWidth || fbo!!.height != relHeight)) || fbo == null) { + fbo = Aether.renderer.createFBO(relWidth, relHeight) + } + } } /** @@ -106,17 +110,12 @@ abstract class UIFrame(style: String?) : UIComponent(style) override fun renderComponent() { if (style.useFBO) { - Aether.renderer.renderFbo( - fbo!!, - relX, - relY, - relWidth, - relHeight, - style.background?.radius?.topLeft ?: 0f, - style.background?.radius?.topRight ?: 0f, - style.background?.radius?.bottomLeft ?: 0f, - style.background?.radius?.bottomRight ?: 0f - ) + renderer { + path { + imagePattern(fbo!!.imagePattern, relX, relY, relWidth, relHeight, 0f, 1f) + rect(relX, relY, relWidth, relHeight) + }.fillPaint() + } } else { if (style.clipContent) UIRendererDSL.scissor(relX, relY, relWidth, relHeight) { components.forEach(UIComponent<*>::render) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt index 7890eab..502be27 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt @@ -96,18 +96,12 @@ open class UIContainer(style: String?) : UIFrame(style) override fun renderComponent() { if (style.useFBO) { - println("rende rthis fudmbfuck") - Aether.renderer.renderFbo( - fbo!!, - 0f, - 0f, - relWidth, - relHeight, - style.background?.radius?.topLeft ?: 0f, - style.background?.radius?.topRight ?: 0f, - style.background?.radius?.bottomLeft ?: 0f, - style.background?.radius?.bottomRight ?: 0f - ) + renderer { + path { + imagePattern(fbo!!.imagePattern, relX, relY, relWidth, relHeight, 0f, 1f) + rect(relX, relY, relWidth, relHeight) + }.fillPaint() + } } else { renderer { if (style.clipContent) { diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt index a7ba8af..cc7ac79 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt @@ -1,7 +1,6 @@ package net.prismclient.aether.ui.renderer import net.prismclient.aether.ui.renderer.image.UIImageData -import net.prismclient.aether.ui.renderer.impl.property.UIRadius import net.prismclient.aether.ui.renderer.other.UIContentFBO import java.nio.ByteBuffer @@ -139,18 +138,6 @@ interface UIRenderer { */ fun unbindFBO() - fun renderFbo( - fbo: UIContentFBO, - x: Float, - y: Float, - width: Float, - height: Float, - topLeft: Float, - topRight: Float, - bottomRight: Float, - bottomLeft: Float - ) - // -- Asset Loading --/ /** * Creates an image from the given [data] registered to the [imageName]. diff --git a/src/test/kotlin/Renderer.kt b/src/test/kotlin/Renderer.kt index 478c8bd..c861bd6 100644 --- a/src/test/kotlin/Renderer.kt +++ b/src/test/kotlin/Renderer.kt @@ -89,7 +89,7 @@ object Renderer : UIRenderer { ctx, (width * contentScale).toInt(), (height * contentScale).toInt(), NVG_IMAGE_REPEATX or NVG_IMAGE_REPEATY ) ?: throw RuntimeException("Failed to create the framebuffer. w: $width, h: $height") val fbo = UIContentFBO( - framebuffer.fbo(), width * contentScale, height * contentScale, width, height, contentScale + framebuffer.image(), width, height, width * contentScale, height * contentScale, contentScale ) framebuffers[fbo] = framebuffer return fbo @@ -118,28 +118,6 @@ object Renderer : UIRenderer { nvgluBindFramebuffer(ctx, null) } - override fun renderFbo( - fbo: UIContentFBO, - x: Float, - y: Float, - width: Float, - height: Float, - topLeft: Float, - topRight: Float, - bottomRight: Float, - bottomLeft: Float, - ) { - allocPaint() - nvgImagePattern(ctx, x, y, width, height, 0f, framebuffers[fbo]!!.image(), 1f, paint!!) - nvgBeginPath(ctx) - color(-1) - paint!!.innerColor(fillColor) - paint!!.outerColor(fillColor) - nvgRoundedRectVarying(ctx, x, y, width, height, topLeft, topRight, bottomRight, bottomLeft) - nvgFillPaint(ctx, paint!!) - nvgFill(ctx) - } - override fun createImage(imageName: String, data: ByteBuffer, flags: Int): UIImageData { val imageData = UIImageData() val width = IntArray(1) diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index 65efab8..3810e11 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -106,7 +106,6 @@ class Default : UIScreen { }.style(UIContainerSheet("navbar-container")) { size(px(236), rel(1)) background(colorOf(36, 36, 37)) -// useFBO = true } // Navbar animation @@ -119,7 +118,7 @@ class Default : UIScreen { } } -// UIProvider.dispatchAnimation("navbar-enter", navbar) + UIProvider.dispatchAnimation("navbar-enter", navbar) } } From 4649e18049e52f823f136989c97947aaf40a5956 Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 10:59:34 -0700 Subject: [PATCH 03/37] Fixed uninheritable functions within core --- .../net/prismclient/aether/ui/Aether.kt | 9 ++--- src/test/kotlin/examples/Default.kt | 34 +++++++------------ 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt index bc7ee23..56f1fbe 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt @@ -33,9 +33,10 @@ import java.util.function.Consumer * functions [Properties.updateSize] and [Properties.updateMouse] to update the values without * invoking the [update], and [mouseMoved] functions. * + * [Aether Documentation](https://aether.prismclient.net/getting-started) + * * @author sen * @since 1.0 - * @see UICore documentation * @see UIProvider */ open class Aether(renderer: UIRenderer) { @@ -123,7 +124,7 @@ open class Aether(renderer: UIRenderer) { * eligibility to be focused or bubbled. The [Properties.mouseX] and [Properties.mouseY] * properties can be found in [Aether.Properties]. */ - fun mouseMoved(mouseX: Float, mouseY: Float) { + open fun mouseMoved(mouseX: Float, mouseY: Float) { updateMouse(mouseX, mouseY) mouseMoveListeners?.forEach { it.value.run() } if (activeScreen != null) for (i in 0 until components!!.size) components!![i].mouseMoved(mouseX, mouseY) @@ -139,7 +140,7 @@ open class Aether(renderer: UIRenderer) { * * @see mouseScrolled */ - fun mouseChanged(mouseButton: Int, isRelease: Boolean) { + open fun mouseChanged(mouseButton: Int, isRelease: Boolean) { if (isRelease) { mouseReleasedListeners?.forEach { it.value.run() } components?.forEach { it.mouseReleased(mouseX, mouseY) } @@ -220,7 +221,7 @@ open class Aether(renderer: UIRenderer) { * @param character The key which was pressed or '\u0000' * @see updateModifierKey To update keys such as Shift, Alt, Tab etc... */ - fun keyPressed(character: Char) { + open fun keyPressed(character: Char) { keyPressListeners?.forEach { it.value.accept(character) } (focusedComponent as? UIComponent<*>)?.keyPressed(character) } diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index 3810e11..f3a076f 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -37,13 +37,8 @@ class Default : UIScreen { size(rel(1), px(118)) } - onMousePressed { - - UIProvider.dispatchAnimation("navbar-enter", this) - } - // Navbar list - list(UIListLayout.ListDirection.Vertical) { + val navList = list(UIListLayout.ListDirection.Vertical) { val layout = UIAutoLayout(UIListLayout.ListDirection.Horizontal, null).apply { componentAlignment = UIAlignment.MIDDLELEFT componentSpacing = px(24) @@ -64,22 +59,16 @@ class Default : UIScreen { font("Montserrat", px(14), colorOf(-1), left or top) } - button("aaaa").style(UIStyleSheet()) { - size(200, 100) - background = gradient { - gradientWidth = rel(1) - gradientHeight = rel(1) - gradientStartColor = colorOf(1f, 0f, 0f, 1f) - gradientEndColor = colorOf(0, 255, 0) - } - } + navButton(layout, "Dashboard", "gradient/home") + navButton(layout, "Mods", "gradient/mail") + navButton(layout, "Settings", "gradient/setting") + navButton(layout, "Store", "gradient/bag") + navButton(layout, "Profiles", "gradient/profile") -// navButton(layout, "Text", "gradient/bag") -// navButton(layout, "Text", "gradient/bag") -// navButton(layout, "Text", "gradient/bag") -// navButton(layout, "Text", "gradient/bag") -// navButton(layout, "Text", "gradient/bag") -// navButton(layout, "Text", "gradient/bag") + navButton(layout, "Messages", "gradient/mail") + navButton(layout, "Friends", "gradient/people") + navButton(layout, "Achievements", "gradient/medal") + navButton(layout, "Recordings", "gradient/video") }.style(UIContainerSheet("navbar-list")) { @@ -102,6 +91,9 @@ class Default : UIScreen { } // Footer +// val footer = component(UIAutoLayout(UIListLayout.ListDirection.Horizontal, null)) { +// verticalResizing = UIAutoLayout.ResizingMode.Hug +// } }.style(UIContainerSheet("navbar-container")) { size(px(236), rel(1)) From c6d5691181440747f446932b6d3ff3857eb19720 Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 11:13:06 -0700 Subject: [PATCH 04/37] Removed protected modifiers --- .../kotlin/net/prismclient/aether/ui/Aether.kt | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt index 56f1fbe..9379149 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt @@ -247,15 +247,12 @@ open class Aether(renderer: UIRenderer) { @JvmStatic lateinit var instance: Aether - protected set @JvmStatic lateinit var renderer: UIRenderer - protected set @JvmStatic var activeScreen: UIScreen? = null - protected set /** * The focused component (if applicable). @@ -266,42 +263,36 @@ open class Aether(renderer: UIRenderer) { */ @JvmStatic var focusedComponent: UIFocusable? = null - protected set /** * The width of the screen. It can be set via [update] */ @JvmStatic var width: Float = 0f - protected set /** * The width of the screen. It can be set via [update] */ @JvmStatic var height: Float = 0f - protected set /** * The device pixel ratio. It can be set via [update]. It is the equivalent of content scale. */ @JvmStatic var devicePxRatio: Float = 1f - protected set /** * The x position of the mouse relative to the screen */ @JvmStatic var mouseX: Float = 0f - protected set /** * The y position of the mouse relative to the screen */ @JvmStatic var mouseY: Float = 0f - protected set /** * Invoked whenever the layout needs to be updated. This can be when the screen @@ -309,49 +300,42 @@ open class Aether(renderer: UIRenderer) { */ @JvmStatic var updateListeners: HashMap? = null - protected set /** * The listeners for then the mouse is moved. Invoked prior to components. */ @JvmStatic var mouseMoveListeners: HashMap? = null - protected set /** * Invoked when the mouse is pressed. Invoked prior to components. */ @JvmStatic var mousePressedListeners: HashMap? = null - protected set /** * Invoked when the mouse is released. Invoked prior to components. */ @JvmStatic var mouseReleasedListeners: HashMap? = null - protected set /** * Invoked when a key is pressed. Invoked prior to components. */ @JvmStatic var keyPressListeners: HashMap>? = null - protected set /** * Invoked when the mouse is scrolled. Invoked prior to components. */ @JvmStatic var mouseScrollListeners: HashMap>? = null - protected set /** * Invoked when the screen is deleted. This is used to deallocate listeners added to UICore. */ @JvmStatic var deallocationListeners: HashMap? = null - protected set /** * The list of modifier keys. The value is if the key is pressed From fb73cd783e0ffe748845b7eb5badd97f5fdf3b7c Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 11:23:22 -0700 Subject: [PATCH 05/37] Possibly fix thingy --- src/main/kotlin/net/prismclient/aether/ui/Aether.kt | 8 +------- .../net/prismclient/aether/ui/component/UIComponent.kt | 2 +- .../net/prismclient/aether/ui/component/type/UILabel.kt | 2 +- .../aether/ui/component/type/input/button/UIButton.kt | 2 +- .../aether/ui/component/type/layout/UIFrame.kt | 3 +-- 5 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt index 9379149..fd95781 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt @@ -487,13 +487,7 @@ open class Aether(renderer: UIRenderer) { * Focuses the component. Please use [UIComponent.focus] instead. */ @JvmStatic - fun focus(component: UIFocusable) { - // Check if the given value is a valid instance of UIComponent - try { - component as UIComponent<*> - } catch (castException: ClassCastException) { - throw RuntimeException("When trying to focus, the provided value is not an instance of UIComponent. Make sure you are only using the UIFocus interface to focus UIComponents.") - } + fun focus(component: T) where T : UIComponent<*>, T : UIFocusable { focusedComponent = component } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt index c721c96..d2a20c3 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt @@ -658,7 +658,7 @@ abstract class UIComponent(style: String?) { open fun focus() { if (this is UIFocusable) { Aether.focus(this) - focusListeners?.forEach { it.value.accept(this as UIComponent, true) } + focusListeners?.forEach { it.value.accept(this, true) } requestUpdate() } } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt index dfa4489..4929747 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt @@ -7,7 +7,7 @@ import net.prismclient.aether.ui.style.UIStyleSheet * [UILabel] is a component which draws a label, or string on screen. * * @author sen - * @since 5/15/2022 + * @since 1.0 */ class UILabel(var text: String, style: String?) : UIComponent(style) { override fun renderComponent() { diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt index 0d522f9..5eaec0f 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt @@ -7,7 +7,7 @@ import net.prismclient.aether.ui.style.UIStyleSheet * [UIButton] is a simple class, which is used to create a button. * * @author sen - * @since 5/16/2022 + * @since 1.0 * @param T The stylesheet (used for inheritance) leave as UIStyleSheet. */ open class UIButton(open var text: String, style: String?) : UIComponent(style) { diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt index c40131d..5b49e94 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt @@ -97,11 +97,10 @@ abstract class UIFrame(style: String?) : UIComponent(style) components.forEach(UIComponent<*>::render) } } - requiresUpdate = false } + requiresUpdate = false } - override fun render() { updateAnimation() style.background?.render() From de8d030f50eb84efdda75f479cccdc4928f78719 Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 11:31:02 -0700 Subject: [PATCH 06/37] now maybe fix --- .../net/prismclient/aether/ui/component/type/layout/UIFrame.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt index 5b49e94..9cec777 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt @@ -124,7 +124,7 @@ abstract class UIFrame(style: String?) : UIComponent(style) } override fun requestUpdate() { - requiresUpdate = true +// requiresUpdate = true super.requestUpdate() } From a77ae3dc694a287de6b8607d4274deb4a76f5fc9 Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 11:37:43 -0700 Subject: [PATCH 07/37] more test --- .../kotlin/net/prismclient/aether/ui/component/UIComponent.kt | 2 +- .../net/prismclient/aether/ui/component/type/layout/UIFrame.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt index d2a20c3..d77e587 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt @@ -657,7 +657,7 @@ abstract class UIComponent(style: String?) { */ open fun focus() { if (this is UIFocusable) { - Aether.focus(this) +// Aether.focus(this) focusListeners?.forEach { it.value.accept(this, true) } requestUpdate() } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt index 9cec777..5b49e94 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt @@ -124,7 +124,7 @@ abstract class UIFrame(style: String?) : UIComponent(style) } override fun requestUpdate() { -// requiresUpdate = true + requiresUpdate = true super.requestUpdate() } From 99e025af743c645bd319e096b89110f862131628 Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 11:48:26 -0700 Subject: [PATCH 08/37] fix x2? --- .../kotlin/net/prismclient/aether/ui/component/UIComponent.kt | 2 +- .../aether/ui/component/type/layout/container/UIContainer.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt index d77e587..d2a20c3 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt @@ -657,7 +657,7 @@ abstract class UIComponent(style: String?) { */ open fun focus() { if (this is UIFocusable) { -// Aether.focus(this) + Aether.focus(this) focusListeners?.forEach { it.value.accept(this, true) } requestUpdate() } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt index 502be27..b60d5eb 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt @@ -137,7 +137,7 @@ open class UIContainer(style: String?) : UIFrame(style) override fun mousePressed(event: UIMouseEvent) { super.mousePressed(event) - if (style.verticalScrollbar.mousePressed() || style.horizontalScrollbar.mousePressed()) focus() +// if (style.verticalScrollbar.mousePressed() || style.horizontalScrollbar.mousePressed()) focus() } override fun mouseReleased(mouseX: Float, mouseY: Float) { From 91461ee555a5dda4a80da3d17dccb278ae4ca2bc Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 11:54:45 -0700 Subject: [PATCH 09/37] ok i fixed --- src/main/kotlin/net/prismclient/aether/ui/Aether.kt | 1 + .../aether/ui/component/type/layout/container/UIContainer.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt index fd95781..2b8ecdb 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt @@ -231,6 +231,7 @@ open class Aether(renderer: UIRenderer) { * of their eligibility to be focused or bubbled. */ open fun mouseScrolled(scrollAmount: Float) { + if (scrollAmount == 0f) return tryFocus() mouseScrollListeners?.forEach { it.value.accept(scrollAmount) } components?.forEach { it.mouseScrolled(mouseX, mouseY, scrollAmount) } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt index b60d5eb..502be27 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt @@ -137,7 +137,7 @@ open class UIContainer(style: String?) : UIFrame(style) override fun mousePressed(event: UIMouseEvent) { super.mousePressed(event) -// if (style.verticalScrollbar.mousePressed() || style.horizontalScrollbar.mousePressed()) focus() + if (style.verticalScrollbar.mousePressed() || style.horizontalScrollbar.mousePressed()) focus() } override fun mouseReleased(mouseX: Float, mouseY: Float) { From 45a046e6858d0fcc8a537dec4d7a9129d2407f34 Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 16:39:42 -0700 Subject: [PATCH 10/37] Deallocate framebuffer --- .../ui/component/type/layout/UIFrame.kt | 5 + src/test/kotlin/examples/Default.kt | 95 ++++++++++++++----- 2 files changed, 75 insertions(+), 25 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt index 5b49e94..fbc1ad2 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt @@ -59,6 +59,11 @@ abstract class UIFrame(style: String?) : UIComponent(style) * frame has been updated, but prior to the first render. */ open fun updateFBO() { + // Deallocate any existing framebuffer + if (fbo != null) { + Aether.renderer.deleteFBO(fbo!!) + fbo = null + } if (style.useFBO) { if ((fbo != null && (fbo!!.width != relWidth || fbo!!.height != relHeight)) || fbo == null) { fbo = Aether.renderer.createFBO(relWidth, relHeight) diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index f3a076f..6fa26be 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -8,28 +8,27 @@ import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.dsl.UIAssetDSL import net.prismclient.aether.ui.renderer.UIProvider +import net.prismclient.aether.ui.renderer.impl.font.UIFont import net.prismclient.aether.ui.screen.UIScreen import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.util.* -import net.prismclient.aether.ui.util.extensions.colorOf -import net.prismclient.aether.ui.util.extensions.minus -import net.prismclient.aether.ui.util.extensions.px -import net.prismclient.aether.ui.util.extensions.rel - -class Default : UIScreen { - override fun build() { - create { - UIAssetDSL.bulkLoad("/prism/icons", imageFlags = REPEATX or REPEATY or GENERATE_MIPMAPS) - createNavbar() - } +import net.prismclient.aether.ui.util.extensions.* + +class Default : UIScreen { override fun build() { + create { + // UIAssetDSL.image("NavbarLogo", "/prism/logo/Logo.png", flags = REPEATX or REPEATY or GENERATE_MIPMAPS) + // TODO: test if lods stuff in root directory + UIAssetDSL.bulkLoad("/prism/icons", imageFlags = REPEATX or REPEATY or GENERATE_MIPMAPS) + createNavbar() } +} private fun createNavbar() { ucreate { val navbar = container { // Logo container { - image("logo/Logo").style(UIImageSheet("navbar-logo")) { + image("NavbarLogo").style(UIImageSheet("navbar-logo")) { control(UIAlignment.CENTER) size(56, 62) } @@ -46,10 +45,19 @@ class Default : UIScreen { }.style(UIContainerSheet("navbar-button")) { control(UIAlignment.CENTER) size(206, 40) - background(colorOf(0f, 1f, 0f, 0.3f), radiusOf(9f)) margin { marginBottom = px(8) } } + // Navbar section style + styleOf(UIStyleSheet("navbar-title")) { + x = px(24) + font("Montserrat", px(11), colorOf(191, 189, 193), top or left) + margin { + marginTop = descender(1f) + marginBottom = px(8) + } + } + // Navbar button styles styleOf(UIImageSheet("navbar-icon")) { size(24, 24) @@ -59,22 +67,22 @@ class Default : UIScreen { font("Montserrat", px(14), colorOf(-1), left or top) } + label("MENU", "navbar-title") navButton(layout, "Dashboard", "gradient/home") - navButton(layout, "Mods", "gradient/mail") + navButton(layout, "Mods", "gradient/folder") navButton(layout, "Settings", "gradient/setting") navButton(layout, "Store", "gradient/bag") navButton(layout, "Profiles", "gradient/profile") + label("SOCIAL", "navbar-title") navButton(layout, "Messages", "gradient/mail") navButton(layout, "Friends", "gradient/people") navButton(layout, "Achievements", "gradient/medal") navButton(layout, "Recordings", "gradient/video") - }.style(UIContainerSheet("navbar-list")) { y = px(118) size(rel(1), rel(1) - px(118 + 235)) // 118 = top area, 235 = bottom area - background(colorOf(0f, 0f, 1f, 0.3f)) verticalScrollbar { x = rel(1) - px(10) y = rel(0.1) @@ -87,17 +95,47 @@ class Default : UIScreen { radius = radiusOf(2.5) } } - useFBO = true + clipContent = true } // Footer -// val footer = component(UIAutoLayout(UIListLayout.ListDirection.Horizontal, null)) { -// verticalResizing = UIAutoLayout.ResizingMode.Hug -// } + val footer = component(UIAutoLayout(UIListLayout.ListDirection.Vertical, null)) { + verticalResizing = UIAutoLayout.ResizingMode.Hug + horizontalResizing = UIAutoLayout.ResizingMode.Hug + componentAlignment = UIAlignment.CENTER + componentSpacing = px(6) + // Promotion + // TODO: Promotion component + + val editHud = component(UIAutoLayout(UIListLayout.ListDirection.Horizontal, null)) { + image("outline/ui", "navbar-icon") + text("Edit HUD", "navbar-text") + + componentAlignment = UIAlignment.CENTER + componentSpacing = px(20) + layoutPadding = paddingOf(15, 37, 15, 37) + + verticalResizing = UIAutoLayout.ResizingMode.Hug + horizontalResizing = UIAutoLayout.ResizingMode.Hug + }.style(UIContainerSheet("edit-hud-layout")) { + control(UIAlignment.BOTTOMCENTER) + background(colorOf(36, 37, 37), radiusOf(15)) + } + }.style(UIContainerSheet("navbar-footer-layout")) { + control(UIAlignment.BOTTOMCENTER) + y -= px(16 + 8) + } + + label("Running Prism Client v1.0.0-Beta").style(UIStyleSheet("nav-prism-version")) { + font("Montserrat", px(8), colorOf(1f, 1f, 1f, 0.8f), left or top, UIFont.FontType.Light) + control(UIAlignment.BOTTOMCENTER) + y -= px(8) + } }.style(UIContainerSheet("navbar-container")) { size(px(236), rel(1)) - background(colorOf(36, 36, 37)) + background(colorOf(32, 32, 32)) + useFBO = true } // Navbar animation @@ -110,6 +148,15 @@ class Default : UIScreen { } } + animationOf("navbar-exit", UIContainerSheet()) { + kf { + x = px(0) + } + UIQuart(500L) to { + x = px(-236) + } + } + UIProvider.dispatchAnimation("navbar-enter", navbar) } } @@ -120,10 +167,8 @@ class Default : UIScreen { private fun navButton(layout: UIAutoLayout, buttonText: String, imageName: String) { ucreate { autoLayout(layout) { - ignore { - image(imageName, "navbar-icon") - text(buttonText, "navbar-text") - } + image(imageName, "navbar-icon") + text(buttonText, "navbar-text") } } } From c0528ce68d06f91f963307918b53f93af17dbb7b Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 21:31:45 -0700 Subject: [PATCH 11/37] Updated some classes, and flipped renderer fbo --- .../ui/component/type/image/UIImageSheet.kt | 5 +- .../type/layout/auto/UIAutoLayout.kt | 21 ++- .../aether/ui/dsl/UIComponentDSL.kt | 69 ++++++-- .../prismclient/aether/ui/dsl/UIPathDSL.kt | 5 +- .../aether/ui/dsl/UIRendererDSL.kt | 2 +- src/test/kotlin/Renderer.kt | 3 +- src/test/kotlin/examples/Default.kt | 158 +----------------- 7 files changed, 80 insertions(+), 183 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt index e63bada..2dc6095 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt @@ -2,6 +2,7 @@ package net.prismclient.aether.ui.component.type.image import net.prismclient.aether.ui.renderer.impl.property.UIRadius import net.prismclient.aether.ui.style.UIStyleSheet +import net.prismclient.aether.ui.util.UIColor /** * [UIImageSheet] is the sheet implementation for [UIImage]. @@ -11,9 +12,9 @@ import net.prismclient.aether.ui.style.UIStyleSheet */ class UIImageSheet(name: String) : UIStyleSheet(name) { /** - * The color of the image. Use -1 (WHITE) for the normal color. + * The color of the image. The default value is RGBA(255, 255, 255) */ - var imageColor = -1 + var imageColor: UIColor? = null /** * The radius of the image. diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayout.kt index f29b036..a53cbe6 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayout.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayout.kt @@ -67,6 +67,14 @@ class UIAutoLayout @JvmOverloads constructor( */ var componentAlignment: UIAlignment = TOPLEFT + /** + * Sets the [verticalResizing] and [horizontalResizing] to [vertical] and [horizontal] respectively. + */ + fun resize(vertical: ResizingMode, horizontal: ResizingMode) { + verticalResizing = vertical + horizontalResizing = horizontal + } + override fun updateLayout() { if (components.isEmpty()) return @@ -82,18 +90,19 @@ class UIAutoLayout @JvmOverloads constructor( var h = 0f for (i in components.indices) { + val component = components[i] if (horizontalResizing == ResizingMode.Hug) { w = if (listDirection == ListDirection.Horizontal) { - w + components[i].relWidth + if (i < components.size - 1) spacing else 0f + w + component.relWidth + component.marginLeft + component.marginRight + if (i < components.size - 1) spacing else 0f } else { - components[i].relWidth.coerceAtLeast(w) + (component.relWidth + component.marginLeft + component.marginRight).coerceAtLeast(w) } } if (verticalResizing == ResizingMode.Hug) { h = if (listDirection == ListDirection.Vertical) { - h + components[i].relHeight + if (i < components.size - 1) spacing else 0f + h + component.relHeight + component.marginTop + component.marginBottom + if (i < components.size - 1) spacing else 0f } else { - components[i].relHeight.coerceAtLeast(h) + (component.relHeight + component.marginTop + component.marginBottom).coerceAtLeast(h) } } } @@ -140,7 +149,7 @@ class UIAutoLayout @JvmOverloads constructor( MIDDLELEFT, CENTER, MIDDLERIGHT -> (height - c.height - top - bottom) / 2f BOTTOMLEFT, BOTTOMCENTER, BOTTOMRIGHT -> (height - c.height - left - right) else -> 0f - } + } + c.marginTop - c.marginBottom x += c.width + spacing } else if (listDirection == ListDirection.Vertical) { c.x = x + when (componentAlignment) { @@ -148,7 +157,7 @@ class UIAutoLayout @JvmOverloads constructor( TOPCENTER, CENTER, BOTTOMCENTER -> (width - c.width - left - right) / 2f TOPRIGHT, MIDDLERIGHT, BOTTOMRIGHT -> (width - c.width - left - right) else -> 0f - } + } + c.marginLeft - c.marginRight c.y = y y += c.height + spacing } diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt index f4649bd..b07acfb 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt @@ -16,6 +16,7 @@ import net.prismclient.aether.ui.component.type.layout.list.UIListLayout import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet import net.prismclient.aether.ui.dsl.UIComponentDSL.activeStyle import net.prismclient.aether.ui.style.UIStyleSheet +import net.prismclient.aether.ui.util.Block import net.prismclient.aether.ui.util.interfaces.UIDependable import java.util.* @@ -136,7 +137,7 @@ object UIComponentDSL { * * @return T The component */ - inline fun > component(component: T, block: T.() -> Unit): T { + inline fun > component(component: T, block: Block): T { pushComponent(component) component.block() component.initialize() @@ -151,7 +152,7 @@ object UIComponentDSL { * * @see ignore */ - inline fun , T : UIComponent<*>> controller(controller: C, block: C.() -> Unit) { + inline fun , T : UIComponent<*>> controller(controller: C, block: Block) { Aether.instance.controllers!!.add(controller) activeController = controller controller.block() @@ -161,7 +162,7 @@ object UIComponentDSL { /** * Creates a block where the [activeController] is ignored. */ - inline fun ignore(block: UIComponentDSL.() -> Unit): UIComponentDSL { + inline fun ignore(block: Block): UIComponentDSL { ignoreController = true block(this) ignoreController = false @@ -171,7 +172,7 @@ object UIComponentDSL { /** * Creates a block where the style is set to the given value. */ - inline fun style(styleName: String, block: UIComponentDSL.() -> Unit) { + inline fun style(styleName: String, block: Block) { // Technically this function supports nesting, soooooo the documentation // is "technically" wrong but whatever. In the case of controllers, it // doesn't actually make sense to have the ability to nest them. @@ -187,8 +188,7 @@ object UIComponentDSL { */ fun include(dependable: UIDependable) = dependable.load() - fun getActiveComponent(): UIComponent<*>? = - if (componentStack.isNullOrEmpty()) null else componentStack!!.peek() + fun getActiveComponent(): UIComponent<*>? = if (componentStack.isNullOrEmpty()) null else componentStack!!.peek() fun getActiveFrame(): UIFrame<*>? = if (frameStack.isNullOrEmpty()) null else frameStack!!.peek() @@ -207,7 +207,7 @@ object UIComponentDSL { * @see label */ @JvmOverloads - inline fun text(text: String, style: String? = activeStyle, block: UILabel.() -> Unit = {}) = + inline fun text(text: String, style: String? = activeStyle, block: Block = {}) = component(UILabel(text, style), block) /** @@ -216,13 +216,13 @@ object UIComponentDSL { * @see text */ @JvmOverloads - inline fun label(text: String, style: String? = activeStyle, block: UILabel.() -> Unit = {}) = + inline fun label(text: String, style: String? = activeStyle, block: Block = {}) = component(UILabel(text, style), block) /** * Creates a [UIButton] with the provided [text], like a label. */ - inline fun button(text: String, style: String? = activeStyle, block: UIButton.() -> Unit = {}) = + inline fun button(text: String, style: String? = activeStyle, block: Block> = {}) = component(UIButton(text, style), block) /** @@ -235,7 +235,7 @@ object UIComponentDSL { deselectedImageName: String = "", imageStyle: String, style: String? = activeStyle, - block: UICheckbox.() -> Unit + block: Block = {} ) = component(UICheckbox(checked, selectedImageName, deselectedImageName, imageStyle, style), block) /** @@ -248,21 +248,21 @@ object UIComponentDSL { range: ClosedFloatingPointRange, step: Number, style: String? = activeStyle, - block: UISlider.() -> Unit = {} + block: Block = {} ) = component(UISlider(value.toDouble(), range, step.toDouble(), style), block) /** * Creates a [UIImage] with the [imageName] as the image to be rendered. */ @JvmOverloads - inline fun image(imageName: String, style: String? = activeStyle, block: UIImage.() -> Unit = {}) = + inline fun image(imageName: String, style: String? = activeStyle, block: Block = {}) = component(UIImage(imageName, style), block) /** * Creates a [UIContainer]. Anything within the block will be added to the component list within this. */ @JvmOverloads - inline fun container(style: String? = activeStyle, block: UIContainer.() -> Unit = {}) = + inline fun container(style: String? = activeStyle, block: Block> = {}) = component(UIContainer(style), block) /** @@ -275,14 +275,50 @@ object UIComponentDSL { listDirection: UIListLayout.ListDirection, listOrder: UIListLayout.ListOrder = UIListLayout.ListOrder.Forward, style: String? = activeStyle, - block: UIListLayout.() -> Unit = {} + block: Block = {} ) = component(UIListLayout(listDirection, listOrder, style), block) + /** + * Creates a vertical list via [list]. + * + * @see list + */ + inline fun verticalList( + listOrder: UIListLayout.ListOrder = UIListLayout.ListOrder.Forward, + style: String? = activeStyle, + block: Block = {} + ) = list(UIListLayout.ListDirection.Vertical, listOrder, style, block) + + /** + * Creates a horizontal list via [list]. + * + * @see list + */ + inline fun horizontalList( + listOrder: UIListLayout.ListOrder = UIListLayout.ListOrder.Forward, + style: String? = activeStyle, + block: Block = {} + ) = list(UIListLayout.ListDirection.Horizontal, listOrder, style, block) + /** * Creates a copy of the given layout and creates a normal block of [UIAutoLayout] where * components can be defined. */ - inline fun autoLayout(layout: UIAutoLayout, block: UIAutoLayout.() -> Unit) = component(layout.copy(), block) + @JvmOverloads + inline fun autoLayout(layout: UIAutoLayout, block: Block = {}) = component(layout.copy(), block) + + /** + * Creates an [UIAutoLayout] from the given [listDirection]. [UIAutoLayout] are designed to mimic Figma's + * auto layout feature. See [UIAutoLayout] for more information. + * + * @see UIAutoLayout + */ + @JvmOverloads + inline fun autoLayout( + listDirection: UIListLayout.ListDirection, + style: String? = null, + block: Block = {} + ) = component(UIAutoLayout(listDirection, style), block) /** * Creates a [UISelectableController] which is a controller that has a single selected @@ -291,6 +327,7 @@ object UIComponentDSL { * @see controller * @see UISelectableController */ - inline fun > selectable(block: UISelectableController.() -> Unit) = + @JvmOverloads + inline fun > selectable(block: Block> = {}) = controller(UISelectableController(T::class), block) } \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIPathDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIPathDSL.kt index d030a77..3d33bfb 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIPathDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIPathDSL.kt @@ -3,6 +3,7 @@ package net.prismclient.aether.ui.dsl import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.renderer.UIRenderer import net.prismclient.aether.ui.renderer.impl.property.UIRadius +import net.prismclient.aether.ui.util.Block /** * [UIPathDSL] is a DSL for paths. [UIRendererDSL] utilizes this to apply the paths. @@ -232,8 +233,8 @@ object UIPathDSL { * @see See NanoVG Composite paths */ @JvmStatic - inline fun hole(block: UIPathDSL.() -> Unit): UIPathDSL { - block() + inline fun hole(block: Block): UIPathDSL { + UIPathDSL.block() renderer.pathHole(true) return this } diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt index fdc8b70..d52edf2 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt @@ -402,7 +402,7 @@ object UIRendererDSL { @JvmStatic inline fun UIContentFBO.renderToFramebuffer(block: Block): UIRendererDSL { renderer.bindFBO(this) - beginFrame(this.scaledWidth, this.scaledHeight, this.contentScale) + beginFrame(this.width, this.height, this.contentScale) UIRendererDSL.block() endFrame() renderer.unbindFBO() diff --git a/src/test/kotlin/Renderer.kt b/src/test/kotlin/Renderer.kt index c861bd6..c1344e8 100644 --- a/src/test/kotlin/Renderer.kt +++ b/src/test/kotlin/Renderer.kt @@ -109,8 +109,7 @@ object Renderer : UIRenderer { nvgluBindFramebuffer( ctx, framebuffers[fbo] ?: throw NullPointerException("Unable to find the framebuffer $fbo.") ) - GL11.glViewport(0, 0, fbo.width.toInt(), fbo.height.toInt()) - GL11.glClearColor(0f, 0f, 0f, 0f) + GL11.glViewport(0, 0, fbo.scaledWidth.toInt(), fbo.scaledHeight.toInt()) GL11.glClear(GL11.GL_COLOR_BUFFER_BIT or GL11.GL_STENCIL_BUFFER_BIT) } diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index 6fa26be..5920318 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -14,161 +14,11 @@ import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.util.* import net.prismclient.aether.ui.util.extensions.* -class Default : UIScreen { override fun build() { - create { - // UIAssetDSL.image("NavbarLogo", "/prism/logo/Logo.png", flags = REPEATX or REPEATY or GENERATE_MIPMAPS) - // TODO: test if lods stuff in root directory - UIAssetDSL.bulkLoad("/prism/icons", imageFlags = REPEATX or REPEATY or GENERATE_MIPMAPS) - createNavbar() - } -} - - private fun createNavbar() { - ucreate { - val navbar = container { - // Logo - container { - image("NavbarLogo").style(UIImageSheet("navbar-logo")) { - control(UIAlignment.CENTER) - size(56, 62) - } - }.style(UIContainerSheet("logo")) { - size(rel(1), px(118)) - } - - // Navbar list - val navList = list(UIListLayout.ListDirection.Vertical) { - val layout = UIAutoLayout(UIListLayout.ListDirection.Horizontal, null).apply { - componentAlignment = UIAlignment.MIDDLELEFT - componentSpacing = px(24) - layoutPadding = paddingOf(8, 9, 8, 9) - }.style(UIContainerSheet("navbar-button")) { - control(UIAlignment.CENTER) - size(206, 40) - margin { marginBottom = px(8) } - } - - // Navbar section style - styleOf(UIStyleSheet("navbar-title")) { - x = px(24) - font("Montserrat", px(11), colorOf(191, 189, 193), top or left) - margin { - marginTop = descender(1f) - marginBottom = px(8) - } - } - - // Navbar button styles - styleOf(UIImageSheet("navbar-icon")) { - size(24, 24) - } - - styleOf(UIStyleSheet("navbar-text")) { - font("Montserrat", px(14), colorOf(-1), left or top) - } - - label("MENU", "navbar-title") - navButton(layout, "Dashboard", "gradient/home") - navButton(layout, "Mods", "gradient/folder") - navButton(layout, "Settings", "gradient/setting") - navButton(layout, "Store", "gradient/bag") - navButton(layout, "Profiles", "gradient/profile") - - label("SOCIAL", "navbar-title") - navButton(layout, "Messages", "gradient/mail") - navButton(layout, "Friends", "gradient/people") - navButton(layout, "Achievements", "gradient/medal") - navButton(layout, "Recordings", "gradient/video") - - }.style(UIContainerSheet("navbar-list")) { - y = px(118) - size(rel(1), rel(1) - px(118 + 235)) // 118 = top area, 235 = bottom area - verticalScrollbar { - x = rel(1) - px(10) - y = rel(0.1) - width = px(5) - height = rel(0.8) - radius = radiusOf(2.5f) - color = colorOf(207, 207, 207) - background { - backgroundColor = colorOf(1f, 1f, 1f, 0.3f) - radius = radiusOf(2.5) - } - } - clipContent = true - } - - // Footer - val footer = component(UIAutoLayout(UIListLayout.ListDirection.Vertical, null)) { - verticalResizing = UIAutoLayout.ResizingMode.Hug - horizontalResizing = UIAutoLayout.ResizingMode.Hug - componentAlignment = UIAlignment.CENTER - componentSpacing = px(6) - - // Promotion - // TODO: Promotion component - - val editHud = component(UIAutoLayout(UIListLayout.ListDirection.Horizontal, null)) { - image("outline/ui", "navbar-icon") - text("Edit HUD", "navbar-text") - - componentAlignment = UIAlignment.CENTER - componentSpacing = px(20) - layoutPadding = paddingOf(15, 37, 15, 37) - - verticalResizing = UIAutoLayout.ResizingMode.Hug - horizontalResizing = UIAutoLayout.ResizingMode.Hug - }.style(UIContainerSheet("edit-hud-layout")) { - control(UIAlignment.BOTTOMCENTER) - background(colorOf(36, 37, 37), radiusOf(15)) - } - }.style(UIContainerSheet("navbar-footer-layout")) { - control(UIAlignment.BOTTOMCENTER) - y -= px(16 + 8) - } - - label("Running Prism Client v1.0.0-Beta").style(UIStyleSheet("nav-prism-version")) { - font("Montserrat", px(8), colorOf(1f, 1f, 1f, 0.8f), left or top, UIFont.FontType.Light) - control(UIAlignment.BOTTOMCENTER) - y -= px(8) - } - }.style(UIContainerSheet("navbar-container")) { - size(px(236), rel(1)) - background(colorOf(32, 32, 32)) - useFBO = true - } - - // Navbar animation - animationOf("navbar-enter", UIContainerSheet()) { - kf { - x = px(-236) - } - UIQuart(1000L) to { - x = px(0) - } - } - - animationOf("navbar-exit", UIContainerSheet()) { - kf { - x = px(0) - } - UIQuart(500L) to { - x = px(-236) - } - } - - UIProvider.dispatchAnimation("navbar-enter", navbar) - } - } +class Default : UIScreen { + override fun build() { + create { + autoLayout(UIListLayout.ListDirection.Vertical) { - /** - * Creates a navbar button - */ - private fun navButton(layout: UIAutoLayout, buttonText: String, imageName: String) { - ucreate { - autoLayout(layout) { - image(imageName, "navbar-icon") - text(buttonText, "navbar-text") } } } From 4576e09a6e1ada9614c9a716399840d3fcfeeea1 Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 21:34:03 -0700 Subject: [PATCH 12/37] Update build.gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b6d9397..5b77d02 100644 --- a/build.gradle +++ b/build.gradle @@ -64,7 +64,7 @@ publishing { maven(MavenPublication) { groupId = 'com.github.Prism-Client' artifactId = 'Aether-UI' - version = '1.2-release' + version = '1.2-Production' from components.kotlin } } From 8bfd72c666e46947be91cd2f4cc15443a6194c16 Mon Sep 17 00:00:00 2001 From: sen Date: Wed, 13 Jul 2022 21:37:10 -0700 Subject: [PATCH 13/37] Fixed compile --- .../net/prismclient/aether/ui/component/type/image/UIImage.kt | 2 +- .../prismclient/aether/ui/component/type/image/UIImageSheet.kt | 2 +- .../aether/ui/component/type/input/button/UICheckbox.kt | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImage.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImage.kt index 85131de..0f5406d 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImage.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImage.kt @@ -37,7 +37,7 @@ class UIImage(name: String, style: String?) : UIComponent(style) { override fun renderComponent() { renderer { - color(style.imageColor) + color(style.imageColor?.rgba ?: -1) renderImage( image, x, y, width, height, style.imageRadius?.topLeft ?: 0f, diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt index 2dc6095..1aacd9e 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt @@ -8,7 +8,7 @@ import net.prismclient.aether.ui.util.UIColor * [UIImageSheet] is the sheet implementation for [UIImage]. * * @author sen - * @since 5/25/2022 + * @since 1.0 */ class UIImageSheet(name: String) : UIStyleSheet(name) { /** diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UICheckbox.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UICheckbox.kt index 61193d6..f77ee04 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UICheckbox.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UICheckbox.kt @@ -3,6 +3,7 @@ package net.prismclient.aether.ui.component.type.input.button import net.prismclient.aether.ui.component.type.image.UIImage import net.prismclient.aether.ui.dsl.UIComponentDSL import net.prismclient.aether.ui.style.UIStyleSheet +import net.prismclient.aether.ui.util.extensions.colorOf open class UICheckbox( checked: Boolean = false, @@ -30,7 +31,7 @@ open class UICheckbox( selectedImage = UIImage(selectedImageName, imageStyle) deselectedImage = UIImage(deselectedImageName, imageStyle) if (deselectedImageName.isEmpty()) // Make the deselected image invisible - deselectedImage.style.imageColor = 0 + deselectedImage.style.imageColor = colorOf(0) UIComponentDSL.pushComponent(selectedImage) UIComponentDSL.pushComponent(deselectedImage) } From c800a7a4b5a8ad5cc1f6db41fbd689ac6936c925 Mon Sep 17 00:00:00 2001 From: sen Date: Thu, 14 Jul 2022 11:07:03 -0700 Subject: [PATCH 14/37] Auto create style sheets --- .../net/prismclient/aether/ui/Aether.kt | 4 +- .../aether/ui/component/UIComponent.kt | 29 ++-- .../aether/ui/component/type/UILabel.kt | 4 +- .../ui/component/type/color/UIColorCursor.kt | 3 - .../ui/component/type/color/UIColorPicker.kt | 10 -- .../type/color/UIColorSwatchSheet.kt | 16 -- .../aether/ui/component/type/image/UIImage.kt | 42 +++-- .../ui/component/type/image/UIImageSheet.kt | 29 ---- .../component/type/input/button/UIButton.kt | 7 +- .../component/type/input/button/UICheckbox.kt | 40 ----- .../type/input/button/UIIconButton.kt | 3 - .../type/input/button/UIImageButton.kt | 23 --- .../type/input/button/UISelectableButton.kt | 37 ----- .../component/type/input/slider/UISlider.kt | 26 ++- .../type/input/slider/UISliderSheet.kt | 28 ---- .../type/input/textfield/UITextField.kt | 50 +++++- .../type/input/textfield/UITextFieldSheet.kt | 48 ------ .../type/layout/{auto => }/UIAutoLayout.kt | 21 +-- .../layout/{container => }/UIContainer.kt | 119 ++++++++++++-- .../ui/component/type/layout/UIFrame.kt | 50 +++++- .../ui/component/type/layout/UIGridLayout.kt | 9 + .../type/layout/{list => }/UIListLayout.kt | 11 +- .../component/type/layout/{tab => }/UITab.kt | 2 +- .../type/layout/grid/UIGridLayout.kt | 155 ------------------ .../type/layout/styles/UIContainerSheet.kt | 102 ------------ .../type/layout/styles/UIFrameSheet.kt | 53 ------ .../type/other/{progress => }/UIProgress.kt | 24 ++- .../type/other/progress/UIProgressSheet.kt | 21 --- .../aether/ui/dsl/UIComponentDSL.kt | 43 ++--- .../ui/renderer/impl/scrollbar/UIScrollbar.kt | 4 +- .../aether/ui/style/UIStyleSheet.kt | 11 +- .../prismclient/aether/ui/util/Shorthands.kt | 67 ++++---- .../net/prismclient/aether/ui/util/Util.kt | 14 ++ src/test/kotlin/examples/Animations.kt | 59 +------ src/test/kotlin/examples/AutoLayouts.kt | 4 +- src/test/kotlin/examples/Default.kt | 12 +- src/test/kotlin/examples/deps/Generic.kt | 6 +- 37 files changed, 404 insertions(+), 782 deletions(-) delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorCursor.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorPicker.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorSwatchSheet.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UICheckbox.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIIconButton.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIImageButton.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UISelectableButton.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/input/slider/UISliderSheet.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextFieldSheet.kt rename src/main/kotlin/net/prismclient/aether/ui/component/type/layout/{auto => }/UIAutoLayout.kt (91%) rename src/main/kotlin/net/prismclient/aether/ui/component/type/layout/{container => }/UIContainer.kt (58%) create mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIGridLayout.kt rename src/main/kotlin/net/prismclient/aether/ui/component/type/layout/{list => }/UIListLayout.kt (90%) rename src/main/kotlin/net/prismclient/aether/ui/component/type/layout/{tab => }/UITab.kt (53%) delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/layout/grid/UIGridLayout.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/layout/styles/UIContainerSheet.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/layout/styles/UIFrameSheet.kt rename src/main/kotlin/net/prismclient/aether/ui/component/type/other/{progress => }/UIProgress.kt (58%) delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/other/progress/UIProgressSheet.kt create mode 100644 src/main/kotlin/net/prismclient/aether/ui/util/Util.kt diff --git a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt index 2b8ecdb..185deb7 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt @@ -5,7 +5,7 @@ import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.component.controller.UIController import net.prismclient.aether.ui.component.type.layout.UIFrame import net.prismclient.aether.ui.component.type.layout.container.UIContainer -import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.container.UIContainerSheet import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.UIRenderer @@ -528,7 +528,7 @@ open class Aether(renderer: UIRenderer) { for (i in 0 until instance.frames!!.size) { // UIContainers are what control scrolling, so // if it is not an instance of it, skip and continue - val container = instance.frames!![i] as? UIContainer<*> ?: continue + val container = instance.frames!![i] as? UIContainer ?: continue if (container.isMouseInsideBounds() && container.expandedHeight > 0f && container.style.overflowY != UIContainerSheet.Overflow.None) { // Iterate through the frame to see if there are more // containers with it. If there are, it will pass true diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt index d2a20c3..5f82ee5 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt @@ -3,9 +3,9 @@ package net.prismclient.aether.ui.component import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.animation.UIAnimation import net.prismclient.aether.ui.component.type.layout.UIFrame +import net.prismclient.aether.ui.component.type.layout.UIFrameSheet import net.prismclient.aether.ui.component.type.layout.container.UIContainer -import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet -import net.prismclient.aether.ui.component.type.layout.styles.UIFrameSheet +import net.prismclient.aether.ui.component.type.layout.container.UIContainerSheet import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.impl.background.UIBackground @@ -37,7 +37,7 @@ import java.util.function.Consumer * @since 1.0 */ @Suppress("UNCHECKED_CAST", "MemberVisibilityCanBePrivate", "LeakingThis") -abstract class UIComponent(style: String?) { +abstract class UIComponent() { /** * The style of the component. */ @@ -193,10 +193,6 @@ abstract class UIComponent(style: String?) { var mouseScrollListeners: HashMap, Float>>? = null protected set - init { - applyStyle(style) - } - /** * Attempts to apply the style to the component. If the style is * empty, or null, a NullPointerException will be thrown when the @@ -246,8 +242,9 @@ abstract class UIComponent(style: String?) { * might request for this method to be invoked. */ open fun update() { - if (!this::style.isInitialized) - throw UninitializedStyleSheetException(this) + if (!this::style.isInitialized) { + style = createsStyle() + } calculateBounds() // Update the size, then the anchor, and then the position @@ -365,7 +362,17 @@ abstract class UIComponent(style: String?) { */ abstract fun renderComponent() - /** Input **/ + /** + * Used to create a new instance of the style sheet provided, [T]. + */ + abstract fun createsStyle(): T + + /** + * Returns true if [style] is intialized. + */ + open fun hasStyle(): Boolean = this::style.isInitialized + + // -- Input -- // /** * Invoked when the mouse moves @@ -431,7 +438,7 @@ abstract class UIComponent(style: String?) { mouseScrollListeners?.forEach { it.value.accept(this, scrollAmount) } } - /** Event **/ + // -- Event -- // /** * Invoked once on the initialization of the component. diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt index 4929747..fac02bd 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt @@ -9,8 +9,10 @@ import net.prismclient.aether.ui.style.UIStyleSheet * @author sen * @since 1.0 */ -class UILabel(var text: String, style: String?) : UIComponent(style) { +class UILabel(var text: String) : UIComponent() { override fun renderComponent() { style.font?.render(text) } + + override fun createsStyle(): UIStyleSheet = UIStyleSheet() } \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorCursor.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorCursor.kt deleted file mode 100644 index 699c807..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorCursor.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.prismclient.aether.ui.component.type.color - -class UIColorCursor \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorPicker.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorPicker.kt deleted file mode 100644 index 5c056bb..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorPicker.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.prismclient.aether.ui.component.type.color - -import net.prismclient.aether.ui.component.UIComponent -import net.prismclient.aether.ui.style.UIStyleSheet - -class UIColorPicker(style: String?) : UIComponent(style) { - override fun renderComponent() { - - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorSwatchSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorSwatchSheet.kt deleted file mode 100644 index 7b82376..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/color/UIColorSwatchSheet.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.prismclient.aether.ui.component.type.color - -import net.prismclient.aether.ui.style.UIStyleSheet -import net.prismclient.aether.ui.util.extensions.setAlpha - -class UIColorSwatchSheet(name: String) : UIStyleSheet(name) { - var swatchColor = -1 - set(value) { - field = value.setAlpha(255) - } - - override fun copy(): UIColorSwatchSheet = UIColorSwatchSheet(name).also { - it.apply(this) - it.swatchColor = swatchColor - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImage.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImage.kt index 0f5406d..3421eda 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImage.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImage.kt @@ -4,16 +4,21 @@ import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.dsl.UIAssetDSL import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.image.UIImageData +import net.prismclient.aether.ui.renderer.impl.property.UIRadius +import net.prismclient.aether.ui.style.UIStyleSheet +import net.prismclient.aether.ui.util.UIColor import net.prismclient.aether.ui.util.extensions.renderer +import net.prismclient.aether.ui.util.name /** - * [UIImage] is the default implementation for rendering images on screen. It accepts` - * an image + * [UIImage] is the default component for rendering images to a screen. It accepts + * the [name] of the image that is to be rendered onto the screen. Alternatively, the + * image can also be loaded with the alternative constructor from a resource file. * * @author sen - * @since 5/20/2022 + * @since 1.0 */ -class UIImage(name: String, style: String?) : UIComponent(style) { +class UIImage(name: String) : UIComponent() { var image: String = name set(value) { field = value @@ -24,12 +29,9 @@ class UIImage(name: String, style: String?) : UIComponent(style) { /** * Loads am image or svg from the specified location with a given name */ - constructor(name: String, location: String, style: String?) : this( - name, - style - ) { - UIAssetDSL.image(name, location) - } + constructor(name: String, location: String) : this( + name + ) { UIAssetDSL.image(name, location) } init { activeImage = UIProvider.getImage(image) @@ -47,4 +49,24 @@ class UIImage(name: String, style: String?) : UIComponent(style) { ) } } + + override fun createsStyle(): UIImageSheet = UIImageSheet() +} + +class UIImageSheet : UIStyleSheet() { + /** + * The color of the image. The default value is RGBA(255, 255, 255) + */ + var imageColor: UIColor? = null + + /** + * The radius of the image. + */ + var imageRadius: UIRadius? = null + + override fun copy() = UIImageSheet().name(name).also { + it.apply(this) + it.imageColor = imageColor + it.imageRadius = imageRadius?.copy() + } } \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt deleted file mode 100644 index 1aacd9e..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/image/UIImageSheet.kt +++ /dev/null @@ -1,29 +0,0 @@ -package net.prismclient.aether.ui.component.type.image - -import net.prismclient.aether.ui.renderer.impl.property.UIRadius -import net.prismclient.aether.ui.style.UIStyleSheet -import net.prismclient.aether.ui.util.UIColor - -/** - * [UIImageSheet] is the sheet implementation for [UIImage]. - * - * @author sen - * @since 1.0 - */ -class UIImageSheet(name: String) : UIStyleSheet(name) { - /** - * The color of the image. The default value is RGBA(255, 255, 255) - */ - var imageColor: UIColor? = null - - /** - * The radius of the image. - */ - var imageRadius: UIRadius? = null - - override fun copy() = UIImageSheet(name).also { - it.apply(this) - it.imageColor = imageColor - it.imageRadius = imageRadius?.copy() - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt index 5eaec0f..0c0082a 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt @@ -4,14 +4,15 @@ import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.style.UIStyleSheet /** - * [UIButton] is a simple class, which is used to create a button. + * [UIButton] is the default implementation of [UIComponent]. It renders the given text to the font. * * @author sen * @since 1.0 - * @param T The stylesheet (used for inheritance) leave as UIStyleSheet. */ -open class UIButton(open var text: String, style: String?) : UIComponent(style) { +open class UIButton(open var text: String) : UIComponent() { override fun renderComponent() { style.font?.render(text) } + + override fun createsStyle(): UIStyleSheet = UIStyleSheet() } \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UICheckbox.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UICheckbox.kt deleted file mode 100644 index f77ee04..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UICheckbox.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.prismclient.aether.ui.component.type.input.button - -import net.prismclient.aether.ui.component.type.image.UIImage -import net.prismclient.aether.ui.dsl.UIComponentDSL -import net.prismclient.aether.ui.style.UIStyleSheet -import net.prismclient.aether.ui.util.extensions.colorOf - -open class UICheckbox( - checked: Boolean = false, - var selectedImageName: String = "checkbox", - var deselectedImageName: String = "", - var imageStyle: String, - style: String? -) : UISelectableButton(checked, "", style) { - lateinit var selectedImage: UIImage - lateinit var deselectedImage: UIImage - - init { - onCheckChange { _, isSelected -> - if (isSelected) { - selectedImage.visible = true - deselectedImage.visible = false - } else { - selectedImage.visible = false - deselectedImage.visible = true - } - } - } - - override fun initialize() { - selectedImage = UIImage(selectedImageName, imageStyle) - deselectedImage = UIImage(deselectedImageName, imageStyle) - if (deselectedImageName.isEmpty()) // Make the deselected image invisible - deselectedImage.style.imageColor = colorOf(0) - UIComponentDSL.pushComponent(selectedImage) - UIComponentDSL.pushComponent(deselectedImage) - } - - override fun renderComponent() {} -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIIconButton.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIIconButton.kt deleted file mode 100644 index c09b6b0..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIIconButton.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.prismclient.aether.ui.component.type.input.button - -class UIIconButton \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIImageButton.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIImageButton.kt deleted file mode 100644 index a8f64f4..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIImageButton.kt +++ /dev/null @@ -1,23 +0,0 @@ -package net.prismclient.aether.ui.component.type.input.button - -import net.prismclient.aether.ui.component.type.image.UIImage -import net.prismclient.aether.ui.style.UIStyleSheet -import net.prismclient.aether.ui.util.ucreate - -/** - * Like [UIButton], but with an image, or icon. - * - * @author sen - * @since 5/9/2022 - */ -class UIImageButton(private val imageName: String, private val imageStyle: String, text: String, style: String?) : - UIButton(text, style) { - lateinit var image: UIImage - - override fun initialize() { - ucreate { - image = image(imageName, style = imageStyle) - } - super.initialize() - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UISelectableButton.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UISelectableButton.kt deleted file mode 100644 index 00d91cc..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UISelectableButton.kt +++ /dev/null @@ -1,37 +0,0 @@ -package net.prismclient.aether.ui.component.type.input.button - -import net.prismclient.aether.ui.style.UIStyleSheet -import java.util.function.BiConsumer - -/** - * [UISelectableButton] is like a [UIButton], except with the ability - * to be selected. To do an action when the button selected, use [onCheckChange] - * which passes this component, and a boolean indicating whether the component - * is selected or not. - * - * @author sen - * @since 5/24/2022 - */ -open class UISelectableButton(checked: Boolean = false, text: String, style: String?) : - UIButton(text, style) { - var checked = checked - set(value) { - field = value - checkListeners?.forEach { it.accept(this, checked) } - } - var checkListeners: MutableList, Boolean>>? = null - - init { - onMousePressed { - if (isMouseInside()) { - this.checked = !this.checked - } - } - } - - open fun onCheckChange(block: BiConsumer, Boolean>): UISelectableButton { - checkListeners = checkListeners ?: mutableListOf() - checkListeners!!.add(block) - return this - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/slider/UISlider.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/slider/UISlider.kt index b1737bf..40dfe84 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/slider/UISlider.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/slider/UISlider.kt @@ -2,6 +2,9 @@ package net.prismclient.aether.ui.component.type.input.slider import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.event.input.UIMouseEvent +import net.prismclient.aether.ui.style.UIStyleSheet +import net.prismclient.aether.ui.util.Block +import net.prismclient.aether.ui.util.name import java.util.function.BiConsumer import kotlin.math.roundToInt @@ -17,16 +20,15 @@ import kotlin.math.roundToInt * @see UISliderShape */ open class UISlider( - value: Double, var range: ClosedFloatingPointRange, var step: Double, style: String? -) : UIComponent(style) { + value: Double, var range: ClosedFloatingPointRange, var step: Double +) : UIComponent() { /** * The value of this slider. */ var value: Double = 0.0 set(value) { val different = value != field - field = ((value / step.coerceAtLeast(Double.MIN_VALUE)).roundToInt() * step) - .coerceAtLeast(range.start) + field = ((value / step.coerceAtLeast(Double.MIN_VALUE)).roundToInt() * step).coerceAtLeast(range.start) .coerceAtMost(range.endInclusive) // normalizedValue = value / (range.endInclusive - range.start) if (different) valueChangeListeners?.forEach { it.value.accept(this, value) } @@ -110,4 +112,20 @@ open class UISlider( valueChangeListeners = valueChangeListeners ?: hashMapOf() valueChangeListeners!![eventName] = event } + + override fun createsStyle(): UISliderSheet = UISliderSheet() +} + +class UISliderSheet : UIStyleSheet() { + /** + * The slider shape. The [UISliderShape.x] dictates the offset of the slider. + */ + var control: UISliderShape = UISliderShape() + + inline fun control(block: Block) = control.block() + + override fun copy(): UISliderSheet = UISliderSheet().name(name).also { + it.apply(this) + it.control = control.copy() + } } \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/slider/UISliderSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/slider/UISliderSheet.kt deleted file mode 100644 index a3efd8e..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/slider/UISliderSheet.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.prismclient.aether.ui.component.type.input.slider - -import net.prismclient.aether.ui.style.UIStyleSheet -import net.prismclient.aether.ui.util.Block - -/** - * [UISliderSheet] is the corresponding sheet to [UISlider]. It contains the slider shape - * which is the shape which is moved to control the value of the slider - * - * @author sen - * @since 1.0 - * @see UISlider - * @see UISliderShape - * @see UISliderSheet.control The slider shape. - */ -class UISliderSheet @JvmOverloads constructor(name: String = "") : UIStyleSheet(name) { - /** - * The slider shape. The [UISliderShape.x] dictates the offset of the slider. - */ - var control: UISliderShape = UISliderShape() - - inline fun control(block: Block) = control.block() - - override fun copy(): UISliderSheet = UISliderSheet(name).also { - it.apply(this) - it.control = control.copy() - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt index 1f85f76..4b007a1 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt @@ -1,12 +1,19 @@ package net.prismclient.aether.ui.component.type.input.textfield import net.prismclient.aether.ui.Aether +import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.component.type.input.button.UIButton +import net.prismclient.aether.ui.component.type.input.textfield.caret.UICaret import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.impl.font.UIFont +import net.prismclient.aether.ui.style.UIStyleSheet +import net.prismclient.aether.ui.util.UIColor +import net.prismclient.aether.ui.util.extensions.em +import net.prismclient.aether.ui.util.extensions.px import net.prismclient.aether.ui.util.extensions.renderer import net.prismclient.aether.ui.util.input.UIModifierKey import net.prismclient.aether.ui.util.interfaces.UIFocusable +import net.prismclient.aether.ui.util.name import net.prismclient.aether.ui.util.warn import java.lang.Integer.max import java.lang.Integer.min @@ -20,12 +27,11 @@ import java.util.function.Consumer * length of the overall text. * * @author sen - * @since 6/6/2022 + * @since 1.0 * @see UITextField.filter Pre-made text filters. */ -open class UITextField(text: String, var placeholder: String? = null, var filter: TextFilter, style: String?) : - UIButton(text, style), UIFocusable { - override var text: String = text +open class UITextField(text: String, var placeholder: String? = null, var filter: TextFilter) : UIComponent(), UIFocusable { + var text: String = text set(value) { field = value textChangedListener?.forEach { it.value.accept(this) } @@ -231,6 +237,8 @@ open class UITextField(text: String, var placeholder: String? = null, var filter textChangedListener!![eventName] = event } + override fun createsStyle(): UITextFieldSheet = UITextFieldSheet() + /** * [TextFilter] holds a string which is compared to the input character. If the character * is found within the string, it will be added to the text field, else it will not. Furthermore, @@ -259,4 +267,38 @@ open class UITextField(text: String, var placeholder: String? = null, var filter @JvmStatic val hex = TextFilter("#ABCDEFabcdef0123456789") } +} + +class UITextFieldSheet : UIStyleSheet() { + /** + * The caret shape which is drawn to display the caret. + */ + var caret: UICaret = UICaret().apply { + this.width = px(2) + this.height = em(1) + } + + /** + * The rate at which the caret blinks at. 0 = never + */ + var blinkRate: Long = 500L + + /** + * The color of the text when the text field is not focused + */ + var placeholderColor: UIColor? = null + + /** + * Creates a caret DSL block. + */ + inline fun caret(block: UICaret.() -> Unit) { + block.invoke(caret) + } + + override fun copy(): UITextFieldSheet = UITextFieldSheet().name(name).also { + it.apply(this) + it.caret = caret.copy() + it.blinkRate = blinkRate + it.placeholderColor = placeholderColor + } } \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextFieldSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextFieldSheet.kt deleted file mode 100644 index 0ae3407..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextFieldSheet.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.prismclient.aether.ui.component.type.input.textfield - -import net.prismclient.aether.ui.component.type.input.textfield.caret.UICaret -import net.prismclient.aether.ui.style.UIStyleSheet -import net.prismclient.aether.ui.util.UIColor -import net.prismclient.aether.ui.util.extensions.em -import net.prismclient.aether.ui.util.extensions.px - -/** - * The corresponding style sheet for text fields. It contains basic styling information - * such as the placeholder color. It also contains caret controls. - * - * @author sen - * @since 5/11/2022 - */ -class UITextFieldSheet(name: String) : UIStyleSheet(name) { - /** - * The caret shape which is drawn to display the caret. - */ - var caret: UICaret = UICaret().apply { - this.width = px(2) - this.height = em(1) - } - - /** - * The rate at which the caret blinks at. 0 = never - */ - var blinkRate: Long = 500L - - /** - * The color of the text when the text field is not focused - */ - var placeholderColor: UIColor? = null - - /** - * Creates a caret DSL block. - */ - inline fun caret(block: UICaret.() -> Unit) { - block.invoke(caret) - } - - override fun copy(): UITextFieldSheet = UITextFieldSheet(name).also { - it.apply(this) - it.caret = caret.copy() - it.blinkRate = blinkRate - it.placeholderColor = placeholderColor - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt similarity index 91% rename from src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayout.kt rename to src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt index a53cbe6..a731d21 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/auto/UIAutoLayout.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt @@ -1,13 +1,12 @@ -package net.prismclient.aether.ui.component.type.layout.auto +package net.prismclient.aether.ui.component.type.layout import net.prismclient.aether.ui.component.UIComponent -import net.prismclient.aether.ui.component.type.layout.list.UIListLayout -import net.prismclient.aether.ui.component.type.layout.styles.UIFrameSheet import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.component.util.enums.UIAlignment.* import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.impl.property.UIPadding import net.prismclient.aether.ui.util.interfaces.UICopy +import net.prismclient.aether.ui.util.name /** * [UIAutoLayout] is a layout which is designed to mimic the behavior of Figma's auto @@ -27,10 +26,8 @@ import net.prismclient.aether.ui.util.interfaces.UICopy * @author sen * @since 1.1 */ -class UIAutoLayout @JvmOverloads constructor( - listDirection: ListDirection = ListDirection.Horizontal, - style: String? -) : UIListLayout(listDirection, ListOrder.Forward, style), UICopy { +class UIAutoLayout @JvmOverloads constructor(listDirection: ListDirection = ListDirection.Horizontal) : + UIListLayout(listDirection, ListOrder.Forward), UICopy { /** * Defines how the width should be sized. [ResizingMode.Hug] resizes based on the components and * the padding and spacing properties, and [ResizingMode.Fixed] acts like a normal component. @@ -102,17 +99,15 @@ class UIAutoLayout @JvmOverloads constructor( h = if (listDirection == ListDirection.Vertical) { h + component.relHeight + component.marginTop + component.marginBottom + if (i < components.size - 1) spacing else 0f } else { - (component.relHeight + component.marginTop + component.marginBottom).coerceAtLeast(h) + (component.relHeight + component.marginTop + component.marginBottom).coerceAtLeast(h) } } } // Adjust the width and/or height of the component based on the calculated // size, and ensure that the size is at least the size prior to this. - if (horizontalResizing == ResizingMode.Hug) - width = (w + left + right).coerceAtLeast(width) - if (verticalResizing == ResizingMode.Hug) - height = (h + top + bottom).coerceAtLeast(height) + if (horizontalResizing == ResizingMode.Hug) width = (w + left + right).coerceAtLeast(width) + if (verticalResizing == ResizingMode.Hug) height = (h + top + bottom).coerceAtLeast(height) // Update calculateBounds() @@ -173,7 +168,7 @@ class UIAutoLayout @JvmOverloads constructor( /** * Copy the properties of this layout to a new one (excluding components). */ - override fun copy(): UIAutoLayout = UIAutoLayout(listDirection, style.name).also { + override fun copy(): UIAutoLayout = UIAutoLayout(listDirection).also { // UIAutoLayout properties it.horizontalResizing = horizontalResizing it.verticalResizing = verticalResizing diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt similarity index 58% rename from src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt rename to src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt index 502be27..8469342 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/container/UIContainer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt @@ -1,27 +1,24 @@ package net.prismclient.aether.ui.component.type.layout.container -import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.component.type.layout.UIFrame -import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.UIFrameSheet import net.prismclient.aether.ui.component.util.interfaces.UILayout import net.prismclient.aether.ui.event.input.UIMouseEvent -import net.prismclient.aether.ui.util.extensions.asRGBA +import net.prismclient.aether.ui.renderer.impl.scrollbar.UIScrollbar +import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.util.extensions.renderer import net.prismclient.aether.ui.util.interfaces.UIFocusable /** - * [UIContainer] is the default implementation for [UIFrame]. It introduces - * scrollbars which automatically resize to content being added/removed. It is - * considered to be a [UIFocusable], so when the mouse is scrolled within the - * container the focused component will become this. - * - * + * [UIContainer] is the default implementation for [UIFrame]. It introduces scrollbars which automatically + * resize to content being added/removed. It is considered to be a [UIFocusable], so when the mouse is scrolled + * within the container the focused component will become this. * * @author sen - * @since 5/12/2022 + * @since 1.0 */ -open class UIContainer(style: String?) : UIFrame(style), UIFocusable, UILayout { +open class UIContainer : UIFrame(), UIFocusable, UILayout { /** * How sensitive the scrolling will be */ @@ -106,12 +103,18 @@ open class UIContainer(style: String?) : UIFrame(style) renderer { if (style.clipContent) { scissor(relX, relY, relWidth, relHeight) { - translate(-(style.horizontalScrollbar.value * expandedWidth), -(style.verticalScrollbar.value * expandedHeight)) { + translate( + -(style.horizontalScrollbar.value * expandedWidth), + -(style.verticalScrollbar.value * expandedHeight) + ) { components.forEach(UIComponent<*>::render) } } } else { - translate(-(style.horizontalScrollbar.value * expandedWidth), -(style.verticalScrollbar.value * expandedHeight)) { + translate( + -(style.horizontalScrollbar.value * expandedWidth), + -(style.verticalScrollbar.value * expandedHeight) + ) { components.forEach(UIComponent<*>::render) } } @@ -167,4 +170,94 @@ open class UIContainer(style: String?) : UIFrame(style) } super.mouseScrolled(mouseX, mouseY, scrollAmount) } + + override fun createsStyle(): T = UIContainerSheet() as T +} + +open class UIContainerSheet : UIFrameSheet() { + /** + * Describes when to introduce the scrollbar + * + * @see Overflow + */ + var overflowX: Overflow = Overflow.Auto + + /** + * Describes when to introduce the scrollbar + * + * @see Overflow + */ + var overflowY: Overflow = Overflow.Auto + + var verticalScrollbar: UIScrollbar = UIScrollbar(UIScrollbar.Scrollbar.Vertical) + var horizontalScrollbar: UIScrollbar = UIScrollbar(UIScrollbar.Scrollbar.Horizontal) + + /** + * Creates a DSL block for the vertical scrollbar + */ + inline fun verticalScrollbar(block: UIScrollbar.() -> Unit) = verticalScrollbar.block() + + /** + * Creates a DSL block for the horizontal scrollbar + */ + inline fun horizontalScrollbar(block: UIScrollbar.() -> Unit) = horizontalScrollbar.block() + + /** + * [Overflow] defines what the vertical, and horizontal scrollbars + * are supposed to do when content leaves the screen. Check the enum + * documentation for more information. + * + * @author sen + * @since 5/12/2022 + */ + enum class Overflow { + /** + * Does not introduce a scrollbar on the given axis + */ + None, + + /** + * Creates a scrollbar on the given axis regardless if content leaves the window + */ + Always, + + /** + * Like scroll, but only adds the scrollbar on the given axis if content leaves the window + */ + Auto + } + + override fun apply(sheet: UIStyleSheet): UIContainerSheet { + // Override the default apply function because + // this is an inheritable class. + this.immutableStyle = sheet.immutableStyle + this.name = sheet.name + + this.background = sheet.background?.copy() + this.font = sheet.font?.copy() + + this.x = sheet.x?.copy() + this.y = sheet.y?.copy() + this.width = sheet.width?.copy() + this.height = sheet.height?.copy() + + this.padding = sheet.padding?.copy() + this.margin = sheet.margin?.copy() + this.anchor = sheet.anchor?.copy() + this.clipContent = sheet.clipContent + + // Frame properties + if (sheet is UIContainerSheet) { + this.useFBO = sheet.useFBO + this.optimizeRenderer = sheet.optimizeRenderer + this.overflowX = sheet.overflowX + this.overflowY = sheet.overflowY + this.verticalScrollbar = sheet.verticalScrollbar.copy() + this.horizontalScrollbar = sheet.horizontalScrollbar.copy() + } + + return this + } + + override fun copy(): UIContainerSheet = UIContainerSheet().apply(this) } \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt index fbc1ad2..b97d2f1 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt @@ -2,12 +2,13 @@ package net.prismclient.aether.ui.component.type.layout import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.component.UIComponent -import net.prismclient.aether.ui.component.type.layout.styles.UIFrameSheet import net.prismclient.aether.ui.dsl.UIRendererDSL import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.other.UIContentFBO +import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.util.extensions.renderer import net.prismclient.aether.ui.util.interfaces.UIFocusable +import net.prismclient.aether.ui.util.name import net.prismclient.aether.ui.util.warn /** @@ -27,7 +28,7 @@ import net.prismclient.aether.ui.util.warn * @author sen * @since 1.0 */ -abstract class UIFrame(style: String?) : UIComponent(style), UIFocusable { +abstract class UIFrame : UIComponent(), UIFocusable { /** * The components of this frame. */ @@ -165,4 +166,49 @@ abstract class UIFrame(style: String?) : UIComponent(style) components.forEach { it.mouseScrolled(mouseX, mouseY, scrollAmount) } requestUpdate() } + + override fun createsStyle(): T = UIFrameSheet() as T +} + +open class UIFrameSheet : UIStyleSheet() { + /** + * If true, the frame will use an FBO to render content. + */ + var useFBO: Boolean = false + + /** + * If true certain optimizations will be applied when + * rendering. This only works with [useFBO] as true. + */ + var optimizeRenderer: Boolean = true + + override fun apply(sheet: UIStyleSheet): UIFrameSheet { + // Override the default apply function because + // this is an inheritable class. + this.immutableStyle = sheet.immutableStyle + this.name = sheet.name + + this.background = sheet.background?.copy() + this.font = sheet.font?.copy() + + this.x = sheet.x?.copy() + this.y = sheet.y?.copy() + this.width = sheet.width?.copy() + this.height = sheet.height?.copy() + + this.padding = sheet.padding?.copy() + this.margin = sheet.margin?.copy() + this.anchor = sheet.anchor?.copy() + this.clipContent = sheet.clipContent + + // Frame properties + if (sheet is UIFrameSheet) { + this.useFBO = sheet.useFBO + this.optimizeRenderer = sheet.optimizeRenderer + } + + return this + } + + override fun copy(): UIFrameSheet = UIFrameSheet().name(name).apply(this) } \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIGridLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIGridLayout.kt new file mode 100644 index 0000000..2c39452 --- /dev/null +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIGridLayout.kt @@ -0,0 +1,9 @@ +package net.prismclient.aether.ui.component.type.layout + +/** + * + * + * @author sen + * @since 1.3 + */ +class UIGridLayout \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/list/UIListLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt similarity index 90% rename from src/main/kotlin/net/prismclient/aether/ui/component/type/layout/list/UIListLayout.kt rename to src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt index 692b393..a1d3e82 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/list/UIListLayout.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt @@ -1,9 +1,9 @@ -package net.prismclient.aether.ui.component.type.layout.list +package net.prismclient.aether.ui.component.type.layout import net.prismclient.aether.ui.component.type.layout.container.UIContainer -import net.prismclient.aether.ui.component.type.layout.list.UIListLayout.ListOrder.Backwards -import net.prismclient.aether.ui.component.type.layout.list.UIListLayout.ListOrder.Forward -import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.UIListLayout.ListOrder.Backwards +import net.prismclient.aether.ui.component.type.layout.UIListLayout.ListOrder.Forward +import net.prismclient.aether.ui.component.type.layout.container.UIContainerSheet import net.prismclient.aether.ui.unit.UIUnit /** @@ -16,8 +16,7 @@ import net.prismclient.aether.ui.unit.UIUnit open class UIListLayout @JvmOverloads constructor( var listDirection: ListDirection = ListDirection.Vertical, var listOrder: ListOrder = Forward, - style: String? -) : UIContainer(style) { +) : UIContainer() { /** * The spacing between each component in the layout. */ diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/tab/UITab.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UITab.kt similarity index 53% rename from src/main/kotlin/net/prismclient/aether/ui/component/type/layout/tab/UITab.kt rename to src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UITab.kt index 2b1a715..c47a21e 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/tab/UITab.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UITab.kt @@ -1,3 +1,3 @@ -package net.prismclient.aether.ui.component.type.layout.tab +package net.prismclient.aether.ui.component.type.layout class UITab // https://chakra-ui.com/docs/components/disclosure/tabs \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/grid/UIGridLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/grid/UIGridLayout.kt deleted file mode 100644 index 624d639..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/grid/UIGridLayout.kt +++ /dev/null @@ -1,155 +0,0 @@ -package net.prismclient.aether.ui.component.type.layout.grid - -class UIGridLayout - -//open class UIGridLayoutComponent(style: String?) : UIContainer(style) { -// protected var columns: Array = arrayOf() -// protected var rows: Array = arrayOf() -// -// var columnSpacing: UIPixel = px(0) -// var rowSpacing: UIPixel = px(0) -// -// var retainSpace = true /* If there is a missing component the grid will take the previous row's spacing to fill */ -// -// fun width(): Float = width -// -// fun height(): Float = height -// -// override fun updateLayout() { -// var i = 0 -// -// val leftoverWidth = FloatArray(rows.size) { 0f } -// var leftoverHeight = 0f -// val heights = FloatArray(rows.size) { 0f } -// var fractionWidthValue = 0f -// var fractionHeightValue = 0f -// var widthAutoCount = 0 -// var heightAutoCount = 0 -// -// var hoffset = 0f -// for ((r, row) in rows.withIndex()) { -// var woffset = 0f -// for ((c, column) in columns.withIndex()) { -// val child = children.getOrNull(i).also { -// it?.updateSize() -// it?.overrided = true -// } -// var w = when (column.type) { -// INITIAL -> width(i) -// PIXELS -> column.value -// RELATIVE -> column.value * width() -// FRACTION -> { -// if (r == 0) { fractionWidthValue += column.value } -// width(i) -// } -// AUTO -> { -// if (r == 0) { widthAutoCount++ } -// width(i) -// } -// else -> throw UnsupportedOperationException() -// } -// var h = when (row.type) { -// INITIAL -> height(i) -// PIXELS -> row.value -// RELATIVE -> row.value * height() -// FRACTION, AUTO -> height(i) -// else -> throw UnsupportedOperationException() -// } -// -// if (child != null) { -// if (heights[r] < h) { -// heights[r] = h -// } -// -// child.x = x + woffset -// child.y = y// + hoffset -// child.width = w -// child.height = h -// w = child.relWidth -// } -// -// woffset += w + if (c < columns.size - 1) columnSpacing.calculate(width) else 0f -// -// i++ -// } -// if (row.type == AUTO) { -// heightAutoCount++ -// } else if (row.type == FRACTION) { -// fractionHeightValue += row.value -// } -// leftoverWidth[r] = width() - woffset -// hoffset += heights[r] + if (r < rows.size - 2) rowSpacing.calculate(height) else 0f -// } -// leftoverHeight = height() - hoffset -// -// i = 0 -// var hp = 0f -// -// for ((r, row) in rows.withIndex()) { -// val widthFractionPush = leftoverWidth[r] / max(1f, fractionWidthValue) -// val heightFractionPush = leftoverHeight / max(1f, fractionHeightValue) -// var totalHeight = 0f -// for (column in columns) { -// val child = children.getOrNull(i) -// -// if (child != null) { -// child.height = heights[r] -// -// var wpush = 0f -// var hpush = 0f -// -// if (column.type == AUTO) { -// if (fractionWidthValue < 1f) { -// wpush = (leftoverWidth[r] - (widthFractionPush * fractionWidthValue)) / widthAutoCount -// } -// } else if (column.type == FRACTION) { -// wpush = widthFractionPush * column.value -// } -// -// if (row.type == AUTO) { -// if (fractionHeightValue < 1f) { -// hpush = (leftoverHeight / heightAutoCount) -// } -// } else if (row.type == FRACTION) { -// hpush = heightFractionPush * row.value//leftoverHeight * row.value -// } -// -// if (wpush != 0f || hpush != 0f) { -// child.y += hp -// child.width += wpush -// child.height += hpush -// totalHeight = child.height -// -// -// for (j in i + 1 until (columns.size * (r + 1))) { -// (children.getOrNull(j) ?: break).x += wpush -// } -// } -// } -// i++ -// } -// hp += totalHeight + if (r < rows.size - 1) rowSpacing.calculate(height) else 0f -// } -// children.forEach(UIComponent::update) -// } -// -// private fun width(index: Int) = children.getOrNull(index)?.width ?: 0f -// -// private fun height(index: Int) = children.getOrNull(index)?.height ?: 0f -// -// fun column(vararg units: UIPixel) { -// columns = arrayOf(*units) -// } -// -// fun row(vararg units: UIPixel) { -// rows = arrayOf(*units) -// } -// -// fun acolumn(units: Array) { -// columns = units -// } -// -// fun arow(units: Array) { -// rows = units -// } -//} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/styles/UIContainerSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/styles/UIContainerSheet.kt deleted file mode 100644 index 210ac5e..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/styles/UIContainerSheet.kt +++ /dev/null @@ -1,102 +0,0 @@ -package net.prismclient.aether.ui.component.type.layout.styles - -import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet.Overflow -import net.prismclient.aether.ui.renderer.impl.scrollbar.UIScrollbar -import net.prismclient.aether.ui.style.UIStyleSheet - -/** - * [UIContainerSheet] is the corresponding style sheet for containers. It provides - * properties for the scrollbar and when to introduce them - * - * @author sen - * @since 5/12/2022 - * @see UIScrollbar - * @see Overflow - */ -open class UIContainerSheet @JvmOverloads constructor(name: String = "") : UIFrameSheet(name) { - /** - * Describes when to introduce the scrollbar - * - * @see Overflow - */ - var overflowX: Overflow = Overflow.Auto - - /** - * Describes when to introduce the scrollbar - * - * @see Overflow - */ - var overflowY: Overflow = Overflow.Auto - - var verticalScrollbar: UIScrollbar = UIScrollbar(UIScrollbar.Scrollbar.Vertical) - var horizontalScrollbar: UIScrollbar = UIScrollbar(UIScrollbar.Scrollbar.Horizontal) - - /** - * Creates a DSL block for the vertical scrollbar - */ - inline fun verticalScrollbar(block: UIScrollbar.() -> Unit) = verticalScrollbar.block() - - /** - * Creates a DSL block for the horizontal scrollbar - */ - inline fun horizontalScrollbar(block: UIScrollbar.() -> Unit) = horizontalScrollbar.block() - - /** - * [Overflow] defines what the vertical, and horizontal scrollbars - * are supposed to do when content leaves the screen. Check the enum - * documentation for more information. - * - * @author sen - * @since 5/12/2022 - */ - enum class Overflow { - /** - * Does not introduce a scrollbar on the given axis - */ - None, - - /** - * Creates a scrollbar on the given axis regardless if content leaves the window - */ - Always, - - /** - * Like scroll, but only adds the scrollbar on the given axis if content leaves the window - */ - Auto - } - - override fun apply(sheet: UIStyleSheet): UIContainerSheet { - // Override the default apply function because - // this is an inheritable class. - this.immutableStyle = sheet.immutableStyle - this.name = sheet.name - - this.background = sheet.background?.copy() - this.font = sheet.font?.copy() - - this.x = sheet.x?.copy() - this.y = sheet.y?.copy() - this.width = sheet.width?.copy() - this.height = sheet.height?.copy() - - this.padding = sheet.padding?.copy() - this.margin = sheet.margin?.copy() - this.anchor = sheet.anchor?.copy() - this.clipContent = sheet.clipContent - - // Frame properties - if (sheet is UIContainerSheet) { - this.useFBO = sheet.useFBO - this.optimizeRenderer = sheet.optimizeRenderer - this.overflowX = sheet.overflowX - this.overflowY = sheet.overflowY - this.verticalScrollbar = sheet.verticalScrollbar.copy() - this.horizontalScrollbar = sheet.horizontalScrollbar.copy() - } - - return this - } - - override fun copy(): UIContainerSheet = UIContainerSheet(name).apply(this) -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/styles/UIFrameSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/styles/UIFrameSheet.kt deleted file mode 100644 index ff31030..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/styles/UIFrameSheet.kt +++ /dev/null @@ -1,53 +0,0 @@ -package net.prismclient.aether.ui.component.type.layout.styles - -import net.prismclient.aether.ui.style.UIStyleSheet - -/** - * [UIFrameSheet] is the corresponding style sheet for frames. See field - * documentation for more information. - * - * @author sen - * @since 1.0 - */ -open class UIFrameSheet(name: String) : UIStyleSheet(name) { - /** - * If true, the frame will use an FBO to render content. - */ - var useFBO: Boolean = false - - /** - * If true certain optimizations will be applied when - * rendering. This only works with [useFBO] as true. - */ - var optimizeRenderer: Boolean = true - - override fun apply(sheet: UIStyleSheet): UIFrameSheet { - // Override the default apply function because - // this is an inheritable class. - this.immutableStyle = sheet.immutableStyle - this.name = sheet.name - - this.background = sheet.background?.copy() - this.font = sheet.font?.copy() - - this.x = sheet.x?.copy() - this.y = sheet.y?.copy() - this.width = sheet.width?.copy() - this.height = sheet.height?.copy() - - this.padding = sheet.padding?.copy() - this.margin = sheet.margin?.copy() - this.anchor = sheet.anchor?.copy() - this.clipContent = sheet.clipContent - - // Frame properties - if (sheet is UIFrameSheet) { - this.useFBO = sheet.useFBO - this.optimizeRenderer = sheet.optimizeRenderer - } - - return this - } - - override fun copy(): UIFrameSheet = UIFrameSheet(name).apply(this) -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/other/progress/UIProgress.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/other/UIProgress.kt similarity index 58% rename from src/main/kotlin/net/prismclient/aether/ui/component/type/other/progress/UIProgress.kt rename to src/main/kotlin/net/prismclient/aether/ui/component/type/other/UIProgress.kt index 583f357..69bd0d2 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/other/progress/UIProgress.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/other/UIProgress.kt @@ -1,7 +1,10 @@ -package net.prismclient.aether.ui.component.type.other.progress +package net.prismclient.aether.ui.component.type.other import net.prismclient.aether.ui.component.UIComponent +import net.prismclient.aether.ui.style.UIStyleSheet +import net.prismclient.aether.ui.util.UIColor import net.prismclient.aether.ui.util.extensions.renderer +import net.prismclient.aether.ui.util.name /** * [UIProgress] is a component which displays the distance between the x and width @@ -12,14 +15,27 @@ import net.prismclient.aether.ui.util.extensions.renderer * To configure the color see [UIProgressSheet.progressColor]. * * @author sen - * @since 6/23/2022 + * @since 1.0 */ -class UIProgress @JvmOverloads constructor(var progress: Float = 0f, style: String?) : - UIComponent(style) { +class UIProgress @JvmOverloads constructor(var progress: Float = 0f) : UIComponent() { override fun renderComponent() { renderer { color(style.progressColor) rect(relX, relY, relWidth * progress, relHeight, style.background?.radius) } } + + override fun createsStyle(): UIProgressSheet = UIProgressSheet() +} + +class UIProgressSheet : UIStyleSheet() { + /** + * The color of the actual progress bar. + */ + var progressColor: UIColor? = null + + override fun copy() = UIProgressSheet().name(name).also { + it.apply(this) + it.progressColor = progressColor + } } \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/other/progress/UIProgressSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/other/progress/UIProgressSheet.kt deleted file mode 100644 index 44b73e9..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/other/progress/UIProgressSheet.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.prismclient.aether.ui.component.type.other.progress - -import net.prismclient.aether.ui.style.UIStyleSheet - -/** - * [UIProgressSheet] is the corresponding sheet to [UIProgress]. - * - * @author sen - * @since 6/23/2022 - */ -class UIProgressSheet(name: String) : UIStyleSheet(name) { - /** - * The color of the progress - */ - var progressColor = -1 - - override fun copy() = UIProgressSheet(name).also { - it.apply(this) - it.progressColor = progressColor - } -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt index b07acfb..093d8c8 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt @@ -7,13 +7,12 @@ import net.prismclient.aether.ui.component.controller.impl.selection.UISelectabl import net.prismclient.aether.ui.component.type.UILabel import net.prismclient.aether.ui.component.type.image.UIImage import net.prismclient.aether.ui.component.type.input.button.UIButton -import net.prismclient.aether.ui.component.type.input.button.UICheckbox import net.prismclient.aether.ui.component.type.input.slider.UISlider import net.prismclient.aether.ui.component.type.layout.UIFrame -import net.prismclient.aether.ui.component.type.layout.auto.UIAutoLayout +import net.prismclient.aether.ui.component.type.layout.UIAutoLayout import net.prismclient.aether.ui.component.type.layout.container.UIContainer -import net.prismclient.aether.ui.component.type.layout.list.UIListLayout -import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.UIListLayout +import net.prismclient.aether.ui.component.type.layout.container.UIContainerSheet import net.prismclient.aether.ui.dsl.UIComponentDSL.activeStyle import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.util.Block @@ -137,8 +136,9 @@ object UIComponentDSL { * * @return T The component */ - inline fun > component(component: T, block: Block): T { + inline fun > component(component: T, style: String?, block: Block): T { pushComponent(component) + component.applyStyle(style) component.block() component.initialize() popComponent(component) @@ -208,7 +208,7 @@ object UIComponentDSL { */ @JvmOverloads inline fun text(text: String, style: String? = activeStyle, block: Block = {}) = - component(UILabel(text, style), block) + component(UILabel(text), style, block) /** * An alternative to [text]. Creates a [UILabel] with the provided [text]. @@ -217,26 +217,13 @@ object UIComponentDSL { */ @JvmOverloads inline fun label(text: String, style: String? = activeStyle, block: Block = {}) = - component(UILabel(text, style), block) + component(UILabel(text), style, block) /** * Creates a [UIButton] with the provided [text], like a label. */ - inline fun button(text: String, style: String? = activeStyle, block: Block> = {}) = - component(UIButton(text, style), block) - - /** - * Creates a [UICheckbox] from the given [selectedImageName], [deselectedImageName] and [imageStyle]. - */ - @JvmOverloads - inline fun checkbox( - checked: Boolean = false, - selectedImageName: String = "checkbox", - deselectedImageName: String = "", - imageStyle: String, - style: String? = activeStyle, - block: Block = {} - ) = component(UICheckbox(checked, selectedImageName, deselectedImageName, imageStyle, style), block) + inline fun button(text: String, style: String? = activeStyle, block: Block = {}) = + component(UIButton(text), style, block) /** * Creates a [UISlider] with the given [value] which stays within the [range] and steps by the @@ -249,21 +236,21 @@ object UIComponentDSL { step: Number, style: String? = activeStyle, block: Block = {} - ) = component(UISlider(value.toDouble(), range, step.toDouble(), style), block) + ) = component(UISlider(value.toDouble(), range, step.toDouble()), style, block) /** * Creates a [UIImage] with the [imageName] as the image to be rendered. */ @JvmOverloads inline fun image(imageName: String, style: String? = activeStyle, block: Block = {}) = - component(UIImage(imageName, style), block) + component(UIImage(imageName), style, block) /** * Creates a [UIContainer]. Anything within the block will be added to the component list within this. */ @JvmOverloads inline fun container(style: String? = activeStyle, block: Block> = {}) = - component(UIContainer(style), block) + component(UIContainer(), style, block) /** * Creates a [UIListLayout] with the given [listDirection], which defines the direction that it @@ -276,7 +263,7 @@ object UIComponentDSL { listOrder: UIListLayout.ListOrder = UIListLayout.ListOrder.Forward, style: String? = activeStyle, block: Block = {} - ) = component(UIListLayout(listDirection, listOrder, style), block) + ) = component(UIListLayout(listDirection, listOrder), style, block) /** * Creates a vertical list via [list]. @@ -305,7 +292,7 @@ object UIComponentDSL { * components can be defined. */ @JvmOverloads - inline fun autoLayout(layout: UIAutoLayout, block: Block = {}) = component(layout.copy(), block) + inline fun autoLayout(layout: UIAutoLayout, block: Block = {}) = component(layout.copy(), activeStyle, block) /** * Creates an [UIAutoLayout] from the given [listDirection]. [UIAutoLayout] are designed to mimic Figma's @@ -318,7 +305,7 @@ object UIComponentDSL { listDirection: UIListLayout.ListDirection, style: String? = null, block: Block = {} - ) = component(UIAutoLayout(listDirection, style), block) + ) = component(UIAutoLayout(listDirection), style, block) /** * Creates a [UISelectableController] which is a controller that has a single selected diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt index a2e837d..cbb1f59 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt @@ -2,7 +2,7 @@ package net.prismclient.aether.ui.renderer.impl.scrollbar import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.component.type.layout.container.UIContainer -import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.container.UIContainerSheet import net.prismclient.aether.ui.renderer.impl.background.UIBackground import net.prismclient.aether.ui.renderer.impl.border.UIBorder import net.prismclient.aether.ui.renderer.impl.property.UIRadius @@ -56,7 +56,7 @@ class UIScrollbar(val type: Scrollbar) : UIColoredShape() { } fun shouldRender() { - val container = component as UIContainer<*> + val container = component as UIContainer // Check based on the overflow if the scrollbar should be rendered or not shouldRender = if (type == Scrollbar.Vertical) { diff --git a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt index eb9ed72..beb56f9 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt @@ -2,7 +2,7 @@ package net.prismclient.aether.ui.style import net.prismclient.aether.ui.animation.UIAnimation import net.prismclient.aether.ui.component.UIComponent -import net.prismclient.aether.ui.component.type.layout.styles.UIFrameSheet +import net.prismclient.aether.ui.component.type.layout.UIFrameSheet import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.component.util.enums.UIAlignment.* import net.prismclient.aether.ui.renderer.impl.background.UIBackground @@ -19,6 +19,7 @@ import net.prismclient.aether.ui.util.extensions.px import net.prismclient.aether.ui.util.extensions.rel import net.prismclient.aether.ui.util.interfaces.UIAnimatable import net.prismclient.aether.ui.util.interfaces.UICopy +import net.prismclient.aether.ui.util.name /** * [UIStyleSheet] is the superclass of all styles. It holds the general @@ -39,11 +40,13 @@ import net.prismclient.aether.ui.util.interfaces.UICopy * are thrown when used. If the style sheet is intended on being inheritable, the apply method * should also be overridden. See [UIFrameSheet] for an example. * + * @since 1.0 * @see Styles * @see How to create styles */ -open class UIStyleSheet @JvmOverloads constructor(var name: String = "") : UICopy, - UIAnimatable { +open class UIStyleSheet() : UICopy, UIAnimatable { + var name: String = "" + /** * When true, the property will not be cleared when Aether cleans up styles. */ @@ -344,7 +347,7 @@ open class UIStyleSheet @JvmOverloads constructor(var name: String = "") : UICop this.marginLeft = marginLeft } - override fun copy(): UIStyleSheet = UIStyleSheet(name).apply(this) + override fun copy(): UIStyleSheet = UIStyleSheet().name(name).apply(this) /** * Applies the properties of an existing sheet to this diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt index 9066422..4a75d80 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt @@ -35,47 +35,49 @@ const val NEAREST = 32 /** * Creates a DSL block from the given [obj] of type [T]. */ -inline fun blockFrom(obj: T, block: T.() -> Unit) = obj.block() +inline fun blockFrom(obj: T, block: Block) = obj.block() /** - * Registers a style sheet for the given style, [S]. + * Creates a style sheet [block] from the given style sheet [S], sets the [name] and registers the style. */ -inline fun style(style: S, block: S.() -> Unit) { - style.block() - UIProvider.registerStyle(style) +@JvmName("styleExtension") +inline fun S.style(name: String, block: Block): S = apply { + this.name = name + this.block() + UIProvider.registerStyle(this) } /** - * Registers a style sheet for the given style, [S]. Alternative to [style]. - * - * @see style + * Creates a style sheet [block] from the given style sheet [S], sets the [name] and registers the style. */ -inline fun styleOf(style: S, block: S.() -> Unit) = style(style, block) - -@JvmName("styleExtension") -inline fun S.style(block: S.() -> Unit) = style(this, block) +inline fun style(sheet: S, name: String, block: Block): S { + sheet.name = name + sheet.block() + UIProvider.registerStyle(sheet) + return sheet +} /** - * Registers a style sheet in the component scope. This is used when no style is provided - * to the component, and instead, the style is provided at the component level. + * Creates a style [block] from the given component [C]. If the style has not been created + * one is automatically allocated with the name of * - * If the style's name is blank, it will be formatted as "Gen-${component.toString()}". - */ -inline fun , S : UIStyleSheet> C.style(style: S, block: S.() -> Unit): C = apply { - UIComponentDSL.updateState(this) - if (style.name.isEmpty()) style.name = "Gen-$this" - styleOf(style, block) - this.applyStyle(style.name) - UIComponentDSL.restoreState(this) -} + * "$C.toString()-sheet" + */ +inline fun , S : UIStyleSheet> C.style(block: Block): C = this.style("$this-sheet", block) /** - * Creates a style [block] on a [UIComponent] of the style sheet [S]. + * Creates a style [block] from the given component [C]. If the style has not been + * created, one is automatically allocated with the name of [name]. */ -inline fun , S : UIStyleSheet> C.style(block: S.() -> Unit): C = apply { - UIComponentDSL.updateState(this) +inline fun , S : UIStyleSheet> C.style(name: String, block: Block): C = also { + if (!this.hasStyle()) { + this.createsStyle().run { + this.name = name + } + this.applyStyle("Gen-$this") + } + this.style.block() - UIComponentDSL.restoreState(this) } /** @@ -136,7 +138,7 @@ fun marginOf(top: UIUnit, right: UIUnit, bottom: UIUnit, left: UIUnit): UIMargin * * @see ucreate */ -inline fun create(block: UIComponentDSL.() -> Unit) { +inline fun create(block: Block) { UIComponentDSL.begin() UIComponentDSL.block() UIComponentDSL.complete() @@ -145,7 +147,7 @@ inline fun create(block: UIComponentDSL.() -> Unit) { /** * Unsafe version of [build]. Does not allocate/deallocate the stacks, thus nothing will be reset */ -inline fun ucreate(block: UIComponentDSL.() -> Unit) = UIComponentDSL.block() +inline fun ucreate(block: Block) = UIComponentDSL.block() /** * Loads the [dependable]. @@ -165,10 +167,7 @@ inline fun animationOf(animationName: String, style: S, block return animation } -inline fun gradient(block: Block): UIGradientBackground = UIGradientBackground().also(block) - /** - * Type alias for a function which has a receiver of [T] and accepts, and returns - * nothing. The block is intended to apply properties to the receiver [T]. + * Creates a [UIGradientBackground], applies the given block to it, and returns it. */ -typealias Block = T.() -> Unit \ No newline at end of file +inline fun gradient(block: Block): UIGradientBackground = UIGradientBackground().also(block) \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt b/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt new file mode 100644 index 0000000..80cd7e1 --- /dev/null +++ b/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt @@ -0,0 +1,14 @@ +package net.prismclient.aether.ui.util + +import net.prismclient.aether.ui.style.UIStyleSheet + +/** + * Type alias for a function which has a receiver of [T] and accepts, and returns + * nothing. The block is intended to apply properties to the receiver [T]. + */ +typealias Block = T.() -> Unit + +fun T.name(styleName: String): T { + this.name = styleName + return this +} \ No newline at end of file diff --git a/src/test/kotlin/examples/Animations.kt b/src/test/kotlin/examples/Animations.kt index 1eb9896..4262ebd 100644 --- a/src/test/kotlin/examples/Animations.kt +++ b/src/test/kotlin/examples/Animations.kt @@ -22,68 +22,15 @@ import net.prismclient.aether.ui.util.extensions.px class Animations : UIScreen { override fun build() { create { - include(Generic()) + button("Hello").style { - UIStyleSheet("button").style { - align(UIAlignment.CENTER) - size(400, 40) - background(colorOf(asRGBA(0f, 0f, 0f, 0.3f)), radiusOf(0f)) { - border { - borderWidth = px(10) - borderColor = colorOf(asRGBA(255, 0, 0)) - } - } - font("Montserrat", px(16f), colorOf(-1), left or top) } - animationOf("someAnimation", UIStyleSheet()) { - UIQuart(1000L) repeat { - kf {} - kf { - position(50, 0) - } - kf { - position(0, 0) - } - kf { - position(0, 50) - } - kf { - position(50, 50) - } - } - onCompletion { - UIProvider.dispatchAnimation("someAnimation", it.component) - } - } - - animationOf("test", UIStyleSheet()) { - kf {} - UILinear(1000L) to { - background { -// backgroundColor = colorOf(asRGBA(0, 0, 255)) - border { - borderColor = colorOf(asRGBA(0f, 1f, 0f)) - borderWidth = px(1) - } - } - } - } + verticalList { - animationOf("move", UIStyleSheet()) { - kf {} - kf { - x = px(50) - } - } - - val l = label("Some text", "button").onMousePressed { - UIProvider.dispatchAnimation("test", it) - println("pressed") }.style { - x = px(-50) + } - UIProvider.dispatchAnimation("move", l) } } } \ No newline at end of file diff --git a/src/test/kotlin/examples/AutoLayouts.kt b/src/test/kotlin/examples/AutoLayouts.kt index 46584d1..323fbb8 100644 --- a/src/test/kotlin/examples/AutoLayouts.kt +++ b/src/test/kotlin/examples/AutoLayouts.kt @@ -2,8 +2,8 @@ package examples import examples.deps.Generic import net.prismclient.aether.ui.component.type.layout.UIFrame -import net.prismclient.aether.ui.component.type.layout.auto.UIAutoLayout -import net.prismclient.aether.ui.component.type.layout.list.UIListLayout +import net.prismclient.aether.ui.component.type.layout.UIAutoLayout +import net.prismclient.aether.ui.component.type.layout.UIListLayout import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.screen.UIScreen diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index 5920318..f8a7e6c 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -1,18 +1,8 @@ package examples -import net.prismclient.aether.ui.animation.ease.impl.UIQuart -import net.prismclient.aether.ui.component.type.image.UIImageSheet -import net.prismclient.aether.ui.component.type.layout.auto.UIAutoLayout -import net.prismclient.aether.ui.component.type.layout.list.UIListLayout -import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet -import net.prismclient.aether.ui.component.util.enums.UIAlignment -import net.prismclient.aether.ui.dsl.UIAssetDSL -import net.prismclient.aether.ui.renderer.UIProvider -import net.prismclient.aether.ui.renderer.impl.font.UIFont +import net.prismclient.aether.ui.component.type.layout.UIListLayout import net.prismclient.aether.ui.screen.UIScreen -import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.util.* -import net.prismclient.aether.ui.util.extensions.* class Default : UIScreen { override fun build() { diff --git a/src/test/kotlin/examples/deps/Generic.kt b/src/test/kotlin/examples/deps/Generic.kt index eb8fc8c..21db87f 100644 --- a/src/test/kotlin/examples/deps/Generic.kt +++ b/src/test/kotlin/examples/deps/Generic.kt @@ -9,7 +9,7 @@ import net.prismclient.aether.ui.util.extensions.colorOf import net.prismclient.aether.ui.util.extensions.px import net.prismclient.aether.ui.util.interfaces.UIDependable import net.prismclient.aether.ui.util.left -import net.prismclient.aether.ui.util.styleOf +import net.prismclient.aether.ui.util.style import net.prismclient.aether.ui.util.top /** @@ -47,12 +47,12 @@ class Generic : UIDependable { // assumeLoadImage() // A 24x24 icon - styleOf(UIImageSheet("icon24x")) { + style(UIImageSheet(), "icon24x") { size(24, 24) } // A example font - styleOf(UIStyleSheet("generic-font")) { + style(UIStyleSheet("generic-font")) { // FontFamily to Montserrat // FontSize -> 16f // FontColor -> -1 = asRGBA(255, 255, 255) (aka white) From 797029cd9006286d6008dbbc9ac0a20cadbb6d43 Mon Sep 17 00:00:00 2001 From: sen Date: Thu, 14 Jul 2022 18:21:38 -0700 Subject: [PATCH 15/37] Update Shorthands.kt --- src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt index 4a75d80..08be80e 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt @@ -73,6 +73,7 @@ inline fun , S : UIStyleSheet> C.style(name: String, block: B if (!this.hasStyle()) { this.createsStyle().run { this.name = name + UIProvider.registerStyle(this) } this.applyStyle("Gen-$this") } From 122e8fc265c5e58fbe8d62c25c9b394dda842443 Mon Sep 17 00:00:00 2001 From: sen Date: Thu, 14 Jul 2022 19:50:11 -0700 Subject: [PATCH 16/37] Update Shorthands.kt --- src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt index 08be80e..1408044 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt @@ -75,7 +75,7 @@ inline fun , S : UIStyleSheet> C.style(name: String, block: B this.name = name UIProvider.registerStyle(this) } - this.applyStyle("Gen-$this") + this.applyStyle(name) } this.style.block() From b87647134bf94dcdef44e49419ee2ac1fddfc8d2 Mon Sep 17 00:00:00 2001 From: Decencies <66835910+Decencies@users.noreply.github.com> Date: Fri, 15 Jul 2022 17:22:15 +0100 Subject: [PATCH 17/37] Feat: timings --- .../net/prismclient/aether/ui/Aether.kt | 5 + .../net/prismclient/aether/ui/Timings.kt | 83 +++++++++++++ .../aether/ui/callback/UICoreCallback.kt | 17 --- .../impl/selection/UISelectableController.kt | 13 +++ .../type/input/textfield/UITextField.kt | 6 +- .../ui/component/type/layout/UIContainer.kt | 18 ++- .../ui/component/type/layout/UIFrame.kt | 6 +- .../ui/component/util/enums/UIAlignment.kt | 2 +- .../ui/component/util/enums/UIOverflow.kt | 8 -- .../prismclient/aether/ui/dsl/UIAssetDSL.kt | 4 +- .../aether/ui/dsl/UIRendererDSL.kt | 7 +- .../aether/ui/renderer/UIProvider.kt | 7 +- .../aether/ui/style/util/UIAnchorPoint.kt | 3 +- .../prismclient/aether/ui/util/Shorthands.kt | 7 +- .../aether/ui/util/UIAetherLogger.kt | 29 ++++- .../net/prismclient/aether/ui/util/Util.kt | 19 ++- .../ui/util/interfaces/UITriConsumer.kt | 23 ---- src/test/kotlin/Runner.kt | 11 +- src/test/kotlin/examples/AutoLayouts.kt | 110 ------------------ 19 files changed, 186 insertions(+), 192 deletions(-) create mode 100644 src/main/kotlin/net/prismclient/aether/ui/Timings.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/callback/UICoreCallback.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/util/enums/UIOverflow.kt delete mode 100644 src/main/kotlin/net/prismclient/aether/ui/util/interfaces/UITriConsumer.kt delete mode 100644 src/test/kotlin/examples/AutoLayouts.kt diff --git a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt index 185deb7..e5db537 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt @@ -109,12 +109,14 @@ open class Aether(renderer: UIRenderer) { open fun render() { renderer { if (activeScreen != null) { + timings.onFrameRenderStart() beginFrame(width, height, devicePxRatio) for (i in 0 until components!!.size) { val component = components!![i] if (component.visible) component.render() } endFrame() + timings.onFrameRenderEnd() } } } @@ -243,6 +245,9 @@ open class Aether(renderer: UIRenderer) { * as de-focusing the focused component and adding listeners to input. */ companion object Properties { + + val timings: Timings = Timings() + @JvmStatic var debug: Boolean = true diff --git a/src/main/kotlin/net/prismclient/aether/ui/Timings.kt b/src/main/kotlin/net/prismclient/aether/ui/Timings.kt new file mode 100644 index 0000000..9218a08 --- /dev/null +++ b/src/main/kotlin/net/prismclient/aether/ui/Timings.kt @@ -0,0 +1,83 @@ +package net.prismclient.aether.ui + +class Timings { + + /** + * Time when the current frame render started in milliseconds + */ + var frameRenderStartTime = 0L + private set + + /** + * Time when the last frame render started in milliseconds + */ + var lastFrameRenderStartTime = 0L + private set + + /** + * Time when the current frame render ended in milliseconds + */ + var frameRenderEndTime = 0L + private set + + /** + * Time when the last frame render ended in milliseconds + */ + var lastFrameRenderEndTime = 0L + private set + + /** + * The delta time (frame render start - frame render end) of the current frame in milliseconds + */ + val frameRenderDeltaTime + get() = frameRenderEndTime - frameRenderStartTime + + /** + * The delta time (frame render start - frame render end) of the current frame in seconds + */ + val deltaFrameRenderTimeSeconds + get() = frameRenderDeltaTime / 1000.0 + + /** + * The delta time (frame render start - frame render end) of the last frame in milliseconds + */ + val lastFrameRenderDeltaTime + get() = lastFrameRenderEndTime - lastFrameRenderStartTime + + /** + * The delta time (frame render start - frame render end) of the last frame in seconds + */ + val lastFrameRenderDeltaTimeSeconds + get() = lastFrameRenderDeltaTime / 1000.0 + + /** + * The approximate amount of renders the last frame would have made in a second + */ + val lastFrameRate + get() = 1000.0 / lastFrameRenderDeltaTime + + /** + * The approximate amount of renders the current frame would have made in a second + */ + val frameRate + get() = (1000 / (frameRenderDeltaTime + 1)).toInt() + + /** + * Set current & last render start times + */ + fun onFrameRenderStart() { + lastFrameRenderStartTime = frameRenderStartTime + + frameRenderStartTime = System.currentTimeMillis() + } + + /** + * Set current & last render end times + */ + fun onFrameRenderEnd() { + lastFrameRenderEndTime = frameRenderEndTime + + frameRenderEndTime = System.currentTimeMillis() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/callback/UICoreCallback.kt b/src/main/kotlin/net/prismclient/aether/ui/callback/UICoreCallback.kt deleted file mode 100644 index 0751f94..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/callback/UICoreCallback.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.prismclient.aether.ui.callback - -/** - * [UICoreCallback] is an interface for getting data that - * is not available to aether by default. - * - * @author sen - * @since 5/13/2022 - */ -interface UICoreCallback { - /** - * Returns the color of the pixel at the given position - * - * @return RGB(A) formatted int - */ - fun getPixelColor(x: Float, y: Float): Int -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/controller/impl/selection/UISelectableController.kt b/src/main/kotlin/net/prismclient/aether/ui/component/controller/impl/selection/UISelectableController.kt index 0d224f0..b0dd7de 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/controller/impl/selection/UISelectableController.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/controller/impl/selection/UISelectableController.kt @@ -2,6 +2,7 @@ package net.prismclient.aether.ui.component.controller.impl.selection import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.component.controller.UIController +import net.prismclient.aether.ui.style.UIStyleSheet import java.util.function.Consumer import kotlin.reflect.KClass @@ -57,8 +58,20 @@ class UISelectableController>(filter: KClass) : UIControll } it.update() } + + selectedComponent = component } + /** + * Returns true if the selected component is at index [index] + */ + fun isSelected(index: Int) = selectedComponent == components[index] + + /** + * Returns true if the selected component is [component] + */ + fun > isSelected(component: O) = selectedComponent == component + /** * The action that is preformed when a component is selected */ diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt index 4b007a1..020a740 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt @@ -28,7 +28,7 @@ import java.util.function.Consumer * * @author sen * @since 1.0 - * @see UITextField.filter Pre-made text filters. + * @see UITextField.Filters pre-made text filters. */ open class UITextField(text: String, var placeholder: String? = null, var filter: TextFilter) : UIComponent(), UIFocusable { var text: String = text @@ -52,7 +52,7 @@ open class UITextField(text: String, var placeholder: String? = null, var filter /** Blink **/ protected var timeSinceLastBlink: Long = 0L - protected var blink: Boolean = false + protected var blink: Boolean = true init { Aether.onModifierKeyChange(this.toString()) { key, value -> @@ -117,7 +117,7 @@ open class UITextField(text: String, var placeholder: String? = null, var filter //style.caret.offsetY = font.cachedY + boundsOf(style.font!!.cachedText)[1] - y if (blink && isFocused()) style.caret.render() - if (timeSinceLastBlink + style.blinkRate <= System.currentTimeMillis()) { + if (style.blinkRate > 0 && (timeSinceLastBlink + style.blinkRate <= System.currentTimeMillis())) { blink = !blink timeSinceLastBlink = System.currentTimeMillis() } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt index 8469342..cf68009 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt @@ -206,9 +206,6 @@ open class UIContainerSheet : UIFrameSheet() { * [Overflow] defines what the vertical, and horizontal scrollbars * are supposed to do when content leaves the screen. Check the enum * documentation for more information. - * - * @author sen - * @since 5/12/2022 */ enum class Overflow { /** @@ -227,6 +224,21 @@ open class UIContainerSheet : UIFrameSheet() { Auto } + /** + * [ScrollBehaviour] defines how the encompassing container behaves when the mouse is scrolled. + */ + enum class ScrollBehaviour { + /** + * Clamps the content + */ + Fixed, + + /** + * Introduces elasticity to the encompassing container. + */ + Elastic + } + override fun apply(sheet: UIStyleSheet): UIContainerSheet { // Override the default apply function because // this is an inheritable class. diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt index b97d2f1..6e30a6e 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt @@ -180,7 +180,11 @@ open class UIFrameSheet : UIStyleSheet() { * If true certain optimizations will be applied when * rendering. This only works with [useFBO] as true. */ - var optimizeRenderer: Boolean = true + var optimizeRenderer: Boolean = false + set(value) { + if (!useFBO && value) warn("attempted to set optimizeRenderer when useFBO is false. ($name)") + field = value + } override fun apply(sheet: UIStyleSheet): UIFrameSheet { // Override the default apply function because diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/util/enums/UIAlignment.kt b/src/main/kotlin/net/prismclient/aether/ui/component/util/enums/UIAlignment.kt index 62f9ea8..12aaeb6 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/util/enums/UIAlignment.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/util/enums/UIAlignment.kt @@ -7,7 +7,7 @@ package net.prismclient.aether.ui.component.util.enums * @author sen * @since 1.0 */ -@Suppress("unused", "SpellCheckingInspection") +@Suppress("Unused", "SpellCheckingInspection") enum class UIAlignment { CUSTOM, TOPLEFT, TOPCENTER, TOPRIGHT, diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/util/enums/UIOverflow.kt b/src/main/kotlin/net/prismclient/aether/ui/component/util/enums/UIOverflow.kt deleted file mode 100644 index d1037de..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/component/util/enums/UIOverflow.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.prismclient.aether.ui.component.util.enums - -enum class UIOverflow { - VISIBLE, - HIDDEN, - SCROLL, - AUTO -} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIAssetDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIAssetDSL.kt index 73a338b..83125fe 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIAssetDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIAssetDSL.kt @@ -108,12 +108,12 @@ object UIAssetDSL { svgScale: Float = Aether.devicePxRatio ): Int { val file = Aether.javaClass.getResource(folderLocation) ?: run { - warn("Failed to bulk load [$folderLocation] as the file was null.") + error("Failed to bulk load [$folderLocation] as the file was null.") return 0 } return internalBulkLoad(File(file.toURI()), deep, appendedString, imageFlags, svgScale).also { - warn("Bulk loaded $it files.") + inform("Bulk loaded $it files.") } } diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt index d52edf2..76eda71 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt @@ -8,8 +8,7 @@ import net.prismclient.aether.ui.renderer.impl.border.UIStrokeDirection import net.prismclient.aether.ui.renderer.impl.font.UIFont import net.prismclient.aether.ui.renderer.impl.property.UIRadius import net.prismclient.aether.ui.renderer.other.UIContentFBO -import net.prismclient.aether.ui.util.Block -import net.prismclient.aether.ui.util.UIColor +import net.prismclient.aether.ui.util.* /** * [UIRendererDSL] wraps the [UIRenderer] class to minimize the amount of calls @@ -154,13 +153,13 @@ object UIRendererDSL { * @see fontBounds */ @JvmStatic - fun fontWidth(): Float = fontBounds()[2] - fontBounds()[0] + fun fontWidth(): Float = fontBounds().maxX() - fontBounds().minX() /** * Returns the height of the most recent text render call. */ @JvmStatic - fun fontHeight(): Float = fontBounds()[3] - fontBounds()[1] + fun fontHeight(): Float = fontBounds().maxY() - fontBounds().minY() /** * Returns the ascender of the most recent text render call. diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/UIProvider.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/UIProvider.kt index 1b94e72..5727146 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/UIProvider.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/UIProvider.kt @@ -48,10 +48,9 @@ object UIProvider { * Returns the name of the images given the [UIImageData] */ fun getImageName(imageData: UIImageData): String? { - for (image in images) { - if (image.value == imageData) { - return image.key - } + for ((name, image) in images) { + if (image == imageData) + return name } return null } diff --git a/src/main/kotlin/net/prismclient/aether/ui/style/util/UIAnchorPoint.kt b/src/main/kotlin/net/prismclient/aether/ui/style/util/UIAnchorPoint.kt index db2bfe2..1aa6ca5 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/style/util/UIAnchorPoint.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/style/util/UIAnchorPoint.kt @@ -3,6 +3,7 @@ package net.prismclient.aether.ui.style.util import net.prismclient.aether.ui.animation.UIAnimation import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.unit.UIUnit +import net.prismclient.aether.ui.util.extensions.align import net.prismclient.aether.ui.util.extensions.lerp import net.prismclient.aether.ui.util.extensions.px import net.prismclient.aether.ui.util.interfaces.UIAnimatable @@ -22,7 +23,7 @@ class UIAnchorPoint : UIAnimatable { infix fun align(alignment: UIAlignment) { x ?: run { x = px(0) } y ?: run { y = px(0) } - net.prismclient.aether.ui.util.extensions.align(alignment, x!!, y!!) + align(alignment, x!!, y!!) } override fun animate( diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt index 1408044..a58938d 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt @@ -156,10 +156,9 @@ inline fun ucreate(block: Block) = UIComponentDSL.block() fun include(dependable: UIDependable) = dependable.load() /** - * Creates an animation where the component is [C], the style of that component is [S], the animation - * name is [animationName], and an instance of the component's style is passed as [style]. A block is - * given to add the keyframes and modify other properties of the [UIAnimation]. The animation is - * automatically registered under the name given. + * Creates an animation for the stylesheet [S], with the name [animationName]. + * A block is given to add the keyframes and modify other properties of the [UIAnimation]. + * The animation is automatically registered under the name given. */ inline fun animationOf(animationName: String, style: S, block: UIAnimation.() -> Unit): UIAnimation { val animation = UIAnimation(animationName, style) diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/UIAetherLogger.kt b/src/main/kotlin/net/prismclient/aether/ui/util/UIAetherLogger.kt index 4509113..46696b9 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/UIAetherLogger.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/UIAetherLogger.kt @@ -2,14 +2,33 @@ package net.prismclient.aether.ui.util import net.prismclient.aether.ui.Aether -internal fun inform(message: String) { - if (Aether.debug) - println("[Aether]: $message") +enum class LogLevel { + DEBUG, + GLOBAL +} + +internal inline fun timed(message: String, level: LogLevel = LogLevel.DEBUG, block: () -> Unit) { + if (level != LogLevel.DEBUG || Aether.debug) { + println("[Aether]: TIMED -> $message") + val start = System.currentTimeMillis() + block() + println("[Aether]: TIMED -> took ${System.currentTimeMillis() - start}ms") + } } -internal fun warn(message: String) { +internal fun inform(message: String, level: LogLevel = LogLevel.DEBUG) { + if (level != LogLevel.DEBUG || Aether.debug) + println("[Aether]: INFO -> $message") +} + +internal fun debug(message: String) { if (Aether.debug) - println("[Aether]: $message") + println("[Aether]: DEBUG -> $message") +} + +internal fun warn(message: String, level: LogLevel = LogLevel.DEBUG) { + if (level != LogLevel.DEBUG || Aether.debug) + println("[Aether]: WARNING -> $message") } internal fun error(message: String) = println("[Aether]: ERR -> $message") \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt b/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt index 80cd7e1..33477f8 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt @@ -11,4 +11,21 @@ typealias Block = T.() -> Unit fun T.name(styleName: String): T { this.name = styleName return this -} \ No newline at end of file +} + +fun FloatArray.minX(): Float = this[0] +fun FloatArray.minY(): Float = this[1] +fun FloatArray.maxX(): Float = this[2] +fun FloatArray.maxY(): Float = this[3] + +fun FloatArray.x(): Float = this[0] +fun FloatArray.y(): Float = this[1] +fun FloatArray.width(): Float = this[2] +fun FloatArray.height(): Float = this[3] + +fun FloatArray.red(): Float = this[0] +fun FloatArray.green(): Float = this[1] +fun FloatArray.blue(): Float = this[2] +fun FloatArray.alpha(): Float = this[3] + +fun FloatArray.advance(): Float = this[4] \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/interfaces/UITriConsumer.kt b/src/main/kotlin/net/prismclient/aether/ui/util/interfaces/UITriConsumer.kt deleted file mode 100644 index 2365d7d..0000000 --- a/src/main/kotlin/net/prismclient/aether/ui/util/interfaces/UITriConsumer.kt +++ /dev/null @@ -1,23 +0,0 @@ -package net.prismclient.aether.ui.util.interfaces - -import java.util.function.BiConsumer -import java.util.function.Consumer - -/** - * [UITriConsumer] is like a normal [Consumer] or [BiConsumer] except it accepts three - * arguments instead of one or two. - * - * @author sen - * @since 6/5/2022 - */ -fun interface UITriConsumer { - fun accept(a: A, b: B, c: C) - - @JvmDefault - fun andThen(after: UITriConsumer): UITriConsumer? { - return UITriConsumer { a: A, b: B, c: C -> - accept(a, b, c) - after.accept(a, b, c) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/Runner.kt b/src/test/kotlin/Runner.kt index 548805a..a98e960 100644 --- a/src/test/kotlin/Runner.kt +++ b/src/test/kotlin/Runner.kt @@ -1,7 +1,7 @@ import examples.Animations -import examples.AutoLayouts import examples.Default import examples.PathRendering + import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.Aether.Properties.updateMouse import net.prismclient.aether.ui.util.input.UIKey @@ -84,9 +84,11 @@ object Runner { } glfwSetFramebufferSizeCallback(window) { _: Long, width: Int, height: Int -> - framebufferWidth = width - framebufferHeight = height - core!!.update(width / contentScaleX, height / contentScaleY, max(contentScaleX, contentScaleY)) + if (width > 0 && height > 0) { + framebufferWidth = width + framebufferHeight = height + core!!.update(width / contentScaleX, height / contentScaleY, max(contentScaleX, contentScaleY)) + } } glfwSetKeyCallback(window) { _: Long, keyCode: Int, scanCode: Int, action: Int, _: Int -> @@ -190,7 +192,6 @@ object Runner { Aether.displayScreen( when (args[0]) { "Animations" -> Animations() - "AutoLayouts" -> AutoLayouts() "PathRendering" -> PathRendering() else -> Default() } diff --git a/src/test/kotlin/examples/AutoLayouts.kt b/src/test/kotlin/examples/AutoLayouts.kt deleted file mode 100644 index 323fbb8..0000000 --- a/src/test/kotlin/examples/AutoLayouts.kt +++ /dev/null @@ -1,110 +0,0 @@ -package examples - -import examples.deps.Generic -import net.prismclient.aether.ui.component.type.layout.UIFrame -import net.prismclient.aether.ui.component.type.layout.UIAutoLayout -import net.prismclient.aether.ui.component.type.layout.UIListLayout -import net.prismclient.aether.ui.component.type.layout.styles.UIContainerSheet -import net.prismclient.aether.ui.component.util.enums.UIAlignment -import net.prismclient.aether.ui.screen.UIScreen -import net.prismclient.aether.ui.util.* -import net.prismclient.aether.ui.util.extensions.asRGBA -import net.prismclient.aether.ui.util.extensions.colorOf -import net.prismclient.aether.ui.util.extensions.px - -/** - * Auto Layouts are a neat feature which is designed to mimic the auto layout - * of Figma. Here is an example screen which utilizes these auto layouts to create - * a list of buttons. - * - * Auto Layouts, in layman terms, are a more advanced version of list layouts. It - * gives spacing and padding properties as well as better alignment support which - * list layouts and other layouts lack. It is one of the few [UIFrame] which are - * designed to be used in mass amounts. - * - * @author sen - * @since 7/3/2022 - * @see UIAutoLayout For more information about the specifics. - */ -class AutoLayouts : UIScreen { - override fun build() { - // Start a `create` block as usual to get a UIComponentDSL block - create { - // Include the UIDependable, so we can use the styles from it - include(Generic()) - - // There are two (suggested) ways to declare Auto Layouts. If intend - // to use auto layout, once (uncommon), you can simply declare the - // layout using `UIComponentDSL.component() - - /* - component(UIAutoLayout(UIListLayout.ListDirection.Horizontal, null)) { - Define properties of the layout, and components... - } - */ - - // Generally you will need to reuse the same layout so instead - // define the layout as a variable instead. - - // ListDirection -> Vertical or Horizontal - val layout = - UIAutoLayout(UIListLayout.ListDirection.Horizontal, null).style(UIContainerSheet(("someStyle"))) { - // Declare the style inline, as we only use it once - background(colorOf(asRGBA(59, 145, 255)), radiusOf(9f)) - } - - // Create a DSL block using this shorthand - blockFrom(layout) { - // Let's say we want an icon on the left, and text on the right - // which is all centered to the middle component. Oh, and let's - // give some padding to the edges, and let it automatically size itself - // (We declare the components below) - componentAlignment = UIAlignment.CENTER // Align the stuff to the center - - // Resize to the size of the layout. (Not) Fun Fact: Setting of the size - // of the component ensures that this it at least that value. - horizontalResizing = UIAutoLayout.ResizingMode.Hug - verticalResizing = UIAutoLayout.ResizingMode.Hug - - // Space the components by 10px... - componentSpacing = px(10) - - // Set the padding to 10 - // Alternatively paddingOf(Unit/Number, Unit/Number, Unit/Number, Unit/Number) - layoutPadding = paddingOf(10) - } - - // Let's make a vertical list, so we can better see what is happening! - // This simply stacks the components vertically. - list(UIListLayout.ListDirection.Vertical, UIListLayout.ListOrder.Forward) { - componentSpacing = px(10) // With a spacing of 10px - - autoLayout(layout) { - // Define your components here! - image("ui", "icon24x") - text("User Interface", "generic-font") - }.style { // Create a style block so we can add a 25px margin at the top - margin { - marginTop = px(25) - } - } - - // Add another - autoLayout(layout) { - image("ui", "icon24x") - text("Some more user interface!!!!", "generic-font") - } - - // Sure, why not another! - autoLayout(layout) { - text("Reversed icons???", "generic-font") - image("ui", "icon24x") - } - }.style(UIContainerSheet("autoLayoutList")) { - // Make the size of this vertical layout - size(400, 500) - // There is no background, as the background was not defined - } - } - } -} \ No newline at end of file From 6cca80425f8302f603f35f5a68be1be2910163f4 Mon Sep 17 00:00:00 2001 From: Decencies <66835910+Decencies@users.noreply.github.com> Date: Fri, 15 Jul 2022 17:43:31 +0100 Subject: [PATCH 18/37] Push common changes --- .gitignore | 4 ++++ .../net/prismclient/aether/ui/dsl/UIRendererDSL.kt | 2 +- .../ui/renderer/impl/scrollbar/UIScrollbar.kt | 13 ++++++++++--- src/test/kotlin/examples/deps/Generic.kt | 5 ++--- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 9368ba7..c28f76d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ gradle-app.setting .idea *.DS_STORE *.log + +src/test/resources/ + +src/test/koltin/examples/prism/ diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt index 76eda71..cf0f1c0 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt @@ -76,7 +76,7 @@ object UIRendererDSL { */ @JvmStatic fun color(color: Int) { - UIRendererDSL.activeColor = color + activeColor = color renderer.color(color) } diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt index cbb1f59..a30cf58 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt @@ -121,12 +121,19 @@ class UIScrollbar(val type: Scrollbar) : UIColoredShape() { val mouseX = component!!.getMouseX() val mouseY = component!!.getMouseY() - if (mouseX >= x && mouseX <= x + w && mouseY >= y && mouseY <= y + h) { + val isSliderSelected = mouseX >= x && mouseX <= x + w && mouseY >= y && mouseY <= y + h + val isScrollbarSelected = mouseX >= cachedX && mouseX <= cachedX + cachedWidth && mouseY >= cachedY && mouseY <= cachedY + cachedHeight + + if (isSliderSelected) { selected = true mouseOffset = if (type == Scrollbar.Vertical) mouseY - y else mouseX - x - return true + } else if (isScrollbarSelected) { + + } else { + return false } - return false + + return true } fun mouseMoved() { diff --git a/src/test/kotlin/examples/deps/Generic.kt b/src/test/kotlin/examples/deps/Generic.kt index 21db87f..5d990ec 100644 --- a/src/test/kotlin/examples/deps/Generic.kt +++ b/src/test/kotlin/examples/deps/Generic.kt @@ -47,12 +47,11 @@ class Generic : UIDependable { // assumeLoadImage() // A 24x24 icon - style(UIImageSheet(), "icon24x") { + UIImageSheet().style("icon24x") { size(24, 24) } - // A example font - style(UIStyleSheet("generic-font")) { + UIStyleSheet().style("generic-font") { // FontFamily to Montserrat // FontSize -> 16f // FontColor -> -1 = asRGBA(255, 255, 255) (aka white) From b2a061fa428b91eb806ef374216d79407906ed54 Mon Sep 17 00:00:00 2001 From: Decencies <66835910+Decencies@users.noreply.github.com> Date: Fri, 15 Jul 2022 21:44:50 +0100 Subject: [PATCH 19/37] fix bugz --- src/main/kotlin/net/prismclient/aether/ui/Aether.kt | 4 ++-- .../prismclient/aether/ui/component/UIComponent.kt | 5 +++-- .../aether/ui/component/type/layout/UIContainer.kt | 6 +++--- .../aether/ui/component/type/layout/UIListLayout.kt | 2 -- .../net/prismclient/aether/ui/dsl/UIComponentDSL.kt | 5 ++--- .../aether/ui/renderer/impl/scrollbar/UIScrollbar.kt | 12 +++++++++--- .../net/prismclient/aether/ui/style/UIStyleSheet.kt | 10 ++++++---- src/test/kotlin/Renderer.kt | 1 + src/test/kotlin/Runner.kt | 2 +- 9 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt index e5db537..f479ed0 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt @@ -4,8 +4,8 @@ import net.prismclient.aether.ui.Aether.Properties import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.component.controller.UIController import net.prismclient.aether.ui.component.type.layout.UIFrame -import net.prismclient.aether.ui.component.type.layout.container.UIContainer -import net.prismclient.aether.ui.component.type.layout.container.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.UIContainer +import net.prismclient.aether.ui.component.type.layout.UIContainerSheet import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.UIRenderer diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt index 5f82ee5..46385ab 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt @@ -4,8 +4,8 @@ import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.animation.UIAnimation import net.prismclient.aether.ui.component.type.layout.UIFrame import net.prismclient.aether.ui.component.type.layout.UIFrameSheet -import net.prismclient.aether.ui.component.type.layout.container.UIContainer -import net.prismclient.aether.ui.component.type.layout.container.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.UIContainer +import net.prismclient.aether.ui.component.type.layout.UIContainerSheet import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.impl.background.UIBackground @@ -243,6 +243,7 @@ abstract class UIComponent() { */ open fun update() { if (!this::style.isInitialized) { + println("creating style...") style = createsStyle() } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt index cf68009..5459dd0 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt @@ -1,8 +1,6 @@ -package net.prismclient.aether.ui.component.type.layout.container +package net.prismclient.aether.ui.component.type.layout import net.prismclient.aether.ui.component.UIComponent -import net.prismclient.aether.ui.component.type.layout.UIFrame -import net.prismclient.aether.ui.component.type.layout.UIFrameSheet import net.prismclient.aether.ui.component.util.interfaces.UILayout import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.impl.scrollbar.UIScrollbar @@ -189,6 +187,8 @@ open class UIContainerSheet : UIFrameSheet() { */ var overflowY: Overflow = Overflow.Auto + var scrollBehaviour: ScrollBehaviour = ScrollBehaviour.Fixed + var verticalScrollbar: UIScrollbar = UIScrollbar(UIScrollbar.Scrollbar.Vertical) var horizontalScrollbar: UIScrollbar = UIScrollbar(UIScrollbar.Scrollbar.Horizontal) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt index a1d3e82..1b6a63f 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt @@ -1,9 +1,7 @@ package net.prismclient.aether.ui.component.type.layout -import net.prismclient.aether.ui.component.type.layout.container.UIContainer import net.prismclient.aether.ui.component.type.layout.UIListLayout.ListOrder.Backwards import net.prismclient.aether.ui.component.type.layout.UIListLayout.ListOrder.Forward -import net.prismclient.aether.ui.component.type.layout.container.UIContainerSheet import net.prismclient.aether.ui.unit.UIUnit /** diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt index 093d8c8..ded9ef4 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt @@ -10,11 +10,10 @@ import net.prismclient.aether.ui.component.type.input.button.UIButton import net.prismclient.aether.ui.component.type.input.slider.UISlider import net.prismclient.aether.ui.component.type.layout.UIFrame import net.prismclient.aether.ui.component.type.layout.UIAutoLayout -import net.prismclient.aether.ui.component.type.layout.container.UIContainer +import net.prismclient.aether.ui.component.type.layout.UIContainer import net.prismclient.aether.ui.component.type.layout.UIListLayout -import net.prismclient.aether.ui.component.type.layout.container.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.UIContainerSheet import net.prismclient.aether.ui.dsl.UIComponentDSL.activeStyle -import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.util.Block import net.prismclient.aether.ui.util.interfaces.UIDependable import java.util.* diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt index a30cf58..ee79d69 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/scrollbar/UIScrollbar.kt @@ -1,8 +1,8 @@ package net.prismclient.aether.ui.renderer.impl.scrollbar import net.prismclient.aether.ui.component.UIComponent -import net.prismclient.aether.ui.component.type.layout.container.UIContainer -import net.prismclient.aether.ui.component.type.layout.container.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.UIContainer +import net.prismclient.aether.ui.component.type.layout.UIContainerSheet import net.prismclient.aether.ui.renderer.impl.background.UIBackground import net.prismclient.aether.ui.renderer.impl.border.UIBorder import net.prismclient.aether.ui.renderer.impl.property.UIRadius @@ -128,7 +128,13 @@ class UIScrollbar(val type: Scrollbar) : UIColoredShape() { selected = true mouseOffset = if (type == Scrollbar.Vertical) mouseY - y else mouseX - x } else if (isScrollbarSelected) { - + println("scrollbarSelected") + +// value = if (type == Scrollbar.Vertical) { +// (mouseY - cachedY - (mouseY - y)) / (cachedHeight - sliderSize).coerceAtLeast(Float.MIN_VALUE) +// } else { +// (mouseX - cachedX - (mouseX - x)) / (cachedWidth - sliderSize).coerceAtLeast(Float.MIN_VALUE) +// } } else { return false } diff --git a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt index beb56f9..353084a 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt @@ -85,10 +85,12 @@ open class UIStyleSheet() : UICopy, UIAnimatable { ) { val component = animation.component - component.x = previous?.x.lerp(current?.x, component, x, progress, false) - component.y = previous?.y.lerp(current?.y, component, y, progress, true) - component.width = previous?.width.lerp(current?.width, component, width, progress, false) - component.height = previous?.height.lerp(current?.height, component, height, progress, true) + if (!component.overridden) { + component.x = previous?.x.lerp(current?.x, component, x, progress, false) + component.y = previous?.y.lerp(current?.y, component, y, progress, true) + component.width = previous?.width.lerp(current?.width, component, width, progress, false) + component.height = previous?.height.lerp(current?.height, component, height, progress, true) + } if (previous?.background != null || current?.background != null) { background = background ?: UIBackground() diff --git a/src/test/kotlin/Renderer.kt b/src/test/kotlin/Renderer.kt index c1344e8..3bde412 100644 --- a/src/test/kotlin/Renderer.kt +++ b/src/test/kotlin/Renderer.kt @@ -110,6 +110,7 @@ object Renderer : UIRenderer { ctx, framebuffers[fbo] ?: throw NullPointerException("Unable to find the framebuffer $fbo.") ) GL11.glViewport(0, 0, fbo.scaledWidth.toInt(), fbo.scaledHeight.toInt()) + GL11.glClearColor(0f, 0f, 0f, 0f) GL11.glClear(GL11.GL_COLOR_BUFFER_BIT or GL11.GL_STENCIL_BUFFER_BIT) } diff --git a/src/test/kotlin/Runner.kt b/src/test/kotlin/Runner.kt index a98e960..0756640 100644 --- a/src/test/kotlin/Runner.kt +++ b/src/test/kotlin/Runner.kt @@ -196,6 +196,6 @@ object Runner { else -> Default() } ) - } + } else Aether.displayScreen(Default()) } } \ No newline at end of file From c3eca82b51eb9d3cadd5b76d34c7eeb9db2f4d13 Mon Sep 17 00:00:00 2001 From: sen Date: Fri, 15 Jul 2022 15:04:57 -0700 Subject: [PATCH 20/37] Ok now fix --- .../net/prismclient/aether/ui/Aether.kt | 6 +-- .../aether/ui/component/UIComponent.kt | 52 ++++++++----------- src/test/kotlin/examples/Default.kt | 7 --- src/test/kotlin/examples/components/Chart.kt | 6 ++- src/test/kotlin/examples/deps/Generic.kt | 2 +- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt index f479ed0..a9a0fba 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt @@ -145,7 +145,7 @@ open class Aether(renderer: UIRenderer) { open fun mouseChanged(mouseButton: Int, isRelease: Boolean) { if (isRelease) { mouseReleasedListeners?.forEach { it.value.run() } - components?.forEach { it.mouseReleased(mouseX, mouseY) } + components?.forEach { it.mouseReleased(it.getMouseX(), it.getMouseY()) } return } @@ -185,7 +185,7 @@ open class Aether(renderer: UIRenderer) { return if (component != null) { component.focus() - component.mousePressed(UIMouseEvent(mouseX, mouseY, mouseButton, clickCount)) + component.mousePressed(UIMouseEvent(component.getMouseX(), component.getMouseY(), mouseButton, clickCount)) true } else false } @@ -214,7 +214,7 @@ open class Aether(renderer: UIRenderer) { i++ } c?.focus() - c?.mousePressed(UIMouseEvent(mouseX, mouseY, mouseButton, clickCount)) + c?.mousePressed(UIMouseEvent(c.getMouseX(), c.getMouseY(), mouseButton, clickCount)) } /** diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt index 46385ab..c6b0a79 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt @@ -2,10 +2,10 @@ package net.prismclient.aether.ui.component import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.animation.UIAnimation -import net.prismclient.aether.ui.component.type.layout.UIFrame -import net.prismclient.aether.ui.component.type.layout.UIFrameSheet import net.prismclient.aether.ui.component.type.layout.UIContainer import net.prismclient.aether.ui.component.type.layout.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.UIFrame +import net.prismclient.aether.ui.component.type.layout.UIFrameSheet import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.impl.background.UIBackground @@ -37,7 +37,7 @@ import java.util.function.Consumer * @since 1.0 */ @Suppress("UNCHECKED_CAST", "MemberVisibilityCanBePrivate", "LeakingThis") -abstract class UIComponent() { +abstract class UIComponent { /** * The style of the component. */ @@ -204,8 +204,7 @@ abstract class UIComponent() { * @throws InvalidStyleSheetException If the style is not a valid style sheet of the given component */ open fun applyStyle(style: String?) { - if (style.isNullOrEmpty()) - return + if (style.isNullOrEmpty()) return // Attempt to apply the style provided to the component. // Throw a InvalidStyleException if the style is not valid. @@ -334,8 +333,7 @@ abstract class UIComponent() { if (animations != null) { animations!!.forEach { it.value.update() } animations!!.entries.removeIf { it.value.isCompleted } - if (animations!!.isEmpty()) - animations = null + if (animations!!.isEmpty()) animations = null } } @@ -609,36 +607,28 @@ abstract class UIComponent() { open fun getMouseY(): Float = Aether.mouseY - getParentYOffset() /** - * Returns the offset of the parent on the x-axis. It incorporates for [UIFrame] and [UIContainer] scroll offsets. + * Returns the actual x position of this component rendered on screen. FBOs change the point of origin + * back to 0, so the values that the component has might not reflect it's actual position on screen. */ - open fun getParentXOffset(): Float { - if (parent == null) return 0f - - return if (parent is UIFrame) { - val clipContent = ((parent as UIFrame).style as UIFrameSheet).clipContent - return (if (clipContent) { - parent!!.relX - } else 0f) + parent!!.getParentXOffset() - if (parent is UIContainer) { - (parent!!.style as UIContainerSheet).horizontalScrollbar.value * (parent as UIContainer).expandedWidth - } else 0f + open fun getParentXOffset(): Float = if (parent is UIFrame) { + ((if ((parent!!.style as UIFrameSheet).useFBO) { + parent!!.relX + } else 0f) + parent!!.getParentXOffset()) - if (parent is UIContainer) { + (parent!!.style as UIContainerSheet).horizontalScrollbar.value * (parent as UIContainer).expandedWidth } else 0f - } + } else 0f /** - * Returns the offset of the parent on the y-axis. It incorporates for [UIFrame] and [UIContainer] scrolling offsets + * Returns the actual y position of this component rendered on screen. FBOs change the point of origin + * back to 0, so the values that the component has might not reflect it's actual position on screen. */ - open fun getParentYOffset(): Float { - if (parent == null) return 0f - - return if (parent is UIFrame) { - val clipContent = ((parent as UIFrame).style as UIFrameSheet).clipContent - return (if (clipContent) { - parent!!.relY - } else 0f) + parent!!.getParentYOffset() - if (parent is UIContainer) { - (parent!!.style as UIContainerSheet).verticalScrollbar.value * (parent as UIContainer).expandedHeight - } else 0f + open fun getParentYOffset(): Float = if (parent is UIFrame) { + ((if ((parent!!.style as UIFrameSheet).useFBO) { + parent!!.relY + } else 0f) + parent!!.getParentYOffset()) - if (parent is UIContainer) { + (parent!!.style as UIContainerSheet).verticalScrollbar.value * (parent as UIContainer).expandedHeight } else 0f - } + } else 0f /** * Shorthand for calculating the x or width of this component diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index f8a7e6c..b5b8f02 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -1,15 +1,8 @@ package examples -import net.prismclient.aether.ui.component.type.layout.UIListLayout import net.prismclient.aether.ui.screen.UIScreen -import net.prismclient.aether.ui.util.* class Default : UIScreen { override fun build() { - create { - autoLayout(UIListLayout.ListDirection.Vertical) { - - } - } } } \ No newline at end of file diff --git a/src/test/kotlin/examples/components/Chart.kt b/src/test/kotlin/examples/components/Chart.kt index e115d40..8e32fc3 100644 --- a/src/test/kotlin/examples/components/Chart.kt +++ b/src/test/kotlin/examples/components/Chart.kt @@ -6,8 +6,12 @@ import net.prismclient.aether.ui.style.UIStyleSheet /** * An example component which draws a chart. */ -class Chart(style: String?) : UIComponent(style) { +class Chart() : UIComponent() { override fun renderComponent() { } + + override fun createsStyle(): UIStyleSheet { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/src/test/kotlin/examples/deps/Generic.kt b/src/test/kotlin/examples/deps/Generic.kt index 5d990ec..831b3ed 100644 --- a/src/test/kotlin/examples/deps/Generic.kt +++ b/src/test/kotlin/examples/deps/Generic.kt @@ -41,7 +41,7 @@ class Generic : UIDependable { // Load some assets into memory. Aether is intended to support mainly JPEG, PNG, and SVG. // Either explicitly state the type // Reference the image with the name "ui" - UIAssetDSL.svg("ui", "/prism/icons/navbar/ui.svg") + //UIAssetDSL.svg("ui", "/prism/icons/navbar/ui.svg") // loadImage() // or let Aether figure it out // assumeLoadImage() From 0351d624fdc08d1cb3ff4a7aa8e7978c12ed2036 Mon Sep 17 00:00:00 2001 From: sen Date: Fri, 15 Jul 2022 15:53:10 -0700 Subject: [PATCH 21/37] Only allocate if the style is not found --- .../kotlin/net/prismclient/aether/ui/util/Shorthands.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt index a58938d..9c91118 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt @@ -71,9 +71,11 @@ inline fun , S : UIStyleSheet> C.style(block: Block): C = */ inline fun , S : UIStyleSheet> C.style(name: String, block: Block): C = also { if (!this.hasStyle()) { - this.createsStyle().run { - this.name = name - UIProvider.registerStyle(this) + if (UIProvider.styles[name] == null) { + this.createsStyle().run { + this.name = name + UIProvider.registerStyle(this) + } } this.applyStyle(name) } From ad771fbc94fe237517fa1649d177bd84d5573f19 Mon Sep 17 00:00:00 2001 From: sen Date: Fri, 15 Jul 2022 21:14:24 -0700 Subject: [PATCH 22/37] Kind of fixed animations --- .../aether/ui/animation/UIAnimation.kt | 1 + .../aether/ui/component/UIComponent.kt | 5 +- .../aether/ui/renderer/UIProvider.kt | 7 +- .../aether/ui/style/UIStyleSheet.kt | 18 +- .../aether/ui/util/extensions/Color.kt | 1 - src/test/kotlin/examples/Default.kt | 266 ++++++++++++++++++ 6 files changed, 287 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt b/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt index 978c772..751ff89 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt @@ -67,6 +67,7 @@ class UIAnimation(val name: String, val style: S) : UICopy) { + println("Started animation") this.component = component component.animations = component.animations ?: hashMapOf() diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt index c6b0a79..5c658ca 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt @@ -333,7 +333,10 @@ abstract class UIComponent { if (animations != null) { animations!!.forEach { it.value.update() } animations!!.entries.removeIf { it.value.isCompleted } - if (animations!!.isEmpty()) animations = null + if (animations!!.isEmpty()) { + println("Completed animation") + animations = null + } } } diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/UIProvider.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/UIProvider.kt index 5727146..f467fc2 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/UIProvider.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/UIProvider.kt @@ -6,6 +6,7 @@ import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.renderer.image.UIImageData import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.style.util.UIFontFamily +import net.prismclient.aether.ui.util.warn /** * [UIProvider] handles style sheets, images, fonts, and animations, a.k.a. resources @@ -91,7 +92,11 @@ object UIProvider { } fun dispatchAnimation(animationName: String, component: UIComponent) { - val animation: UIAnimation = animations[animationName]?.copy() as UIAnimation + val animation: UIAnimation? = animations[animationName]?.copy() as? UIAnimation + if (animation == null) { + warn("Animation of name [$animationName] was not found") + return + } animation.start(component) } diff --git a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt index 353084a..b8b55c1 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt @@ -86,8 +86,8 @@ open class UIStyleSheet() : UICopy, UIAnimatable { val component = animation.component if (!component.overridden) { - component.x = previous?.x.lerp(current?.x, component, x, progress, false) - component.y = previous?.y.lerp(current?.y, component, y, progress, true) + component.x = previous?.x.lerp(current?.x, component, x, progress, false) + component.getParentX() + component.y = previous?.y.lerp(current?.y, component, y, progress, true) + component.getParentY() component.width = previous?.width.lerp(current?.width, component, width, progress, false) component.height = previous?.height.lerp(current?.height, component, height, progress, true) } @@ -112,17 +112,19 @@ open class UIStyleSheet() : UICopy, UIAnimatable { anchor = anchor ?: UIAnchorPoint() anchor!!.animate(animation, previous?.anchor, current?.anchor, progress) } - component.x += component.getParentX() + component.marginLeft - component.anchorX - component.y += component.getParentY() + component.marginTop - component.anchorY + if (!component.overridden) { + component.x += component.getParentX() + component.marginLeft - component.anchorX + component.y += component.getParentY() + component.marginTop - component.anchorY + } component.updateBounds() component.updateStyle() } override fun save(animation: UIAnimation<*>, keyframe: UIStyleSheet?) { - x = keyframe?.x ?: x - y = keyframe?.y ?: y - width = keyframe?.width ?: width - height = keyframe?.height ?: height + x = keyframe?.x ?: x?.copy() + y = keyframe?.y ?: y?.copy() + width = keyframe?.width ?: width?.copy() + height = keyframe?.height ?: height?.copy() background?.save(animation, keyframe?.background) font?.save(animation, keyframe?.font) diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Color.kt b/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Color.kt index f6d4755..fea03a5 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Color.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Color.kt @@ -69,7 +69,6 @@ fun Float.limit(): Float = this.coerceAtMost(1f).coerceAtLeast(0f) * Creates a new color from two provided values based on the progress between each value. */ fun transition(color1: Int, color2: Int, progress: Float): Int { - println(progress) val red1 = color1 shr 16 and 0xFF val green1 = color1 shr 8 and 0xFF val blue1 = color1 and 0xFF diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index b5b8f02..725b61f 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -1,8 +1,274 @@ package examples +import examples.deps.Generic +import net.prismclient.aether.ui.animation.ease.impl.UIQuart +import net.prismclient.aether.ui.component.type.UILabel +import net.prismclient.aether.ui.component.type.image.UIImage +import net.prismclient.aether.ui.component.type.image.UIImageSheet +import net.prismclient.aether.ui.component.type.input.button.UIButton +import net.prismclient.aether.ui.component.type.layout.UIAutoLayout +import net.prismclient.aether.ui.component.type.layout.UIListLayout +import net.prismclient.aether.ui.component.type.layout.UIContainerSheet +import net.prismclient.aether.ui.component.util.enums.UIAlignment +import net.prismclient.aether.ui.dsl.UIAssetDSL +import net.prismclient.aether.ui.renderer.UIProvider +import net.prismclient.aether.ui.renderer.impl.font.UIFont import net.prismclient.aether.ui.screen.UIScreen +import net.prismclient.aether.ui.style.UIStyleSheet +import net.prismclient.aether.ui.util.* +import net.prismclient.aether.ui.util.extensions.* class Default : UIScreen { + + val activeNavbarButtonColor = colorOf(87, 164, 255) + val hoveredNavbarButtonColor = colorOf(41, 41, 41) + override fun build() { + create { + include(Generic()) + UIAssetDSL.image("NavbarLogo", "/prism/logo/Logo.png", flags = REPEATX or REPEATY or GENERATE_MIPMAPS) + // TODO: test if lods stuff in root directory + UIAssetDSL.bulkLoad("/prism/icons", imageFlags = REPEATX or REPEATY or GENERATE_MIPMAPS) + createNavbar() + } + } + + private fun createNavbar() { + ucreate { + val navbar = container { + + UIImageSheet().style("navbar-logo") { + control(UIAlignment.CENTER) + size(56, 62) + } + + // Logo + container { + image("NavbarLogo", "navbar-logo") + }.style("logo") { + size(rel(1), px(118)) + } + + val navbarButtonStyle = UIContainerSheet().style("navbar-button") { + control(UIAlignment.CENTER) + size(206, 40) + margin { marginBottom = px(8) } + } + + val activeNavbarButtonStyle = UIContainerSheet().style("active-navbar-button") { + control(UIAlignment.CENTER) + size(206, 40) + background(activeNavbarButtonColor, radiusOf(9f)) + margin { marginBottom = px(8) } + } + + val hoveredNavbarButtonStyle = UIContainerSheet().style("hovered-navbar-button") { + background(colorOf(255, 0, 0), radiusOf(9f)) + } + + // Navbar list + val navList = list(UIListLayout.ListDirection.Vertical) { + val layout = UIAutoLayout(UIListLayout.ListDirection.Horizontal).apply { + componentAlignment = UIAlignment.MIDDLELEFT + componentSpacing = px(24) + layoutPadding = paddingOf(8, 9, 8, 9) + } + + // Navbar section style + UIStyleSheet().style("navbar-title") { + x = px(24) + font("Montserrat", px(11), colorOf(191, 189, 193), top or left) + margin { + marginTop = descender(1f) + marginBottom = px(8) + } + } + + animationOf("navbar-button", UIContainerSheet()) { +// keyframe(UIQuart(1000L), navbarButtonStyle) + kf {} + kf { + background { + background(colorOf(0)) + } + } + } + +// animationOf("active-navbar-button", UIContainerSheet()) { +// kf {} +// keyframe(UIQuart(1000L), activeNavbarButtonStyle.copy()) +// } + + animationOf("hovered-navbar-button", UIContainerSheet()) { + keyframe(UIQuart(1000L), hoveredNavbarButtonStyle.copy()) + } + + // Navbar button styles + UIImageSheet().style("navbar-icon") { + size(24, 24) + } + + UIStyleSheet().style("navbar-text") { + font("Montserrat", px(14), colorOf(-1), left or top) + } + + selectable { + label("MENU", "navbar-title") + navButton(layout, "Dashboard", "gradient/home") + navButton(layout, "Mods", "gradient/folder") + navButton(layout, "Settings", "gradient/setting") + navButton(layout, "Store", "gradient/bag") + navButton(layout, "Profiles", "gradient/profile") + + label("SOCIAL", "navbar-title") + navButton(layout, "Messages", "gradient/mail") + navButton(layout, "Friends", "gradient/people") + navButton(layout, "Achievements", "gradient/medal") + navButton(layout, "Recordings", "gradient/video") + + onSelection { + UIProvider.dispatchAnimation("active-navbar-button", it) + //it.applyStyle("active-navbar-button") + val image = (it.components[0] as UIImage) + image.image = "solid/" + image.image.substring(image.image.indexOf('/') + 1) + } + + onDeselection { + UIProvider.dispatchAnimation("navbar-button", it) + val image = (it.components[0] as UIImage) + image.image = "gradient/" + image.image.substring(image.image.indexOf('/') + 1) + it.applyStyle("navbar-button") + } + + this.components.forEach { component -> + component.onMousePressed { + println("This was selected: ${((it as UIAutoLayout).components[1] as UILabel).text}") + //selectComponent(component) + } + + component.onMouseEnter { + if (!isSelected(it)) { + println("enter") + UIProvider.dispatchAnimation("hovered-navbar-button", it) + //it.applyStyle("hovered-navbar-button") + } + } + + component.onMouseLeave { + if (!isSelected(it)) { + println("leave") + UIProvider.dispatchAnimation("navbar-button", it) + //it.applyStyle("navbar-button") + } + } + } + + // TODO: Style does not get initialized before this call. + //selectComponent(0) + } + + }.style("navbar-list") { + y = px(118) + size(rel(1), rel(1) - px(118 + 235)) // 118 = top area, 235 = bottom area + verticalScrollbar { + x = rel(1) - px(10) + y = rel(0.1) + width = px(5) + height = rel(0.8) + radius = radiusOf(2.5f) + color = colorOf(207, 207, 207) + background { + backgroundColor = colorOf(1f, 1f, 1f, 0.3f) + radius = radiusOf(2.5) + } + } + clipContent = true + } + + // Footer + val footer = autoLayout(UIListLayout.ListDirection.Vertical) { + verticalResizing = UIAutoLayout.ResizingMode.Hug + horizontalResizing = UIAutoLayout.ResizingMode.Hug + componentAlignment = UIAlignment.CENTER + componentSpacing = px(6) + + // Promotion + // TODO: Promotion component + + val editHud = autoLayout(UIListLayout.ListDirection.Horizontal) { + image("outline/ui", "navbar-icon").style { + imageColor = colorOf(87, 164, 255) + } + text("Edit HUD", "navbar-text").style { + font { + fontColor = colorOf(-1) //colorOf(87, 164, 255) + } + } + + componentAlignment = UIAlignment.CENTER + componentSpacing = px(20) + layoutPadding = paddingOf(15, 37, 15, 37) + + verticalResizing = UIAutoLayout.ResizingMode.Hug + horizontalResizing = UIAutoLayout.ResizingMode.Hug + }.style("edit-hud-layout") { + control(UIAlignment.BOTTOMCENTER) + background(colorOf(36, 37, 37), radiusOf(15)) + } + + }.style("navbar-footer-layout") { + control(UIAlignment.BOTTOMCENTER) + y -= px(16 + 8) + } + + label("Running Prism Client v1.0.0-Beta").style("nav-prism-version") { + font("Montserrat", px(8), colorOf(1f, 1f, 1f, 0.8f), left or top, UIFont.FontType.Light) + control(UIAlignment.BOTTOMCENTER) + y -= px(8) + } + }.style("navbar-container") { + size(px(236), rel(1)) + background(colorOf(32, 32, 32)) + useFBO = true + } + + // Navbar animation + animationOf("navbar-enter", UIContainerSheet()) { + kf { + x = px(-236) + } + UIQuart(1000L) to { + x = px(0) + } + } + + animationOf("navbar-exit", UIContainerSheet()) { + kf { + x = px(0) + } + UIQuart(500L) to { + x = px(-236) + } + } + + UIProvider.dispatchAnimation("navbar-enter", navbar) + } + } + + /** + * Creates a navbar button + */ + private fun navButton(layout: UIAutoLayout, buttonText: String, imageName: String): UIAutoLayout { + ucreate { + return autoLayout(layout) { + image(imageName, "navbar-icon") + text(buttonText, "navbar-text") + }.style { + control(UIAlignment.CENTER) + size(206, 40) + margin { marginBottom = px(8) } + } + } + return throw RuntimeException() } } \ No newline at end of file From d0a6faf40070512d482a1391762efa6cd3831ebd Mon Sep 17 00:00:00 2001 From: sen Date: Fri, 15 Jul 2022 21:22:18 -0700 Subject: [PATCH 23/37] Fixed --- .../aether/ui/animation/UIAnimation.kt | 10 ++++------ src/test/kotlin/examples/Default.kt | 15 ++++----------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt b/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt index 751ff89..5622ca5 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt @@ -17,9 +17,8 @@ import java.util.function.Consumer * keyframe is given, the animation will automatically allocate a default animation before * the keyframe. * - * When ran, a copy of this is assigned to the component. Everything except the actual keyframes - * are copied. If any styles are changed at that point, it will change the original animation keyframes. - * The same is applied to listeners. They are not copied. + * When ran, a copy of this is assigned to the component. If any styles are changed at that point, + * it will change the original animation keyframes. Everything but the listeners are copied. * * @author sen * @since 1.0 @@ -67,7 +66,6 @@ class UIAnimation(val name: String, val style: S) : UICopy) { - println("Started animation") this.component = component component.animations = component.animations ?: hashMapOf() @@ -76,7 +74,7 @@ class UIAnimation(val name: String, val style: S) : UICopy(val name: String, val style: S) : UICopy Date: Sun, 17 Jul 2022 12:48:33 -0700 Subject: [PATCH 24/37] lil Update to the readme --- README.md | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ace3cd5..bf71b3e 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,14 @@ -**Aether is a UI component library engine for Minecraft** and LWJGL projects (or anything really). You can create your -own renderer implementation or use the default implementation -with *[NanoVG](https://github.com/memononen/nanovg "An anti-aliased vector graphics library")*, which is an ** -Anti-Aliased vector graphics library**. **Please note the project is in the early stages of development.** Bugs may -arise, and there might be incomplete/missing features. The library is licensed under GPL-v2.0 license. +Aether is a UI component engine for Minecraft. It's designed with Kotlin, Figma, and Minecraft in mind. Aether is +designed to allow you to customize the renderer to your own, or the default one +with *[NanoVG, an anti-aliased 2D vector graphics library](https://github.com/memononen/nanovg "An anti-aliased vector graphics library")*. +As mentioned before, Aether is designed with Figma in mind. Features such as [Auto Layouts](https://help.figma.com/hc/en-us/articles/360040451373-Explore-auto-layout-properties) +and [Figma Font](https://help.figma.com/hc/en-us/articles/360039956434-Getting-started-with-text) are implemented to +streamline the process of creating UIs from Figma. The library is licensed under GPL-v2.0 license. -Ready to get started? [Check out the docs!](https://aether.prismclient.net/) +Convinced? [Check out the docs!](https://aether.prismclient.net/) # Including the project @@ -18,11 +19,11 @@ Ready to get started? [Check out the docs!](https://aether.prismclient.net/) ```groovy repositories { - maven { url "https://jitpack.io" } + maven { url "https://jitpack.io" } } dependencies { - implementation "com.github.Prism-Client:Aether-UI:Release" + implementation "com.github.Prism-Client:Aether-UI:Release" } ``` @@ -33,18 +34,19 @@ dependencies {

Maven ```xml + jitpack.io https://jitpack.io - + - - com.github.Prism-Client - Aether-UI - Release - + + com.github.Prism-Client + Aether-UI + Release + ``` @@ -55,16 +57,17 @@ dependencies { Personally, the greatest feat in making a Minecraft client is making appealing, flexible, and highly customizable UI. Very few are able to reach even 2/3 of my goals. I found that popular clients lacked quality (anti-aliasing) in their client, and in other cases, it simply wasn't that appealing (though that might be more related to the designer). Around -the time of the intial creation of this library, I picked up Kotlin. Kotlin introduces a ton of useful features -especially the DSL feature. It makes creating UIs just feel a whole lot easier. Because of it, I decided to design it +the time of the initial creation of this library, I picked up Kotlin. Kotlin introduces a ton of useful features +especially the DSL feature. It makes creating UIs just feel a lot easier. Because of it, I decided to design it with Kotlin as the main programming language. Java is still supported, however I highly suggest for you to use Kotlin. # Design -I initially created the engine to act a bit like HTML and CSS, where you would create "style sheets" and define them as -components. This design, dare I say pattern, is essentially is how it is today. However, that is about the only thing -related to web stuff. +I initially created the engine to act a bit like HTML and CSS, however, I decided to swerve from that. I kept the idea +of styles, however, a lot of things are different from web. After all, it was initially designed for newspapers! (Obviously, +it's changed a lot since then). Because I had Figma in mind, quite a few different features (such as Auto Layouts and Text) are +somewhat "loosely" based on Figma. # Development From 6a2764c59de4ea6703e8905b0ef3118dce9e5b52 Mon Sep 17 00:00:00 2001 From: sen Date: Sun, 17 Jul 2022 12:49:10 -0700 Subject: [PATCH 25/37] Removed unnecessary stuff from buildscript --- build.gradle | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/build.gradle b/build.gradle index 5b77d02..6209a69 100644 --- a/build.gradle +++ b/build.gradle @@ -47,18 +47,6 @@ dependencies { testRuntimeOnly "org.lwjgl:lwjgl-stb:$lwjglVersion:$lwjglNatives" } -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { - kotlinOptions { - languageVersion = "1.7" - freeCompilerArgs += [ - '-Xno-call-assertions', - '-Xno-receiver-assertions', - '-Xno-param-assertions', - '-Xjvm-default=enable' - ] - } -} - publishing { publications { maven(MavenPublication) { From b0a9988969d72103351bba4a59ee58b0c1f94efe Mon Sep 17 00:00:00 2001 From: sen Date: Sun, 17 Jul 2022 12:49:56 -0700 Subject: [PATCH 26/37] Removed some compiler warnings --- .../net/prismclient/aether/ui/Aether.kt | 1 + .../aether/ui/animation/UIAnimation.kt | 2 + .../type/input/textfield/UITextField.kt | 605 +++++++++--------- .../ui/component/type/layout/UIContainer.kt | 1 + .../ui/component/type/layout/UIFrame.kt | 1 + 5 files changed, 307 insertions(+), 303 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt index a9a0fba..28cb9e5 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt @@ -533,6 +533,7 @@ open class Aether(renderer: UIRenderer) { for (i in 0 until instance.frames!!.size) { // UIContainers are what control scrolling, so // if it is not an instance of it, skip and continue + @Suppress("UNCHECKED_CAST") val container = instance.frames!![i] as? UIContainer ?: continue if (container.isMouseInsideBounds() && container.expandedHeight > 0f && container.style.overflowY != UIContainerSheet.Overflow.None) { // Iterate through the frame to see if there are more diff --git a/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt b/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt index 5622ca5..7f1a2bf 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/animation/UIAnimation.kt @@ -25,6 +25,7 @@ import java.util.function.Consumer * * @param style The style sheet of the component. The style should be completely new, with no styles changed. */ +@Suppress("UNCHECKED_CAST") class UIAnimation(val name: String, val style: S) : UICopy> { /** * The component that this animation is attached to @@ -158,6 +159,7 @@ class UIAnimation(val name: String, val style: S) : UICopy Unit = {}) { + val style = this.style.copy() as S style.block() // Ease -> activeEase -> UILinear if null. diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt index 020a740..d22771b 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/textfield/UITextField.kt @@ -1,304 +1,303 @@ -package net.prismclient.aether.ui.component.type.input.textfield - -import net.prismclient.aether.ui.Aether -import net.prismclient.aether.ui.component.UIComponent -import net.prismclient.aether.ui.component.type.input.button.UIButton -import net.prismclient.aether.ui.component.type.input.textfield.caret.UICaret -import net.prismclient.aether.ui.event.input.UIMouseEvent -import net.prismclient.aether.ui.renderer.impl.font.UIFont -import net.prismclient.aether.ui.style.UIStyleSheet -import net.prismclient.aether.ui.util.UIColor -import net.prismclient.aether.ui.util.extensions.em -import net.prismclient.aether.ui.util.extensions.px -import net.prismclient.aether.ui.util.extensions.renderer -import net.prismclient.aether.ui.util.input.UIModifierKey -import net.prismclient.aether.ui.util.interfaces.UIFocusable -import net.prismclient.aether.ui.util.name -import net.prismclient.aether.ui.util.warn -import java.lang.Integer.max -import java.lang.Integer.min -import java.util.function.Consumer - -/** - * [UITextField] is a focusable component which accepts an input from - * the user via the keyboard. [placeholder] is the text which will be - * rendered when the field is empty. If null, the field will not render. - * [filter] defines the type of characters which will be accepted, and the - * length of the overall text. - * - * @author sen - * @since 1.0 - * @see UITextField.Filters pre-made text filters. - */ -open class UITextField(text: String, var placeholder: String? = null, var filter: TextFilter) : UIComponent(), UIFocusable { - var text: String = text - set(value) { - field = value - textChangedListener?.forEach { it.value.accept(this) } - } - - /** - * If true, the placeholder text will be rendered if this is defocused - */ - var resetOnDefocus: Boolean = false - - /** - * Invoked when the text within this text field has changed. - */ - var textChangedListener: HashMap>? = null - protected set - - protected var isCtrlHeld = false - - /** Blink **/ - protected var timeSinceLastBlink: Long = 0L - protected var blink: Boolean = true - - init { - Aether.onModifierKeyChange(this.toString()) { key, value -> - if (!this.isFocused()) return@onModifierKeyChange - if (key == UIModifierKey.LEFT_CTRL || key == UIModifierKey.RIGHT_CTRL) isCtrlHeld = !value - if (value) return@onModifierKeyChange - when (key) { - UIModifierKey.BACKSPACE -> { - if (this.text.isNotEmpty()) { - if (this.style.font!!.hasSelection()) { - clearSelection() -// moveCaret(getCaretPosition() - clearSelection()) - } else if (getCaretPosition() > 0) { - this.text = this.text.substring(0, getCaretPosition() - 1) + this.text.substring( - getCaretPosition(), - this.text.length - ) - moveCaret(getCaretPosition() - 1) - } - } - } - // Arrow keys are already handled by UIFont - else -> {} - } - } - } - - override fun deallocate() { - // Deallocate the UICore.onModifierKeyChange listener - Aether.modifierKeyListeners.remove(this.toString()) - } - - override fun updateStyle() { - super.updateStyle() - - // Allocate (if needed), and force the font to be selectable - style.font = style.font ?: UIFont() - style.font!!.isSelectable = true - - // Update the caret - style.caret.update(this) - } - - override fun renderComponent() { - val font = style.font!! - val color = font.fontColor - - font.render(text.ifEmpty { - font.fontColor = style.placeholderColor - placeholder ?: "" - }); font.fontColor = color - - renderer { - val bounds = text.substring(0, getCaretPosition()).fontBounds() - -// style.caret.offsetX = font.cachedX - x + when { -// (font.textAlignment and ALIGNLEFT) != 0 -> bounds[4] -// (font.textAlignment and ALIGNCENTER) != 0 -> bounds[4] -// (font.textAlignment and ALIGNRIGHT) != 0 -> -bounds[4] -// else -> 0f +//package net.prismclient.aether.ui.component.type.input.textfield +// +//import net.prismclient.aether.ui.Aether +//import net.prismclient.aether.ui.component.UIComponent +//import net.prismclient.aether.ui.component.type.input.textfield.caret.UICaret +//import net.prismclient.aether.ui.event.input.UIMouseEvent +//import net.prismclient.aether.ui.renderer.impl.font.UIFont__ +//import net.prismclient.aether.ui.style.UIStyleSheet +//import net.prismclient.aether.ui.util.UIColor +//import net.prismclient.aether.ui.util.extensions.em +//import net.prismclient.aether.ui.util.extensions.px +//import net.prismclient.aether.ui.util.extensions.renderer +//import net.prismclient.aether.ui.util.input.UIModifierKey +//import net.prismclient.aether.ui.util.interfaces.UIFocusable +//import net.prismclient.aether.ui.util.name +//import net.prismclient.aether.ui.util.warn +//import java.lang.Integer.max +//import java.lang.Integer.min +//import java.util.function.Consumer +// +///** +// * [UITextField] is a focusable component which accepts an input from +// * the user via the keyboard. [placeholder] is the text which will be +// * rendered when the field is empty. If null, the field will not render. +// * [filter] defines the type of characters which will be accepted, and the +// * length of the overall text. +// * +// * @author sen +// * @since 1.0 +// * @see UITextField.Filters pre-made text filters. +// */ +//open class UITextField(text: String, var placeholder: String? = null, var filter: TextFilter) : UIComponent(), UIFocusable { +// var text: String = text +// set(value) { +// field = value +// textChangedListener?.forEach { it.value.accept(this) } // } - //style.caret.offsetY = font.cachedY + boundsOf(style.font!!.cachedText)[1] - y - - if (blink && isFocused()) style.caret.render() - if (style.blinkRate > 0 && (timeSinceLastBlink + style.blinkRate <= System.currentTimeMillis())) { - blink = !blink - timeSinceLastBlink = System.currentTimeMillis() - } - } - } - - override fun mousePressed(event: UIMouseEvent) { - super.mousePressed(event) - if (text.isEmpty()) { - moveCaret(0) - } - } - - override fun keyPressed(character: Char) { - if (isCtrlHeld) { - when (character.lowercase()[0]) { - /* Select all */ 'a' -> select(0, text.length) - /* Copy */ 'c' -> { - warn("Copy not implemented") - } - /* Paste */ 'v' -> { - warn("Paste not implemented") - } - /* Cut */ 'x' -> { - warn("Cut not implemented") - } - /* Undo */ 'z' -> { - warn("Undo not implemented") - } - /* Redo */ 'y' -> { - warn("Redo not implemented") - } - } - return - } - if (text.length > filter.maxLength && filter.maxLength != -1) return - if (filter.accept(character)) { - if (isSelected()) clearSelection() - text = text.substring(0, getCaretPosition()) + character + text.substring(getCaretPosition(), text.length) - style.font!!.updateCaretPosition(getCaretPosition() + 1) - } - super.keyPressed(character) - } - - override fun defocus() { - super.defocus() - if (resetOnDefocus) { - text = "" - style.font!!.updateCaretPosition(0) - } - } - - /** - * Moves the caret/cursor to the given position - */ - open fun moveCaret(index: Int) = style.font!!.updateCaretPosition(index) - - /** - * Selects the text with the [startingIndex] as the caret position and the [endingIndex] as the place to select - */ - open fun select(startingIndex: Int, endingIndex: Int) { - style.font!!.select(startingIndex, endingIndex) - } - - /** - * Deselects the text if available - */ - open fun deselect() { - style.font!!.deselect() - } - - /** - * Resets the selection and clears the text - */ - open fun clear() { - deselect() - style.font!!.updateCaretPosition(0) - text = "" - } - - /** - * Clears the active selection (if applicable). - * - * @return The length of the cleared string - */ - open fun clearSelection() { - text = text.substring( - 0, - min(getCaretPosition(), getSelectionPosition()) - ) + text.substring(max(getSelectionPosition(), getCaretPosition()), text.length) - moveCaret(min(getCaretPosition(), getSelectionPosition())) - } - - /** - * Returns true if the font is selected - */ - open fun isSelected() = style.font!!.hasSelection() - - /** - * Returns the position of the caret/cursor - */ - open fun getCaretPosition() = style.font!!.position!!.caretPosition - - /** - * Returns the ending position of the selection - * - * @see getCaretPosition - */ - open fun getSelectionPosition() = style.font!!.position!!.selectionPosition - - /** - * Adds a listener for when the text is changed - */ - @JvmOverloads - open fun onTextChanged(eventName: String = "${textChangedListener?.size ?: 0f}", event: Consumer) { - textChangedListener = textChangedListener ?: hashMapOf() - textChangedListener!![eventName] = event - } - - override fun createsStyle(): UITextFieldSheet = UITextFieldSheet() - - /** - * [TextFilter] holds a string which is compared to the input character. If the character - * is found within the string, it will be added to the text field, else it will not. Furthermore, - * [maxLength] determines the max length of the input text. - * - * @author sen - * @since 4/28/2022 - * @see UITextField.Filters Pre-made filters - * - */ - class TextFilter(val pattern: String, val maxLength: Int = -1) { - fun accept(char: Char) = pattern.contains(char) - } - - companion object Filters { - @JvmStatic - val any = - TextFilter("ABCDEFGHIJKLMOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890~`!@#$%^&*()_+-=|,./<>?;':{}[]\"\\ ") - - @JvmStatic - val alphabet = TextFilter("ABCDEFGHIJKLMOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") - - @JvmStatic - val number = TextFilter("1234567890") - - @JvmStatic - val hex = TextFilter("#ABCDEFabcdef0123456789") - } -} - -class UITextFieldSheet : UIStyleSheet() { - /** - * The caret shape which is drawn to display the caret. - */ - var caret: UICaret = UICaret().apply { - this.width = px(2) - this.height = em(1) - } - - /** - * The rate at which the caret blinks at. 0 = never - */ - var blinkRate: Long = 500L - - /** - * The color of the text when the text field is not focused - */ - var placeholderColor: UIColor? = null - - /** - * Creates a caret DSL block. - */ - inline fun caret(block: UICaret.() -> Unit) { - block.invoke(caret) - } - - override fun copy(): UITextFieldSheet = UITextFieldSheet().name(name).also { - it.apply(this) - it.caret = caret.copy() - it.blinkRate = blinkRate - it.placeholderColor = placeholderColor - } -} \ No newline at end of file +// +// /** +// * If true, the placeholder text will be rendered if this is defocused +// */ +// var resetOnDefocus: Boolean = false +// +// /** +// * Invoked when the text within this text field has changed. +// */ +// var textChangedListener: HashMap>? = null +// protected set +// +// protected var isCtrlHeld = false +// +// /** Blink **/ +// protected var timeSinceLastBlink: Long = 0L +// protected var blink: Boolean = true +// +// init { +// Aether.onModifierKeyChange(this.toString()) { key, value -> +// if (!this.isFocused()) return@onModifierKeyChange +// if (key == UIModifierKey.LEFT_CTRL || key == UIModifierKey.RIGHT_CTRL) isCtrlHeld = !value +// if (value) return@onModifierKeyChange +// when (key) { +// UIModifierKey.BACKSPACE -> { +// if (this.text.isNotEmpty()) { +// if (this.style.font!!.hasSelection()) { +// clearSelection() +//// moveCaret(getCaretPosition() - clearSelection()) +// } else if (getCaretPosition() > 0) { +// this.text = this.text.substring(0, getCaretPosition() - 1) + this.text.substring( +// getCaretPosition(), +// this.text.length +// ) +// moveCaret(getCaretPosition() - 1) +// } +// } +// } +// // Arrow keys are already handled by UIFont +// else -> {} +// } +// } +// } +// +// override fun deallocate() { +// // Deallocate the UICore.onModifierKeyChange listener +// Aether.modifierKeyListeners.remove(this.toString()) +// } +// +// override fun updateStyle() { +// super.updateStyle() +// +// // Allocate (if needed), and force the font to be selectable +// style.font = style.font ?: UIFont__() +// style.font!!.isSelectable = true +// +// // Update the caret +// style.caret.update(this) +// } +// +// override fun renderComponent() { +// val font = style.font!! +// val color = font.fontColor +// +// font.render(text.ifEmpty { +// font.fontColor = style.placeholderColor +// placeholder ?: "" +// }); font.fontColor = color +// +// renderer { +// val bounds = text.substring(0, getCaretPosition()).fontBounds() +// +//// style.caret.offsetX = font.cachedX - x + when { +//// (font.textAlignment and ALIGNLEFT) != 0 -> bounds[4] +//// (font.textAlignment and ALIGNCENTER) != 0 -> bounds[4] +//// (font.textAlignment and ALIGNRIGHT) != 0 -> -bounds[4] +//// else -> 0f +//// } +// //style.caret.offsetY = font.cachedY + boundsOf(style.font!!.cachedText)[1] - y +// +// if (blink && isFocused()) style.caret.render() +// if (style.blinkRate > 0 && (timeSinceLastBlink + style.blinkRate <= System.currentTimeMillis())) { +// blink = !blink +// timeSinceLastBlink = System.currentTimeMillis() +// } +// } +// } +// +// override fun mousePressed(event: UIMouseEvent) { +// super.mousePressed(event) +// if (text.isEmpty()) { +// moveCaret(0) +// } +// } +// +// override fun keyPressed(character: Char) { +// if (isCtrlHeld) { +// when (character.lowercase()[0]) { +// /* Select all */ 'a' -> select(0, text.length) +// /* Copy */ 'c' -> { +// warn("Copy not implemented") +// } +// /* Paste */ 'v' -> { +// warn("Paste not implemented") +// } +// /* Cut */ 'x' -> { +// warn("Cut not implemented") +// } +// /* Undo */ 'z' -> { +// warn("Undo not implemented") +// } +// /* Redo */ 'y' -> { +// warn("Redo not implemented") +// } +// } +// return +// } +// if (text.length > filter.maxLength && filter.maxLength != -1) return +// if (filter.accept(character)) { +// if (isSelected()) clearSelection() +// text = text.substring(0, getCaretPosition()) + character + text.substring(getCaretPosition(), text.length) +// style.font!!.updateCaretPosition(getCaretPosition() + 1) +// } +// super.keyPressed(character) +// } +// +// override fun defocus() { +// super.defocus() +// if (resetOnDefocus) { +// text = "" +// style.font!!.updateCaretPosition(0) +// } +// } +// +// /** +// * Moves the caret/cursor to the given position +// */ +// open fun moveCaret(index: Int) = style.font!!.updateCaretPosition(index) +// +// /** +// * Selects the text with the [startingIndex] as the caret position and the [endingIndex] as the place to select +// */ +// open fun select(startingIndex: Int, endingIndex: Int) { +// style.font!!.select(startingIndex, endingIndex) +// } +// +// /** +// * Deselects the text if available +// */ +// open fun deselect() { +// style.font!!.deselect() +// } +// +// /** +// * Resets the selection and clears the text +// */ +// open fun clear() { +// deselect() +// style.font!!.updateCaretPosition(0) +// text = "" +// } +// +// /** +// * Clears the active selection (if applicable). +// * +// * @return The length of the cleared string +// */ +// open fun clearSelection() { +// text = text.substring( +// 0, +// min(getCaretPosition(), getSelectionPosition()) +// ) + text.substring(max(getSelectionPosition(), getCaretPosition()), text.length) +// moveCaret(min(getCaretPosition(), getSelectionPosition())) +// } +// +// /** +// * Returns true if the font is selected +// */ +// open fun isSelected() = style.font!!.hasSelection() +// +// /** +// * Returns the position of the caret/cursor +// */ +// open fun getCaretPosition() = style.font!!.position!!.caretPosition +// +// /** +// * Returns the ending position of the selection +// * +// * @see getCaretPosition +// */ +// open fun getSelectionPosition() = style.font!!.position!!.selectionPosition +// +// /** +// * Adds a listener for when the text is changed +// */ +// @JvmOverloads +// open fun onTextChanged(eventName: String = "${textChangedListener?.size ?: 0f}", event: Consumer) { +// textChangedListener = textChangedListener ?: hashMapOf() +// textChangedListener!![eventName] = event +// } +// +// override fun createsStyle(): UITextFieldSheet = UITextFieldSheet() +// +// /** +// * [TextFilter] holds a string which is compared to the input character. If the character +// * is found within the string, it will be added to the text field, else it will not. Furthermore, +// * [maxLength] determines the max length of the input text. +// * +// * @author sen +// * @since 4/28/2022 +// * @see UITextField.Filters Pre-made filters +// * +// */ +// class TextFilter(val pattern: String, val maxLength: Int = -1) { +// fun accept(char: Char) = pattern.contains(char) +// } +// +// companion object Filters { +// @JvmStatic +// val any = +// TextFilter("ABCDEFGHIJKLMOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890~`!@#$%^&*()_+-=|,./<>?;':{}[]\"\\ ") +// +// @JvmStatic +// val alphabet = TextFilter("ABCDEFGHIJKLMOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") +// +// @JvmStatic +// val number = TextFilter("1234567890") +// +// @JvmStatic +// val hex = TextFilter("#ABCDEFabcdef0123456789") +// } +//} +// +//class UITextFieldSheet : UIStyleSheet() { +// /** +// * The caret shape which is drawn to display the caret. +// */ +// var caret: UICaret = UICaret().apply { +// this.width = px(2) +// this.height = em(1) +// } +// +// /** +// * The rate at which the caret blinks at. 0 = never +// */ +// var blinkRate: Long = 500L +// +// /** +// * The color of the text when the text field is not focused +// */ +// var placeholderColor: UIColor? = null +// +// /** +// * Creates a caret DSL block. +// */ +// inline fun caret(block: UICaret.() -> Unit) { +// block.invoke(caret) +// } +// +// override fun copy(): UITextFieldSheet = UITextFieldSheet().name(name).also { +// it.apply(this) +// it.caret = caret.copy() +// it.blinkRate = blinkRate +// it.placeholderColor = placeholderColor +// } +//} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt index 5459dd0..b14eaba 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt @@ -169,6 +169,7 @@ open class UIContainer : UIFrame(), UIFocusable, UILayo super.mouseScrolled(mouseX, mouseY, scrollAmount) } + @Suppress("UNCHECKED_CAST") override fun createsStyle(): T = UIContainerSheet() as T } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt index 6e30a6e..5fb02d4 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt @@ -167,6 +167,7 @@ abstract class UIFrame : UIComponent(), UIFocusable { requestUpdate() } + @Suppress("UNCHECKED_CAST") override fun createsStyle(): T = UIFrameSheet() as T } From 6e973795bebd8979ab0d065b28bb13822222eea7 Mon Sep 17 00:00:00 2001 From: sen Date: Sun, 17 Jul 2022 12:50:04 -0700 Subject: [PATCH 27/37] Figma text --- .../aether/ui/component/UIComponent.kt | 10 +- .../aether/ui/component/type/UILabel.kt | 10 +- .../component/type/input/button/UIButton.kt | 15 +- .../aether/ui/dsl/UIRendererDSL.kt | 16 +- .../aether/ui/renderer/UIRenderer.kt | 32 +- .../ui/renderer/impl/font/TextAlignment.kt | 37 + .../aether/ui/renderer/impl/font/UIFont.kt | 788 +++--------------- .../aether/ui/style/UIStyleSheet.kt | 32 +- .../aether/ui/util/extensions/Units.kt | 4 +- src/test/kotlin/Renderer.kt | 141 +++- src/test/kotlin/examples/Default.kt | 283 +------ src/test/kotlin/examples/deps/Generic.kt | 14 +- 12 files changed, 424 insertions(+), 958 deletions(-) create mode 100644 src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/TextAlignment.kt diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt index 5c658ca..64605c8 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/UIComponent.kt @@ -9,9 +9,9 @@ import net.prismclient.aether.ui.component.type.layout.UIFrameSheet import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.impl.background.UIBackground -import net.prismclient.aether.ui.renderer.impl.font.UIFont import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.unit.UIUnit +import net.prismclient.aether.ui.util.Block import net.prismclient.aether.ui.util.extensions.calculate import net.prismclient.aether.ui.util.extensions.renderer import net.prismclient.aether.ui.util.interfaces.UIFocusable @@ -30,7 +30,7 @@ import java.util.function.Consumer * values. The relative values are the normal property, except with the bounds of the * component calculated and applied to it via the [calculateBounds] function. Bounds * include the padding and margin properties of the component. Classes such as [UIBackground] - * render the background to the relative properties, while other classes such as [UIFont] + * render the background to the relative properties, while other classes such as [UIFont__] * render to the absolute properties. * * @author sen @@ -241,8 +241,7 @@ abstract class UIComponent { * might request for this method to be invoked. */ open fun update() { - if (!this::style.isInitialized) { - println("creating style...") + if (!hasStyle()) { style = createsStyle() } @@ -334,7 +333,6 @@ abstract class UIComponent { animations!!.forEach { it.value.update() } animations!!.entries.removeIf { it.value.isCompleted } if (animations!!.isEmpty()) { - println("Completed animation") animations = null } } @@ -679,6 +677,8 @@ abstract class UIComponent { if (parent != null) parent!!.requestUpdate() } + inline fun stylize(block: Block) = style.block() + /** * [UninitializedStyleSheetException] is thrown when the style sheet of * this was not initialized. The solution is to add a valid style sheet diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt index fac02bd..d5a13e5 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt @@ -9,9 +9,15 @@ import net.prismclient.aether.ui.style.UIStyleSheet * @author sen * @since 1.0 */ -class UILabel(var text: String) : UIComponent() { +class UILabel(text: String) : UIComponent() { + var text: String = text + set(value) { + field = value + style.font?.actualText = text + } + override fun renderComponent() { - style.font?.render(text) + style.font?.render() } override fun createsStyle(): UIStyleSheet = UIStyleSheet() diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt index 0c0082a..26fcf0f 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt @@ -9,9 +9,20 @@ import net.prismclient.aether.ui.style.UIStyleSheet * @author sen * @since 1.0 */ -open class UIButton(open var text: String) : UIComponent() { +open class UIButton(text: String) : UIComponent() { + open var text: String = text + set(value) { + field = value + style.font?.actualText = text + } + + override fun update() { + super.update() + style.font?.actualText = text + } + override fun renderComponent() { - style.font?.render(text) + style.font?.render() } override fun createsStyle(): UIStyleSheet = UIStyleSheet() diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt index cf0f1c0..38b6ba9 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt @@ -5,7 +5,7 @@ import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.UIRenderer import net.prismclient.aether.ui.renderer.impl.border.UIStrokeDirection -import net.prismclient.aether.ui.renderer.impl.font.UIFont +import net.prismclient.aether.ui.renderer.impl.font.TextAlignment import net.prismclient.aether.ui.renderer.impl.property.UIRadius import net.prismclient.aether.ui.renderer.other.UIContentFBO import net.prismclient.aether.ui.util.* @@ -92,23 +92,13 @@ object UIRendererDSL { * Applies the given font values to the active context. */ @JvmStatic - fun font(fontFace: String, fontSize: Float, fontAlign: Int, fontSpacing: Float) { + fun font(fontFace: String, fontSize: Float, horizontalAlignment: TextAlignment, verticalAlignment: TextAlignment, fontSpacing: Float) { renderer.fontFace(fontFace) renderer.fontSize(fontSize) - renderer.fontAlignment(fontAlign) + renderer.fontAlignment(horizontalAlignment, verticalAlignment) renderer.fontSpacing(fontSpacing) } - /** - * Applies the property of the given [font] to the active context. - */ - - @JvmStatic - fun font(font: UIFont) { - color(font.fontColor) - font(font.fontName, font.cachedFontSize, font.textAlignment, font.cachedFontSpacing) - } - /** * Renders the given string at the given position */ diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt index cc7ac79..a989fdd 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt @@ -1,6 +1,7 @@ package net.prismclient.aether.ui.renderer import net.prismclient.aether.ui.renderer.image.UIImageData +import net.prismclient.aether.ui.renderer.impl.font.TextAlignment import net.prismclient.aether.ui.renderer.other.UIContentFBO import java.nio.ByteBuffer @@ -185,13 +186,16 @@ interface UIRenderer { */ fun fontSize(size: Float) - fun fontAlignment(alignment: Int) - /** * Spaces the text by the given [spacing]. */ fun fontSpacing(spacing: Float) + /** + * Aligns the text to the given [horizontalAlignment] and [verticalAlignment] based on the enum [TextAlignment]. + */ + fun fontAlignment(horizontalAlignment: TextAlignment, verticalAlignment: TextAlignment) + /** * Renders the [text] based on the [x], and [y] position and all the states * @@ -200,6 +204,30 @@ interface UIRenderer { */ fun renderText(text: String, x: Float, y: Float) + /** + * Renders the given [text] splitting each line by the given [lineHeight]. + */ + fun renderText(text: ArrayList, x: Float, y: Float, lineHeight: Float) + + /** + * Renders the given [text]; however, if the text exceeds [lineWidth], the text will be put onto a newline + * with the spacing of [lineHeight]. If [lines] is not null, the string will be added to it. + * + * @return The row count + */ + fun renderText(text: String, x: Float, y: Float, lineWidth: Float, lineHeight: Float, lines: ArrayList?): Int + + /** + * Calculates the bounds given the array of [text] splitting it by the [lineHeight]. + */ + fun calculateText(text: ArrayList, x: Float, y: Float, lineHeight: Float) + + /** + * Calculates the given [text] where if the text exceeds the [lineWidth], it will + * be put onto a new line. If [lines] is not null, the string will be added to it. + */ + fun calculateText(text: String, x: Float, y: Float, lineWidth: Float, lineHeight: Float, lines: ArrayList?): Int + /** * Returns an array of five floats representing the most recently rendered text. * diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/TextAlignment.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/TextAlignment.kt new file mode 100644 index 0000000..b3f125c --- /dev/null +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/TextAlignment.kt @@ -0,0 +1,37 @@ +package net.prismclient.aether.ui.renderer.impl.font + +/** + * The equivalent of Vertical and Horizontal text alignment in Figma. Certain properties cannot be used + * on the certain axis. See the enum documentation for more information. + * + * @author sen + * @since 1.3 + */ +enum class TextAlignment { + /** + * Aligns to the left on the x-axis + */ + LEFT, + + /** + * Aligns to the center / middle on the x/y-axis + */ + CENTER, + + /** + * Aligns to the right of the x-axis + */ + RIGHT, + + /** + * Aligns the text to the top on the y-axis + */ + TOP, + + //CENTER// + + /** + * Aligns to the text to the bottom on the y-axis + */ + BOTTOM, +} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt index 85ac2e9..21f3b68 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt @@ -1,750 +1,228 @@ package net.prismclient.aether.ui.renderer.impl.font -import net.prismclient.aether.ui.Aether -import net.prismclient.aether.ui.animation.UIAnimation import net.prismclient.aether.ui.component.UIComponent -import net.prismclient.aether.ui.component.type.UILabel -import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.dsl.UIRendererDSL -import net.prismclient.aether.ui.dsl.UIRendererDSL.fontBounds -import net.prismclient.aether.ui.dsl.UIRendererDSL.indexOffset -import net.prismclient.aether.ui.renderer.UIRenderer -import net.prismclient.aether.ui.renderer.impl.font.UIFont.FontRenderType +import net.prismclient.aether.ui.renderer.impl.background.UIBackground +import net.prismclient.aether.ui.renderer.impl.font.TextAlignment.* import net.prismclient.aether.ui.shape.UIShape -import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.unit.UIUnit import net.prismclient.aether.ui.util.UIColor -import net.prismclient.aether.ui.util.extensions.* -import net.prismclient.aether.ui.util.input.UIModifierKey -import net.prismclient.aether.ui.util.interfaces.UIAnimatable -import java.lang.Integer.max -import java.lang.Integer.min +import net.prismclient.aether.ui.util.extensions.asRGBA +import net.prismclient.aether.ui.util.extensions.calculate +import net.prismclient.aether.ui.util.extensions.renderer +import net.prismclient.aether.ui.util.middle import java.util.regex.Pattern /** - * [UIFont] controls the rendering of text on string. It is a default part - * of [UIStyleSheet]. Because this is a shape, it has its own position - * relative to the component and can be transformed your desire. + * [UIFont] is the core for rendering text on screen with components. It handles everything + * from line breaking to text selection. The design is based off of Figma's text components. + * Features such as [] * - * Multiline text is an available feature. By default, it is impossible to render - * multiline text. However, it can be enabled by changing [fontRenderType] to a multiline - * supported type. Backend a stored variable, [cachedText] is used to store the text which - * is then used to calculate selections and other things. If it is not a multiline supported - * type, the text will always be a size of 1. - * - * An important feature pertain to this class is selection. The mouse and keyboard - * are able to select a portion of singular, and multiline text. In the case of multiline - * text, the length of each line is increased by 1 because of the ability to have the caret - * at the end of a selection. - * - * Note: Properties width, and height are ignored. The [fontRenderType] property is null for - * animations however if a string is attempted to be rendered while it is null, it is - * automatically set to [FontRenderType.NORMAL]. + * To break text by the width, set [lineBreakWidth] to a value. * * @author sen - * @since 4/26/2022 + * @since 1.0 */ -@Suppress("MemberVisibilityCanBePrivate") -open class UIFont : UIShape(), UIAnimatable { - companion object { - @JvmStatic - protected val newline: Pattern = Pattern.compile("\\r?\\n|\\r") - } - - /** - * When true, the component will be ensured to be at - * least the size of the font width and height. It does - * not update the x and y positions of the component if - * the text spans to the left or up - */ - var overrideParent: Boolean = true - - /** - * Instructs [UIFont] how to render text. The default value is [FontRenderType.NORMAL]. - * - * @see FontRenderType - * @see appendedString - */ - var fontRenderType: FontRenderType? = null - - /** - * The alignment of the text - * - * @see UIRenderer Text Alignment - */ - var textAlignment = 0 - - /** - * Read-only property set by [fontStyle], [fontType] and [fontFamily]. If when the font was loaded had a - * special name, use [overwriteFontName] to set it to that font name. - */ - var fontName: String = "" - protected set - - /** - * Specifies if the style is Normal or Italic - * - * @see FontStyle - */ - var fontStyle: FontStyle? = null - set(value) { - field = value; updateFontName() - } +open class UIFont : UIShape() { /** - * The color of this font, represent in [UIColor]. + * The actual, unformatted original text provided to the font */ - var fontColor: UIColor? = null - - /** - * The name of the font family. - */ - var fontFamily = "" + var actualText: String = "" set(value) { - field = value; updateFontName() - } - - /** - * Specifies the font type e.g. Regular, Bold, Thin etc.. - * - * Default is [FontType.Regular] - * - * @see FontType - */ - var fontType: FontType? = null - set(value) { - field = value; updateFontName() + field = value + updateFont() } + var fontName: String = "" var fontSize: UIUnit? = null - - /** - * The spacing between each character. - */ + var fontColor: UIColor? = null var fontSpacing: UIUnit? = null - /** - * If the [fontRenderType] is WRAP, the text will wrap at the given width. - */ - var lineBreakWidth: UIUnit? = null - - /** - * If the [fontRenderType] is multiline, the next line of text will be the font - * size plus the given value, or 0 if null - */ - var lineHeight: UIUnit? = null - - /** - * When applicable, the given text will be appended to the string. - * - * @see FontRenderType.APPEND - */ - var appendedString: String? = null - - /** - * Instructs Aether that this is a selectable font. Selectable fonts are highlighted - * when the mouse clicks and drags over the font. It also allows for functions such - * as copying the text. - */ - var isSelectable = false + var textResizing: TextResizing = TextResizing.AutoWidth - /** - * The color of the selection if [isSelectable] - */ - var selectionColor: UIColor? = null + var horizontalAlignment: TextAlignment = CENTER + var verticalAlignment: TextAlignment = CENTER /** - * The amount of lines in the text. 1 by default if it is a single line render type - * - * @see fontRenderType + * The spacing between each line break. */ - var lineCount = 1 - protected set + var lineHeightSpacing: UIUnit? = null /** - * The bounds of the most recently rendered text. It represents the x, y - * and the ending x, y positions of the text. The fifth value represents - * the next position that the text would be rendered at. - * - * @see UIRenderer.textBounds + * The text to be appended to the string when it is truncated */ - var textBounds: FloatArray = floatArrayOf(0f, 0f, 0f, 0f, 0f) + var truncatedText: String? = null + set(value) { + field = value + if (value != null) textResizing = TextResizing.TruncateText + } - /** - * When true, the text is actively selected. - * - * @see isSelectable Must be true. - */ - var selected = false - protected set + // -- Internal properties -- // /** - * The holder of the current position of the caret/cursor and the ending selection position (if applicable). - * This is null if [isSelectable] is false + * The text based on [actualText] except a new line is created for `\n` and line break. */ - var position: Positions? = null + var text: ArrayList = arrayListOf() - /** - * If true, fontName will not update automatically. It will only update when [overwriteFontName] is called - * - * @see overwriteFontName - */ - var isOverridden = false + var textBounds: FloatArray = floatArrayOf() protected set - - /** - * If true, the font will not be updated. Used internally to avoid recursive calls. - */ - protected var ignore = false - var cachedFontSize: Float = 0f protected set var cachedFontSpacing: Float = 0f protected set - var cachedLineBreakWidth: Float = 0f + var fontAscender: Float = 0f protected set - var cachedLineHeight: Float = 0f + var fontDescender: Float = 0f protected set - - /** - * An internal variable which holds the text in lines split if necessary. - */ - var cachedText: ArrayList = arrayListOf() + var cachedLineHeightSpacing: Float = 0f protected set - protected var isShiftHeld = false - - /** - * Aligns the text to the given [alignment] - */ - open fun align(alignment: UIAlignment) { - x ?: run { x = px(0) } - y ?: run { y = px(0) } - align(alignment, x!!, y!!) - } override fun update(component: UIComponent<*>?) { - if (ignore) return - - this.component = component!! - cachedX = component.x + calculate(x, component, component.width, component.height, false) //- component.anchorX - cachedY = component.y + calculate(y, component, component.width, component.height, true) //- component.anchorY - cachedWidth = calculate(width, component, component.width, component.height, false) - cachedHeight = calculate(height, component, component.width, component.height, true) - - cachedFontSize = calculate(fontSize, component, component.width, component.height, false) - cachedFontSpacing = calculate(fontSpacing, component, component.width, component.height, false) - - // Selection handling - if (isSelectable) { - // Allocate, and add listeners for when the mouse is moved, pressed and released - position = Positions(0, 0) - val pos = position!! - Aether.onMousePressed("$this-SelectionListener") { // TODO: function for bounds - val mousePosition = getClosestTextIndex(component.getMouseX(), component.getMouseY()) - - if (mousePosition != position!!.caretPosition) { - updateCaretPosition(mousePosition) - selected = true - } - } - Aether.onMouseMove("$this-MoveListener") { - if (selected) { - select(getClosestTextIndex(component.getMouseX(), component.getMouseY()), pos.selectionPosition) - } - } - Aether.onMousePressed("$this-DeselectionListener") { - if (component.getMouseY() <= cachedY + textBounds[1] && component.getMouseY() <= cachedY + textBounds[1] + cachedFontSize) { - deselect() - } - } - Aether.onMouseReleased("$this-DeselectionListener") { selected = false } - Aether.onModifierKeyChange("$this-ModifierListener") { key, value -> - if (key == UIModifierKey.LEFT_SHIFT || key == UIModifierKey.RIGHT_SHIFT) isShiftHeld = !value - if (value) return@onModifierKeyChange - when (key) { - UIModifierKey.ARROW_LEFT -> { - if (hasSelection() && !isShiftHeld) updateCaretPosition( - min( - pos.caretPosition, pos.selectionPosition - ) - ) - else if (isShiftHeld) select(getPreviousPosition(pos.caretPosition), pos.selectionPosition) - else updateCaretPosition(getPreviousPosition(pos.caretPosition)) - } - UIModifierKey.ARROW_RIGHT -> { - if (hasSelection() && !isShiftHeld) updateCaretPosition( - max( - pos.caretPosition, pos.selectionPosition - ) - ) - else if (isShiftHeld) select(getNextPosition(pos.caretPosition), pos.selectionPosition) - else updateCaretPosition(getNextPosition(pos.caretPosition)) - } - else -> {} // TODO: Up and down keys - } - } - Aether.onDeallocation("$this-DeallocationListener") { - // Deallocate all previous event calls - Aether.mousePressedListeners?.remove("$this-MoveListener") - Aether.mousePressedListeners?.remove("$this-DeselectionListener") - Aether.mousePressedListeners?.remove("$this-DeselectionListener") - } - } else { - // Deallocate and remove the listener if it is not selectable - position = null - component.mousePressedListeners?.remove("UIFontSelectionListener") - Aether.mousePressedListeners?.remove("$this-DeselectionListener") - } - - if (component is UILabel) { - ignore = true - render(component.text) - // Updates the component to ensure that the width, and - // height are at least the size of the text rendered - if (overrideParent && (textBounds[2] - cachedX > component.width || textBounds[3] - cachedY > component.height)) { - component.width = (textBounds[2] - cachedX) - component.height = (textBounds[3] - cachedY) - component.calculateBounds() - component.updateAnchorPoint() - component.updatePosition() - component.updateBounds() - component.updateStyle() - cachedX -= component.anchorX - cachedY -= component.anchorY - } - ignore = false - } - - cachedLineBreakWidth = calculate(lineBreakWidth, component, component.width, component.height, false) - cachedLineHeight = calculate(lineHeight, component, component.width, component.height, false) - } - - @Deprecated("Use render(text: String) instead", ReplaceWith("render(text: String)")) - override fun render() = throw IllegalStateException("Use render(text: String) instead") - - open fun render(text: String) { - fontRenderType = fontRenderType ?: FontRenderType.NORMAL - // TODO: Multiline - lineCount = 1 - renderer { - font(this@UIFont) - if (ignore) color(0) - when (fontRenderType) { - FontRenderType.NORMAL -> { - cachedText.clear() - cachedText.add(0, text) - text.render(cachedX, cachedY) - setBounds() - } // TODO: \n available by default - // TODO: Add back multiline support - FontRenderType.NEWLINE -> { - cachedText.clear() - val lines = text.split(newline) - var minx = Float.MAX_VALUE - var miny = Float.MAX_VALUE - var maxx = 0f - var maxy = 0f - - for (i in lines.indices) { - val line = lines[i] - cachedText.add(line) - line.render(cachedX, cachedY + i * (cachedLineHeight + cachedFontSize)) - minx = fontBounds()[0].coerceAtMost(minx) - miny = fontBounds()[1].coerceAtMost(miny) - maxx = fontBounds()[2].coerceAtLeast(maxx) - maxy = fontBounds()[3].coerceAtLeast(maxy) - } - textBounds[0] = minx - textBounds[1] = miny - textBounds[2] = maxx - textBounds[3] = maxy - textBounds[4] = maxx - } - FontRenderType.WRAP -> { - // TODO: Wrap strin grendereruing -// cachedText.clear() -// lineCount = -// render.wrapString(text, cachedX, cachedY, cachedLineBreakWidth, cachedLineHeight, cachedText) -// setBounds() - } // TODO: Clip & Append - FontRenderType.CLIP -> {} - FontRenderType.APPEND -> {} - else -> {} - } - } - renderSelection() + super.update(component) + updateFont() } /** - * As the name suggests, the selection is rendered over the selected text. + * Calculates and updates the bounds for this. */ - open fun renderSelection() { - if (position != null) { - renderer { - color(selectionColor) - val caretLine = getLine(position!!.caretPosition) - val selectionLine = getLine(position!!.selectionPosition) - - if (caretLine == selectionLine) { - val caretX = getXOffset(position!!.caretPosition) - rect( - cachedX + caretX, - cachedY + getYOffset(position!!.caretPosition), - getXOffset(position!!.selectionPosition) - caretX, - cachedFontSize - ) - } else { - val larger = max(position!!.caretPosition, position!!.selectionPosition) - val smaller = min(position!!.caretPosition, position!!.selectionPosition) - val largerLine = getLine(larger) - val smallerLine = getLine(smaller) - - val smallBounds = cachedText[smallerLine].fontBounds() - val smallerX = getXOffset(smaller) - val smallerY = getYOffset(smaller) - - // Start selection - rect( - cachedX + smallerX, - cachedY + smallerY, - smallBounds[4] + smallBounds[0] - smallerX, - cachedFontSize - ) - - var y = - cachedY + ((cachedFontSize + cachedLineHeight) * (smallerLine + 1)) - cachedText[0].fontBounds()[1] - - // Fully selected lines - for (i in smallerLine + 1 until (largerLine - smallerLine) + smallerLine) { - val bounds = cachedText[i].fontBounds() - rect(cachedX + bounds[0], bounds[1] + y, bounds[4], bounds[3] - bounds[1]) - y += cachedLineHeight + cachedFontSize + fun updateFont() { + val component = component ?: throw RuntimeException("Component cannot be null for UIFont") + text.clear() + + // Update initial properties + cachedFontSize = calculate(fontSize, component, cachedWidth, cachedHeight, false) + cachedFontSpacing = calculate(fontSpacing, component, cachedWidth, cachedHeight, false) + cachedLineHeightSpacing = calculate(lineHeightSpacing, component, cachedWidth, cachedHeight, true) + + renderer { + font(fontName, cachedFontSize, horizontalAlignment, verticalAlignment, cachedFontSpacing) + when (textResizing) { + TextResizing.AutoWidth -> { + if (actualText.contains(NEWLINE)) { + textResizing = TextResizing.AutoHeight + updateFont() + return } + text.add(actualText) - val bounds = cachedText[largerLine].fontBounds() + val (minx, miny, maxx, maxy) = actualText.fontBounds() - // End selection - rect(cachedX + bounds[0], bounds[1] + y, getXOffset(larger) - bounds[0], cachedFontSize) + // Update component size + cachedWidth = maxx - minx + cachedHeight = maxy - miny } - } - } - } - - /** - * Selects the text with the [startingIndex] as the caret position and the [endingIndex] - * as the place to select. Does nothing if this is not [isSelectable]. - */ - open fun select(startingIndex: Int, endingIndex: Int) { - if (position != null) { - position!!.caretPosition = startingIndex - position!!.selectionPosition = endingIndex - } - } + TextResizing.AutoHeight -> { + if (width != null || height != null) { + textResizing = TextResizing.FixedSize + updateFont() + return + } + text.addAll(actualText.split(NEWLINE)) - /** - * Deselects the text. It does nothing if this is not [isSelectable]. - */ - open fun deselect() { - if (position != null) { - position!!.selectionPosition = position!!.caretPosition - } - } + renderer.calculateText(text, 0f, 0f, cachedLineHeightSpacing) - /** - * Returns the next position of the given [index]. Because there is an extra - * position add per line, this will return the previous position of the line. - */ - open fun getNextPosition(index: Int): Int { - var i = 0 - for (line in cachedText) { - if (index == i + line.length) return (index + 2).coerceAtMost(getTextLength()) - i += line.length + 1 - } - return (index + 1).coerceAtMost(getTextLength()) - } + val (minx, miny, maxx, maxy) = fontBounds() - /** - * Returns the previous position of the given [index]. Because there is an extra - * position add per line, this will return the previous position of the line. - */ - open fun getPreviousPosition(index: Int): Int { - var i = 0 - for (line in cachedText) { - if (index == i) return (index - 2).coerceAtLeast(0) - i += line.length + 1 - } - return (index - 1).coerceAtLeast(0) - } + // Update component size + cachedWidth = maxx - minx + cachedHeight = maxy - miny + } + TextResizing.FixedSize -> { + renderer.calculateText(actualText, 0f, 0f, cachedWidth, cachedLineHeightSpacing, text) - /** - * Changes the caret position to [index]. This does nothing if this is not [isSelectable] - */ - open fun updateCaretPosition(index: Int) { - if (position != null) { - deselect() - position!!.caretPosition = index - position!!.selectionPosition = index - } - } + val (minx, miny, maxx, maxy) = fontBounds() - /** - * Returns the closest index of text relative to the [mouseX] and - * [mouseY]. If out of bounds, returns the caret position. - */ - open fun getClosestTextIndex(mouseX: Float, mouseY: Float): Int { - var yOffset = cachedText[0].fontBounds()[1] - var i = 0 - for (row in cachedText) { - val rowBounds = row.fontBounds() - val y = cachedY + yOffset - - yOffset += cachedFontSize + cachedLineHeight - - // Row check - if (mouseY < y || mouseY > y + cachedFontSize + cachedLineHeight) { - // Caret can be at the end spot - i += row.length + 1 - continue - } - // Vertical row check. If the mouse is at - // the end or start, return the start, or end index - if (mouseX <= cachedX + rowBounds[0]) { - return i - } else if (mouseX >= cachedX + rowBounds[2]) { - return i + row.length + // Update component size + cachedHeight = (maxy - miny).coerceAtLeast(cachedHeight) + } + TextResizing.TruncateText -> {} } - var x = cachedX + rowBounds[0] - var previous = 0f - - // Iterate through the row. Return the character at the index closest - // to the mouse for the first portion of it, and the last portion of - // the previous character. - for (j in row.indices) { - val width = row[j].toString().fontBounds()[4] / 2f - - if (mouseX >= x && mouseX <= x + width + previous) return i - - x += width + previous - previous = width - - i++ - // Check if the last half of the last character - // in this row is the closest to the mouse - if (j == row.length - 1 && mouseX >= width) return i - } - i++ // Add one for each row because the caret can be at the end of the row + // Update properties + fontAscender = fontAscender() + fontDescender = fontDescender() } - return position!!.caretPosition } /** - * Returns the x offset of the given [index] in the text. Returns 0f if index is out of bounds. + * Splits the active text into [activeText] */ - open fun getXOffset(index: Int): Float { - var i = 0 - for (row in cachedText) { - if (index <= i + row.length) return row.indexOffset(index - i) - i += row.length + 1 - if (index == i - 1) return 0f - } - return 0f - } + fun splitText() { - /** - * Returns the y offset of the caret position. Returns 0f if [index] is out of bounds. - */ - open fun getYOffset(index: Int): Float { - val line = getLine(index) - return cachedText[getLine(index)].fontBounds()[1] + line * (cachedFontSize + cachedLineHeight) } - /** - * Returns true if the [Positions.caretPosition] and [Positions.selectionPosition] aren't equal. - */ - open fun hasSelection() = position!!.caretPosition != position!!.selectionPosition - - /** - * Returns the line index of the given index. Returns the first line if the index is out of bounds. - */ - open fun getLine(index: Int): Int { - var j = 0 - for (i in cachedText.indices) { - if (index <= cachedText[i].length + j) return i - j += cachedText[i].length + 1 + override fun render() { + val x = cachedX + when (verticalAlignment) { + CENTER -> cachedWidth / 2f + RIGHT -> cachedWidth + else -> 0f } - return 0 - } - - /** - * Returns the length of [cachedText] - */ - open fun getTextLength() = cachedText.sumOf { it.length + 1 } - 1 - - /** - * Returns the position of the ascender line in the current font - */ - open fun getAscend(): Float { - renderer { - font(this@UIFont) - "abc".fontBounds() + val y = cachedY + when (horizontalAlignment) { + CENTER -> cachedFontSize / 2f + (cachedHeight - (cachedFontSize * text.size) - (cachedLineHeightSpacing * (text.size - 1))) / 2f + BOTTOM -> cachedFontSize + (cachedHeight - (cachedFontSize * text.size) - (cachedLineHeightSpacing * (text.size - 1))) + else -> 0f } - return UIRendererDSL.fontAscender() - } - - /** - * Returns the position of the descender line in the current font - */ - open fun getDescend(): Float { renderer { - font(this@UIFont) - "abc".fontBounds() + color(fontColor) + font(fontName, cachedFontSize, verticalAlignment, horizontalAlignment, cachedFontSpacing) + when (textResizing) { + TextResizing.AutoWidth -> actualText.render(x, y) + TextResizing.AutoHeight -> renderer.renderText(text, x, y, cachedLineHeightSpacing) + TextResizing.FixedSize -> renderer.renderText(actualText, x, y, cachedWidth, cachedLineHeightSpacing, null) + TextResizing.TruncateText -> {} // TODO: Truncate text + } + color(asRGBA(1f, 0f, 0f, 0.3f)) + rect(cachedX, cachedY, cachedWidth, cachedHeight) } - return UIRendererDSL.fontDescender() - } - - /** - * If the font was loaded with a special name, use this function to override the default formatting - */ - open fun overwriteFontName(name: String) { - fontName = name - isOverridden = true - } - - /** - * Updates [fontName] based on the [fontStyle], [fontType] and [fontFamily] - */ - protected open fun updateFontName() { - if (isOverridden) return - fontName = fontFamily + "-" + (fontType?.name?.lowercase() - ?: "regular") + if (fontStyle == FontStyle.Italic) "-italic" else "" - } - - private fun setBounds() { - val bounds = fontBounds() - textBounds[0] = bounds[0] - textBounds[1] = bounds[1] - textBounds[2] = bounds[2] - textBounds[3] = bounds[3] - textBounds[4] = bounds[4] - } - - override fun animate(animation: UIAnimation<*>, previous: UIFont?, current: UIFont?, progress: Float) { - super.animate(animation, previous, current, progress) - fontColor?.rgba = previous?.fontColor.mix(current?.fontColor, progress) - cachedFontSize = previous?.fontSize.lerp(current?.fontSize, animation.component, progress, false) - cachedLineBreakWidth = - previous?.lineBreakWidth.lerp(current?.lineBreakWidth, animation.component, progress, false) - cachedLineHeight = previous?.lineHeight.lerp(current?.lineHeight, animation.component, progress, true) - selectionColor?.rgba = previous?.selectionColor.mix(current?.selectionColor, progress) } - override fun save(animation: UIAnimation<*>, keyframe: UIFont?) { - super.save(animation, keyframe) - overrideParent = keyframe?.overrideParent ?: overrideParent - fontRenderType = keyframe?.fontRenderType ?: fontRenderType - textAlignment = keyframe?.textAlignment ?: textAlignment - fontName = keyframe?.fontName ?: fontName - fontStyle = keyframe?.fontStyle ?: fontStyle - fontColor = keyframe?.fontColor ?: fontColor?.copy() - fontStyle = keyframe?.fontStyle ?: fontStyle - fontFamily = keyframe?.fontFamily ?: fontFamily - fontType = keyframe?.fontType ?: fontType - fontSize = keyframe?.fontSize ?: fontSize?.copy() - fontSpacing = keyframe?.fontSpacing ?: fontSpacing?.copy() - lineBreakWidth = keyframe?.lineBreakWidth ?: lineBreakWidth?.copy() - lineHeight = keyframe?.lineHeight ?: lineHeight?.copy() - appendedString = keyframe?.appendedString ?: appendedString - isSelectable = keyframe?.isSelectable ?: isSelectable - selectionColor = keyframe?.selectionColor ?: selectionColor + override fun copy(): UIFont = UIFont().also { + it.actualText = actualText + it.fontName = fontName + it.fontSize = fontSize?.copy() + it.fontColor = fontColor?.copy() + it.fontSpacing = fontSpacing?.copy() + it.textResizing = textResizing + it.verticalAlignment = verticalAlignment + it.horizontalAlignment = horizontalAlignment + it.lineHeightSpacing = lineHeightSpacing?.copy() + it.truncatedText = truncatedText } /** - * Instructs [UIFont] on how to render the text. See the enums for details. + * The equivalent of the Resizing property for fonts in Figma. See the respected enums + * for information on what it does. Change [UIFont.textResizing] to adjust this property. * * @author sen - * @since 1.0 + * @since 1.3 */ - enum class FontRenderType { - /** - * Renders the string normally, as a one line string. - */ - NORMAL, - + enum class TextResizing { /** - * Creates a new line when \n is appended to a string + * Renders the string as a one line text where the width and height are the bounds of it. If + * the text contains a line break, this will automatically be switched to [AutoHeight]. */ - NEWLINE, + AutoWidth, /** - * Omits any text that exceeds the bounds of [lineBreakWidth]. + * Like [AutoWidth], however it supports multi-line text. The bounds will adjust to the overall size of the text. */ - CLIP, + AutoHeight, /** - * If the string exceeds the bounds, the text will be wrapped + * Creates a newline when the width of the text exceeds the size of this. The height + * is adjusted to be at least the height of the text. */ - WRAP, + FixedSize, /** - * Renders a string until the given width, where then the string is - * truncated to the point, and the appended string is added. For example, - * if the text is "Hello", the appended string is ".." and "Hel.." width - * is greater than width, then the string rendered is "Hel..". If appended - * string is blank, the string is cut off at the given width point, like normal - * clipped text. + * Cuts the text off at the point where it exceeds the width of this, and append the string [UIFont.truncatedText] */ - APPEND - } - - /** - * The style of the font: Normal or Italic - * - * @author sen - * @since 1.0 - */ - enum class FontStyle { - Normal, Italic - } - - /** - * The type of the font: Regular, Bold, Thin etc.. - * - * @author sen - * @since 1.0 - */ - enum class FontType { - Regular, Medium, Black, Bold, Light, Thin + TruncateText } - /** - * [Positions] is automatically allocated when the property [isSelectable] is true. It describes - * the position which the caret is at; the position where the mouse clicked relative to the font. It - * also holds the ending point if a selection in the font is active. - * - * @author sen - * @since 1.0 - */ - class Positions(var caretPosition: Int, var selectionPosition: Int) - - override fun copy(): UIFont = UIFont().also { - it.apply(this) - - it.overrideParent = overrideParent - it.fontRenderType = fontRenderType - it.textAlignment = textAlignment - it.fontStyle = fontStyle - it.fontType = fontType - it.fontColor = fontColor?.copy() - it.fontFamily = fontFamily - it.fontSize = fontSize?.copy() - it.fontSpacing = fontSpacing?.copy() - it.lineBreakWidth = lineBreakWidth?.copy() - it.lineHeight = lineHeight?.copy() - it.appendedString = appendedString - it.isSelectable = isSelectable - it.selectionColor = selectionColor - - if (isOverridden) it.overwriteFontName(fontName) + companion object { + @JvmStatic + protected val NEWLINE: Regex = Pattern.compile("\\r?\\n|\\r").toRegex() } } \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt index b8b55c1..acd4fd6 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt @@ -262,22 +262,22 @@ open class UIStyleSheet() : UICopy, UIAnimatable { /** * Creates a font DSL block which optionally accepts a size, color, text alignment, font family, and font type. */ - @JvmOverloads - inline fun font( - fontFamily: String = font?.fontFamily ?: "", - fontSize: UIUnit? = font?.fontSize, - fontColor: UIColor? = font?.fontColor, - textAlignment: Int = font?.textAlignment ?: 0, - fontType: UIFont.FontType? = font?.fontType, - block: UIFont.() -> Unit = {} - ) = font { - this.fontSize = fontSize - this.fontColor = fontColor - this.textAlignment = textAlignment - this.fontFamily = fontFamily - this.fontType = fontType - this.block() - } +// @JvmOverloads +// inline fun font( +// fontFamily: String = font?.fontFamily ?: "", +// fontSize: UIUnit? = font?.fontSize, +// fontColor: UIColor? = font?.fontColor, +// textAlignment: Int = font?.textAlignment ?: 0, +// fontType: UIFont.FontType? = font?.fontType, +// block: UIFont.() -> Unit = {} +// ) = font { +// this.fontSize = fontSize +// this.fontColor = fontColor +// this.textAlignment = textAlignment +// this.fontFamily = fontFamily +// this.fontType = fontType +// this.block() +// } /** Plotting **/ diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Units.kt b/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Units.kt index dbdd400..1ecd82a 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Units.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Units.kt @@ -180,8 +180,8 @@ fun calculate(unit: UIUnit?, component: UIComponent<*>?, width: Float, height: F return when (unit.type) { PIXELS -> unit.value RELATIVE -> (if (isY) height else width) * unit.value - ASCENDER -> (component?.style?.font?.getAscend() ?: 0f) * unit.value - DESCENDER -> (component?.style?.font?.getDescend() ?: 0f) * unit.value + ASCENDER -> (component?.style?.font?.fontAscender ?: 0f) * unit.value + DESCENDER -> (component?.style?.font?.fontDescender ?: 0f) * unit.value EM -> (component?.style?.font?.cachedFontSize ?: 0f) * unit.value WIDTH -> width * unit.value HEIGHT -> height * unit.value diff --git a/src/test/kotlin/Renderer.kt b/src/test/kotlin/Renderer.kt index 3bde412..0cdd08c 100644 --- a/src/test/kotlin/Renderer.kt +++ b/src/test/kotlin/Renderer.kt @@ -2,7 +2,9 @@ import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.UIRenderer import net.prismclient.aether.ui.renderer.image.UIImageData +import net.prismclient.aether.ui.renderer.impl.font.TextAlignment import net.prismclient.aether.ui.renderer.other.UIContentFBO +import net.prismclient.aether.ui.util.bottom import net.prismclient.aether.ui.util.extensions.getAlpha import net.prismclient.aether.ui.util.extensions.getBlue import net.prismclient.aether.ui.util.extensions.getGreen @@ -208,10 +210,24 @@ object Renderer : UIRenderer { override fun fontSize(size: Float) = nvgFontSize(ctx, size) - override fun fontAlignment(alignment: Int) = nvgTextAlign(ctx, alignment) - override fun fontSpacing(spacing: Float) = nvgTextLetterSpacing(ctx, spacing) + override fun fontAlignment(horizontalAlignment: TextAlignment, verticalAlignment: TextAlignment) { + var alignment = when (horizontalAlignment) { + TextAlignment.LEFT -> NVG_ALIGN_LEFT + TextAlignment.CENTER -> NVG_ALIGN_CENTER + TextAlignment.RIGHT -> NVG_ALIGN_RIGHT + else -> 0 + } + alignment = alignment or when (verticalAlignment) { + TextAlignment.TOP -> NVG_ALIGN_TOP + TextAlignment.CENTER -> NVG_ALIGN_MIDDLE + TextAlignment.BOTTOM -> NVG_ALIGN_BOTTOM + else -> 0 + } + nvgTextAlign(ctx, alignment) + } + override fun renderText(text: String, x: Float, y: Float) { nvgFillColor(ctx, fillColor) nvgText(ctx, x, y, text) @@ -219,6 +235,127 @@ object Renderer : UIRenderer { fontBounds[4] = nvgTextBounds(ctx, x, y, text, fontBounds) } + override fun renderText(text: ArrayList, x: Float, y: Float, lineHeight: Float) { + var offset = y + var minx = x + var miny = y + var maxx = 0f + var maxy = 0f + + for (i in text.indices) { + renderText(text[i], x, offset) + minx = fontBounds[0].coerceAtMost(minx) + miny = fontBounds[1].coerceAtMost(miny) + maxx = fontBounds[2].coerceAtLeast(maxx) + maxy = fontBounds[3].coerceAtLeast(maxy) + offset += fontBounds[3] - fontBounds[1] + lineHeight + } + + fontBounds[0] = minx + fontBounds[1] = miny + fontBounds[2] = maxx + fontBounds[3] = maxy + fontBounds[4] = maxx + } + + val rows = NVGTextRow.create(50) + + override fun renderText(text: String, x: Float, y: Float, lineWidth: Float, lineHeight: Float, lines: ArrayList?): Int { + val nrows = nvgTextBreakLines(ctx, text, lineWidth, rows) + + var offset = y + var minx = x + var miny = y + var maxx = 0f + var maxy = 0f + + for (i in 0 until nrows) { + val row = rows[i] + + lines?.add(MemoryUtil.memUTF8(row.start(), (row.end() - row.start()).toInt())) + nnvgTextBounds(ctx, x, offset, row.start(), row.end(), fontBounds) + nnvgText(ctx, x, offset, row.start(), row.end()) + + minx = fontBounds[0].coerceAtMost(minx) + miny = fontBounds[1].coerceAtMost(miny) + maxx = fontBounds[2].coerceAtLeast(maxx) + maxy = fontBounds[3].coerceAtLeast(maxy) + + offset += fontBounds[3] - fontBounds[1] + lineHeight + } + + fontBounds[0] = minx + fontBounds[1] = miny + fontBounds[2] = maxx + fontBounds[3] = maxy + fontBounds[4] = maxx + + return nrows + } + + override fun calculateText(text: ArrayList, x: Float, y: Float, lineHeight: Float) { + var offset = y + var minx = x + var miny = y + var maxx = 0f + var maxy = 0f + + for (i in text.indices) { + nvgTextBounds(ctx, x, offset, text[i], fontBounds) + minx = fontBounds[0].coerceAtMost(minx) + miny = fontBounds[1].coerceAtMost(miny) + maxx = fontBounds[2].coerceAtLeast(maxx) + maxy = fontBounds[3].coerceAtLeast(maxy) + offset += fontBounds[3] - fontBounds[1] + lineHeight + } + + fontBounds[0] = minx + fontBounds[1] = miny + fontBounds[2] = maxx + fontBounds[3] = maxy + fontBounds[4] = maxx + } + + override fun calculateText( + text: String, + x: Float, + y: Float, + lineWidth: Float, + lineHeight: Float, + lines: ArrayList? + ): Int { + val nrows = nvgTextBreakLines(ctx, text, lineWidth, rows) + + var offset = y + + var minx = x + var miny = y + var maxx = 0f + var maxy = 0f + + for (i in 0 until nrows) { + val row = rows[i] + + lines?.add(MemoryUtil.memUTF8(row.start(), (row.end() - row.start()).toInt())) + nnvgTextBounds(ctx, x, offset, row.start(), row.end(), fontBounds) + + minx = fontBounds[0].coerceAtMost(minx) + miny = fontBounds[1].coerceAtMost(miny) + maxx = fontBounds[2].coerceAtLeast(maxx) + maxy = fontBounds[3].coerceAtLeast(maxy) + + offset += fontBounds[3] - fontBounds[1] + lineHeight + } + + fontBounds[0] = minx + fontBounds[1] = miny + fontBounds[2] = maxx + fontBounds[3] = maxy + fontBounds[4] = maxx + + return nrows + } + override fun fontBounds(): FloatArray = fontBounds override fun fontBounds(text: String): FloatArray { diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index 95152f4..49640fa 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -1,267 +1,46 @@ package examples -import examples.deps.Generic -import net.prismclient.aether.ui.animation.ease.impl.UIQuart -import net.prismclient.aether.ui.component.type.UILabel -import net.prismclient.aether.ui.component.type.image.UIImage -import net.prismclient.aether.ui.component.type.image.UIImageSheet -import net.prismclient.aether.ui.component.type.input.button.UIButton -import net.prismclient.aether.ui.component.type.layout.UIAutoLayout -import net.prismclient.aether.ui.component.type.layout.UIListLayout -import net.prismclient.aether.ui.component.type.layout.UIContainerSheet import net.prismclient.aether.ui.component.util.enums.UIAlignment -import net.prismclient.aether.ui.dsl.UIAssetDSL -import net.prismclient.aether.ui.renderer.UIProvider +import net.prismclient.aether.ui.renderer.impl.font.TextAlignment import net.prismclient.aether.ui.renderer.impl.font.UIFont import net.prismclient.aether.ui.screen.UIScreen -import net.prismclient.aether.ui.style.UIStyleSheet -import net.prismclient.aether.ui.util.* -import net.prismclient.aether.ui.util.extensions.* +import net.prismclient.aether.ui.style.util.UIFontFamily +import net.prismclient.aether.ui.util.create +import net.prismclient.aether.ui.util.extensions.colorOf +import net.prismclient.aether.ui.util.extensions.px +import net.prismclient.aether.ui.util.left +import net.prismclient.aether.ui.util.style class Default : UIScreen { - - val activeNavbarButtonColor = colorOf(87, 164, 255) - val hoveredNavbarButtonColor = colorOf(41, 41, 41) - override fun build() { - create { - include(Generic()) - UIAssetDSL.image("NavbarLogo", "/prism/logo/Logo.png", flags = REPEATX or REPEATY or GENERATE_MIPMAPS) - // TODO: test if lods stuff in root directory - UIAssetDSL.bulkLoad("/prism/icons", imageFlags = REPEATX or REPEATY or GENERATE_MIPMAPS) - createNavbar() - } - } - - private fun createNavbar() { - ucreate { - val navbar = container { - - UIImageSheet().style("navbar-logo") { - control(UIAlignment.CENTER) - size(56, 62) - } - - // Logo - container { - image("NavbarLogo", "navbar-logo") - }.style("logo") { - size(rel(1), px(118)) - } - - val navbarButtonStyle = UIContainerSheet().style("navbar-button") { - control(UIAlignment.CENTER) - size(206, 40) - margin { marginBottom = px(8) } - background(colorOf(0)) - } - - val activeNavbarButtonStyle = UIContainerSheet().style("active-navbar-button") { - control(UIAlignment.CENTER) - size(206, 40) - background(activeNavbarButtonColor, radiusOf(9f)) - margin { marginBottom = px(8) } - } - - val hoveredNavbarButtonStyle = UIContainerSheet().style("hovered-navbar-button") { - background(colorOf(41, 41, 41), radiusOf(9f)) - } - - // Navbar list - val navList = list(UIListLayout.ListDirection.Vertical) { - val layout = UIAutoLayout(UIListLayout.ListDirection.Horizontal).apply { - componentAlignment = UIAlignment.MIDDLELEFT - componentSpacing = px(24) - layoutPadding = paddingOf(8, 9, 8, 9) - } - - // Navbar section style - UIStyleSheet().style("navbar-title") { - x = px(24) - font("Montserrat", px(11), colorOf(191, 189, 193), top or left) - margin { - marginTop = descender(1f) - marginBottom = px(8) - } - } - - animationOf("navbar-button", UIContainerSheet()) { - keyframe(UIQuart(1000L), navbarButtonStyle) - } - -// animationOf("active-navbar-button", UIContainerSheet()) { -// kf {} -// keyframe(UIQuart(1000L), activeNavbarButtonStyle.copy()) -// } - - animationOf("hovered-navbar-button", UIContainerSheet()) { - keyframe(UIQuart(1000L), hoveredNavbarButtonStyle) - } - - // Navbar button styles - UIImageSheet().style("navbar-icon") { - size(24, 24) - } - - UIStyleSheet().style("navbar-text") { - font("Montserrat", px(14), colorOf(-1), left or top) - } - - selectable { - label("MENU", "navbar-title") - navButton(layout, "Dashboard", "gradient/home") - navButton(layout, "Mods", "gradient/folder") - navButton(layout, "Settings", "gradient/setting") - navButton(layout, "Store", "gradient/bag") - navButton(layout, "Profiles", "gradient/profile") - - label("SOCIAL", "navbar-title") - navButton(layout, "Messages", "gradient/mail") - navButton(layout, "Friends", "gradient/people") - navButton(layout, "Achievements", "gradient/medal") - navButton(layout, "Recordings", "gradient/video") - - onSelection { - UIProvider.dispatchAnimation("active-navbar-button", it) - //it.applyStyle("active-navbar-button") - val image = (it.components[0] as UIImage) - image.image = "solid/" + image.image.substring(image.image.indexOf('/') + 1) - } - - onDeselection { - UIProvider.dispatchAnimation("navbar-button", it) - val image = (it.components[0] as UIImage) - image.image = "gradient/" + image.image.substring(image.image.indexOf('/') + 1) - it.applyStyle("navbar-button") - } - - this.components.forEach { component -> - component.onMousePressed { - println("This was selected: ${((it as UIAutoLayout).components[1] as UILabel).text}") - //selectComponent(component) - } + UIFontFamily( + "Montserrat", + "/prism/fonts/montserrat/", + "Montserrat-regular", + "Montserrat-medium", + "Montserrat-black", + "Montserrat-bold", + "Montserrat-light", + "Montserrat-thin" + ) - component.onMouseEnter { - if (!isSelected(it)) { - UIProvider.dispatchAnimation("hovered-navbar-button", it) - //it.applyStyle("hovered-navbar-button") - } - } - - component.onMouseLeave { - if (!isSelected(it)) { - UIProvider.dispatchAnimation("navbar-button", it) - //it.applyStyle("navbar-button") - } - } - } - - // TODO: Style does not get initialized before this call. - //selectComponent(0) - } - - }.style("navbar-list") { - y = px(118) - size(rel(1), rel(1) - px(118 + 235)) // 118 = top area, 235 = bottom area - verticalScrollbar { - x = rel(1) - px(10) - y = rel(0.1) - width = px(5) - height = rel(0.8) - radius = radiusOf(2.5f) - color = colorOf(207, 207, 207) - background { - backgroundColor = colorOf(1f, 1f, 1f, 0.3f) - radius = radiusOf(2.5) - } - } - clipContent = true - } - - // Footer - val footer = autoLayout(UIListLayout.ListDirection.Vertical) { - verticalResizing = UIAutoLayout.ResizingMode.Hug - horizontalResizing = UIAutoLayout.ResizingMode.Hug - componentAlignment = UIAlignment.CENTER - componentSpacing = px(6) - - // Promotion - // TODO: Promotion component - - val editHud = autoLayout(UIListLayout.ListDirection.Horizontal) { - image("outline/ui", "navbar-icon").style { - imageColor = colorOf(87, 164, 255) - } - text("Edit HUD", "navbar-text").style { - font { - fontColor = colorOf(-1) //colorOf(87, 164, 255) - } - } - - componentAlignment = UIAlignment.CENTER - componentSpacing = px(20) - layoutPadding = paddingOf(15, 37, 15, 37) - - verticalResizing = UIAutoLayout.ResizingMode.Hug - horizontalResizing = UIAutoLayout.ResizingMode.Hug - }.style("edit-hud-layout") { - control(UIAlignment.BOTTOMCENTER) - background(colorOf(36, 37, 37), radiusOf(15)) - } - - }.style("navbar-footer-layout") { - control(UIAlignment.BOTTOMCENTER) - y -= px(16 + 8) - } - - label("Running Prism Client v1.0.0-Beta").style("nav-prism-version") { - font("Montserrat", px(8), colorOf(1f, 1f, 1f, 0.8f), left or top, UIFont.FontType.Light) - control(UIAlignment.BOTTOMCENTER) - y -= px(8) - } - }.style("navbar-container") { - size(px(236), rel(1)) - background(colorOf(32, 32, 32)) - useFBO = true - } - - // Navbar animation - animationOf("navbar-enter", UIContainerSheet()) { - kf { - x = px(-236) - } - UIQuart(1000L) to { - x = px(0) - } - } - - animationOf("navbar-exit", UIContainerSheet()) { - kf { - x = px(0) - } - UIQuart(500L) to { - x = px(-236) - } - } - - UIProvider.dispatchAnimation("navbar-enter", navbar) - } - } + create { + button("Hello World! I really like trains. Also this text breaks at EXACTLY 500 pixels! It has a line spacing of 10 pixels!!! Don't forget, the initial size is (500x100)! The text will increase the height if it's too small! (Hint: it is too small!! jk its not its 600)") { - /** - * Creates a navbar button - */ - private fun navButton(layout: UIAutoLayout, buttonText: String, imageName: String): UIAutoLayout { - ucreate { - return autoLayout(layout) { - image(imageName, "navbar-icon") - text(buttonText, "navbar-text") }.style { - control(UIAlignment.CENTER) - size(206, 40) - margin { marginBottom = px(8) } + position(20,20) + font { + width = px(500) + height = px(600) + fontName = "Montserrat-light" + fontColor = colorOf(-1) + fontSize = px(24) + lineHeightSpacing = px(10) + verticalAlignment = TextAlignment.LEFT + horizontalAlignment = TextAlignment.CENTER + textResizing = UIFont.TextResizing.FixedSize + } } } - return throw RuntimeException() } } \ No newline at end of file diff --git a/src/test/kotlin/examples/deps/Generic.kt b/src/test/kotlin/examples/deps/Generic.kt index 831b3ed..7055220 100644 --- a/src/test/kotlin/examples/deps/Generic.kt +++ b/src/test/kotlin/examples/deps/Generic.kt @@ -56,13 +56,13 @@ class Generic : UIDependable { // FontSize -> 16f // FontColor -> -1 = asRGBA(255, 255, 255) (aka white) // TextAlignment -> How the text aligns relative to the screen using bit shifting - font("Montserrat", px(16f), colorOf(-1), left or top) - - // The above was a shorthand for - font { - fontFamily = "Montserrat" - // etc... - } +// font("Montserrat", px(16f), colorOf(-1), left or top) +// +// // The above was a shorthand for +// font { +// fontFamily = "Montserrat" +// // etc... +// } } } } \ No newline at end of file From d4411ebb74bf27ef3184f7fba66ad232691ec7c3 Mon Sep 17 00:00:00 2001 From: sen Date: Sun, 17 Jul 2022 13:01:53 -0700 Subject: [PATCH 28/37] Shorthands --- .../aether/ui/renderer/impl/font/UIFont.kt | 19 +++++ .../aether/ui/style/UIStyleSheet.kt | 73 ++++++++----------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt index 21f3b68..fb7eaea 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt @@ -9,6 +9,7 @@ import net.prismclient.aether.ui.unit.UIUnit import net.prismclient.aether.ui.util.UIColor import net.prismclient.aether.ui.util.extensions.asRGBA import net.prismclient.aether.ui.util.extensions.calculate +import net.prismclient.aether.ui.util.extensions.px import net.prismclient.aether.ui.util.extensions.renderer import net.prismclient.aether.ui.util.middle import java.util.regex.Pattern @@ -78,6 +79,24 @@ open class UIFont : UIShape() { var cachedLineHeightSpacing: Float = 0f protected set + // -- Shorthands -- // + + fun size(width: UIUnit?, height: UIUnit?) { + this.width = width + this.height = height + } + + fun size(width: Number, height: Number) = size(px(width), px(height)) + + fun align(horizontal: TextAlignment, vertical: TextAlignment) = alignment(horizontal, vertical) + + fun alignment(horizontal: TextAlignment, vertical: TextAlignment) { + horizontalAlignment = horizontal + verticalAlignment = vertical + } + + // -- Core -- // + override fun update(component: UIComponent<*>?) { super.update(component) updateFont() diff --git a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt index acd4fd6..36fa94d 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt @@ -6,12 +6,14 @@ import net.prismclient.aether.ui.component.type.layout.UIFrameSheet import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.component.util.enums.UIAlignment.* import net.prismclient.aether.ui.renderer.impl.background.UIBackground +import net.prismclient.aether.ui.renderer.impl.font.TextAlignment import net.prismclient.aether.ui.renderer.impl.font.UIFont import net.prismclient.aether.ui.renderer.impl.property.UIMargin import net.prismclient.aether.ui.renderer.impl.property.UIPadding import net.prismclient.aether.ui.renderer.impl.property.UIRadius import net.prismclient.aether.ui.style.util.UIAnchorPoint import net.prismclient.aether.ui.unit.UIUnit +import net.prismclient.aether.ui.util.Block import net.prismclient.aether.ui.util.UIColor import net.prismclient.aether.ui.util.extensions.RELATIVE import net.prismclient.aether.ui.util.extensions.lerp @@ -133,7 +135,7 @@ open class UIStyleSheet() : UICopy, UIAnimatable { anchor?.save(animation, keyframe?.anchor) } - /** Shorthands **/ + // -- Control Shorthands -- // /** * Shorthand for setting the position and size as pixels. @@ -159,7 +161,7 @@ open class UIStyleSheet() : UICopy, UIAnimatable { /** * Positions the component at the given [x], and [y] values as the given units. */ - fun position(x: UIUnit, y: UIUnit) { + fun position(x: UIUnit?, y: UIUnit?) { this.x = x this.y = y } @@ -232,59 +234,44 @@ open class UIStyleSheet() : UICopy, UIAnimatable { } } - /** Background **/ + // -- Background Shorthands -- // - /** - * Creates a background DSL block. If background is null, an instance of it is created - */ - inline fun background(block: UIBackground.() -> Unit) { + inline fun background(block: Block) { background = background ?: UIBackground() background!!.block() } - /** - * Sets the color of the background - */ - @JvmOverloads - inline fun background(color: UIColor, radius: UIRadius? = background?.radius, block: UIBackground.() -> Unit = {}) = + inline fun background(color: UIColor, radius: UIRadius? = background?.radius, block: Block = {}) = background { this.backgroundColor = color; this.radius = radius; this.block() } - /** Font **/ + // -- Font Shorthands -- // - /** - * Creates a font DSL block. If font is null, an instance of it is created - */ - inline fun font(block: UIFont.() -> Unit) { + inline fun font(block: Block) { font = font ?: UIFont() font!!.block() } - /** - * Creates a font DSL block which optionally accepts a size, color, text alignment, font family, and font type. - */ -// @JvmOverloads -// inline fun font( -// fontFamily: String = font?.fontFamily ?: "", -// fontSize: UIUnit? = font?.fontSize, -// fontColor: UIColor? = font?.fontColor, -// textAlignment: Int = font?.textAlignment ?: 0, -// fontType: UIFont.FontType? = font?.fontType, -// block: UIFont.() -> Unit = {} -// ) = font { -// this.fontSize = fontSize -// this.fontColor = fontColor -// this.textAlignment = textAlignment -// this.fontFamily = fontFamily -// this.fontType = fontType -// this.block() -// } - - /** Plotting **/ + inline fun font(fontName: String, fontSize: UIUnit?, fontColor: UIColor, block: Block = {}) = font { + this.fontName = fontName + this.fontSize = fontSize + this.fontColor = fontColor + this.block() + } - /** - * Creates a padding DSL block. If padding is null, an instance of it is created - */ - inline fun padding(block: UIPadding.() -> Unit) { + inline fun font(verticalAlignment: TextAlignment, horizontalAlignment: TextAlignment, block: Block) = font { + this.verticalAlignment = verticalAlignment + this.horizontalAlignment = horizontalAlignment + this.block() + } + + inline fun fontSize(width: UIUnit, height: UIUnit, block: Block) = font { + this.size(width, height) + this.block() + } + + // -- Padding and Margin Shorthands -- // + + inline fun padding(block: Block) { padding = padding ?: UIPadding() padding!!.block() } @@ -322,7 +309,7 @@ open class UIStyleSheet() : UICopy, UIAnimatable { /** * Creates a margin DSL block. If margin is null, an instance of it is created */ - inline fun margin(block: UIMargin.() -> Unit) { + inline fun margin(block: Block) { margin = margin ?: UIMargin() margin!!.block() } From 4b7721913f8e539dda0ada8d2c2a27f0f7cb6666 Mon Sep 17 00:00:00 2001 From: sen Date: Sun, 17 Jul 2022 16:55:09 -0700 Subject: [PATCH 29/37] Shorthands + bug fix --- .../aether/ui/renderer/impl/font/UIFont.kt | 61 +++++++++++-------- .../prismclient/aether/ui/shape/UIShape.kt | 19 ++++++ .../aether/ui/style/UIStyleSheet.kt | 19 +----- .../net/prismclient/aether/ui/util/Util.kt | 26 +++++++- src/test/kotlin/examples/Default.kt | 16 ++--- 5 files changed, 89 insertions(+), 52 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt index fb7eaea..1ff39d8 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt @@ -1,10 +1,12 @@ package net.prismclient.aether.ui.renderer.impl.font import net.prismclient.aether.ui.component.UIComponent +import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.dsl.UIRendererDSL import net.prismclient.aether.ui.renderer.impl.background.UIBackground import net.prismclient.aether.ui.renderer.impl.font.TextAlignment.* import net.prismclient.aether.ui.shape.UIShape +import net.prismclient.aether.ui.style.util.UIAnchorPoint import net.prismclient.aether.ui.unit.UIUnit import net.prismclient.aether.ui.util.UIColor import net.prismclient.aether.ui.util.extensions.asRGBA @@ -35,7 +37,9 @@ open class UIFont : UIShape() { updateFont() } - var fontName: String = "" + var anchor: UIAnchorPoint? = null + + var fontName: String? = null var fontSize: UIUnit? = null var fontColor: UIColor? = null var fontSpacing: UIUnit? = null @@ -66,7 +70,10 @@ open class UIFont : UIShape() { */ var text: ArrayList = arrayListOf() - var textBounds: FloatArray = floatArrayOf() + /** + * The bounds of the font, at [0, 0] + */ + lateinit var textBounds: FloatArray protected set var cachedFontSize: Float = 0f protected set @@ -79,14 +86,12 @@ open class UIFont : UIShape() { var cachedLineHeightSpacing: Float = 0f protected set - // -- Shorthands -- // - - fun size(width: UIUnit?, height: UIUnit?) { - this.width = width - this.height = height - } + var cachedAnchorX: Float = 0f + protected set + var cachedAnchorY: Float = 0f + protected set - fun size(width: Number, height: Number) = size(px(width), px(height)) + // -- Shorthands -- // fun align(horizontal: TextAlignment, vertical: TextAlignment) = alignment(horizontal, vertical) @@ -95,17 +100,18 @@ open class UIFont : UIShape() { verticalAlignment = vertical } - // -- Core -- // - - override fun update(component: UIComponent<*>?) { - super.update(component) - updateFont() + fun anchor(alignment: UIAlignment) { + anchor = anchor ?: UIAnchorPoint() + anchor!!.align(alignment) } + // -- Core -- // + /** * Calculates and updates the bounds for this. */ fun updateFont() { + update(component) val component = component ?: throw RuntimeException("Component cannot be null for UIFont") text.clear() @@ -115,7 +121,7 @@ open class UIFont : UIShape() { cachedLineHeightSpacing = calculate(lineHeightSpacing, component, cachedWidth, cachedHeight, true) renderer { - font(fontName, cachedFontSize, horizontalAlignment, verticalAlignment, cachedFontSpacing) + font(fontName ?: "", cachedFontSize, horizontalAlignment, verticalAlignment, cachedFontSpacing) when (textResizing) { TextResizing.AutoWidth -> { if (actualText.contains(NEWLINE)) { @@ -148,7 +154,12 @@ open class UIFont : UIShape() { cachedHeight = maxy - miny } TextResizing.FixedSize -> { - renderer.calculateText(actualText, 0f, 0f, cachedWidth, cachedLineHeightSpacing, text) + if (actualText.contains(NEWLINE)) { + text.addAll(actualText.split(NEWLINE)) + renderer.calculateText(text, 0f, 0f, cachedLineHeightSpacing) + } else { + renderer.calculateText(actualText, 0f, 0f, cachedWidth, cachedLineHeightSpacing, text) + } val (minx, miny, maxx, maxy) = fontBounds() @@ -159,16 +170,16 @@ open class UIFont : UIShape() { } // Update properties + textBounds = fontBounds() fontAscender = fontAscender() fontDescender = fontDescender() } + updateAnchor() } - /** - * Splits the active text into [activeText] - */ - fun splitText() { - + fun updateAnchor() { + cachedAnchorX = calculate(anchor?.x, null, cachedWidth, cachedHeight, false) + cachedAnchorY = calculate(anchor?.y, null, cachedWidth, cachedHeight, true) } override fun render() { @@ -176,15 +187,15 @@ open class UIFont : UIShape() { CENTER -> cachedWidth / 2f RIGHT -> cachedWidth else -> 0f - } + } - cachedAnchorX val y = cachedY + when (horizontalAlignment) { CENTER -> cachedFontSize / 2f + (cachedHeight - (cachedFontSize * text.size) - (cachedLineHeightSpacing * (text.size - 1))) / 2f BOTTOM -> cachedFontSize + (cachedHeight - (cachedFontSize * text.size) - (cachedLineHeightSpacing * (text.size - 1))) else -> 0f - } + } - cachedAnchorY renderer { color(fontColor) - font(fontName, cachedFontSize, verticalAlignment, horizontalAlignment, cachedFontSpacing) + font(fontName ?: "", cachedFontSize, verticalAlignment, horizontalAlignment, cachedFontSpacing) when (textResizing) { TextResizing.AutoWidth -> actualText.render(x, y) TextResizing.AutoHeight -> renderer.renderText(text, x, y, cachedLineHeightSpacing) @@ -192,7 +203,7 @@ open class UIFont : UIShape() { TextResizing.TruncateText -> {} // TODO: Truncate text } color(asRGBA(1f, 0f, 0f, 0.3f)) - rect(cachedX, cachedY, cachedWidth, cachedHeight) + rect(cachedX - cachedAnchorX, cachedY - cachedAnchorY, cachedWidth, cachedHeight) } } diff --git a/src/main/kotlin/net/prismclient/aether/ui/shape/UIShape.kt b/src/main/kotlin/net/prismclient/aether/ui/shape/UIShape.kt index b6e9deb..16e39e2 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/shape/UIShape.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/shape/UIShape.kt @@ -5,6 +5,7 @@ import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.unit.UIUnit import net.prismclient.aether.ui.util.extensions.calculate import net.prismclient.aether.ui.util.extensions.lerp +import net.prismclient.aether.ui.util.extensions.px import net.prismclient.aether.ui.util.interfaces.UIAnimatable import net.prismclient.aether.ui.util.interfaces.UICopy @@ -31,6 +32,24 @@ abstract class UIShape> : UIObject(), UICopy, UIAnimatable var cachedHeight = 0f protected set + // -- Shorthands -- // + + fun position(x: UIUnit?, y: UIUnit?) { + this.x = x + this.y = y + } + + fun position(x: Number, y: Number) = position(px(x), px(y)) + + fun size(width: UIUnit?, height: UIUnit?) { + this.width = width + this.height = height + } + + fun size(width: Number, height: Number) = size(px(width), px(height)) + + // -- Core -- // + override fun update(component: UIComponent<*>?) { this.component = component cachedX = (component?.x ?: 0f) + calculate(x, component, component?.width ?: 0f, component?.height ?: 0f, false) diff --git a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt index 36fa94d..e6c4355 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt @@ -214,24 +214,7 @@ open class UIStyleSheet() : UICopy, UIAnimatable { */ fun anchor(alignment: UIAlignment) { anchor = anchor ?: UIAnchorPoint() - anchor!!.x = anchor!!.x ?: px(0) - anchor!!.y = anchor!!.y ?: px(0) - anchor!!.x!!.type = RELATIVE - anchor!!.y!!.type = RELATIVE - - anchor!!.x!!.value = when (alignment) { - TOPLEFT, MIDDLELEFT, BOTTOMLEFT -> 0f - TOPCENTER, CENTER, BOTTOMCENTER -> 0.5f - TOPRIGHT, MIDDLERIGHT, BOTTOMRIGHT -> 1f - else -> throw UnsupportedOperationException("Unknown alignment type: $alignment") - } - - anchor!!.y!!.value = when (alignment) { - TOPLEFT, TOPCENTER, TOPRIGHT -> 0f - MIDDLELEFT, CENTER, MIDDLERIGHT -> 0.5f - BOTTOMLEFT, BOTTOMCENTER, BOTTOMRIGHT -> 1f - else -> throw UnsupportedOperationException("Unknown alignment type: $alignment") - } + anchor!!.align(alignment) } // -- Background Shorthands -- // diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt b/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt index 33477f8..f415058 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/Util.kt @@ -1,6 +1,11 @@ package net.prismclient.aether.ui.util +import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.style.UIStyleSheet +import net.prismclient.aether.ui.style.util.UIAnchorPoint +import net.prismclient.aether.ui.util.extensions.RELATIVE +import net.prismclient.aether.ui.util.extensions.px +import net.prismclient.aether.ui.util.extensions.rel /** * Type alias for a function which has a receiver of [T] and accepts, and returns @@ -28,4 +33,23 @@ fun FloatArray.green(): Float = this[1] fun FloatArray.blue(): Float = this[2] fun FloatArray.alpha(): Float = this[3] -fun FloatArray.advance(): Float = this[4] \ No newline at end of file +fun FloatArray.advance(): Float = this[4] + +fun UIAnchorPoint.anchor(alignment: UIAlignment) { + x = x ?: rel(0) + y = y ?: rel(0) + + x!!.value = when (alignment) { + UIAlignment.TOPLEFT, UIAlignment.MIDDLELEFT, UIAlignment.BOTTOMLEFT -> 0f + UIAlignment.TOPCENTER, UIAlignment.CENTER, UIAlignment.BOTTOMCENTER -> 0.5f + UIAlignment.TOPRIGHT, UIAlignment.MIDDLERIGHT, UIAlignment.BOTTOMRIGHT -> 1f + else -> throw UnsupportedOperationException("Unknown alignment type: $alignment") + } + + y!!.value = when (alignment) { + UIAlignment.TOPLEFT, UIAlignment.TOPCENTER, UIAlignment.TOPRIGHT -> 0f + UIAlignment.MIDDLELEFT, UIAlignment.CENTER, UIAlignment.MIDDLERIGHT -> 0.5f + UIAlignment.BOTTOMLEFT, UIAlignment.BOTTOMCENTER, UIAlignment.BOTTOMRIGHT -> 1f + else -> throw UnsupportedOperationException("Unknown alignment type: $alignment") + } +} \ No newline at end of file diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index 49640fa..0c1bc5b 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -6,8 +6,7 @@ import net.prismclient.aether.ui.renderer.impl.font.UIFont import net.prismclient.aether.ui.screen.UIScreen import net.prismclient.aether.ui.style.util.UIFontFamily import net.prismclient.aether.ui.util.create -import net.prismclient.aether.ui.util.extensions.colorOf -import net.prismclient.aether.ui.util.extensions.px +import net.prismclient.aether.ui.util.extensions.* import net.prismclient.aether.ui.util.left import net.prismclient.aether.ui.util.style @@ -25,20 +24,21 @@ class Default : UIScreen { ) create { - button("Hello World! I really like trains. Also this text breaks at EXACTLY 500 pixels! It has a line spacing of 10 pixels!!! Don't forget, the initial size is (500x100)! The text will increase the height if it's too small! (Hint: it is too small!! jk its not its 600)") { + button("Hello\nWorld!") { }.style { position(20,20) + size(400, 140) + background(colorOf(asRGBA(0f, 0f, 0f, 0.3f))) font { - width = px(500) - height = px(600) + anchor(UIAlignment.CENTER) + x = rel(0.5) + y = rel(0.5) + size(rel(1), rel(1)) fontName = "Montserrat-light" fontColor = colorOf(-1) fontSize = px(24) lineHeightSpacing = px(10) - verticalAlignment = TextAlignment.LEFT - horizontalAlignment = TextAlignment.CENTER - textResizing = UIFont.TextResizing.FixedSize } } } From 06d80e36e661b5bfbb00e19dbd50de99182f03fa Mon Sep 17 00:00:00 2001 From: sen Date: Sun, 17 Jul 2022 18:53:32 -0700 Subject: [PATCH 30/37] Many shorthands + bug fixed Fixed framecolor issue Fixed image color issue Added tons of shothands --- .../aether/ui/component/type/UILabel.kt | 17 ++- .../ui/component/type/layout/UIContainer.kt | 11 +- .../ui/component/type/layout/UIFrame.kt | 17 +-- .../aether/ui/dsl/UIComponentDSL.kt | 3 +- .../aether/ui/dsl/UIRendererDSL.kt | 4 +- .../aether/ui/renderer/UIRenderer.kt | 6 +- .../aether/ui/renderer/impl/font/UIFont.kt | 31 ++--- .../{TextAlignment.kt => UITextAlignment.kt} | 2 +- .../aether/ui/style/UIStyleSheet.kt | 119 ++++++++++-------- .../prismclient/aether/ui/util/Shorthands.kt | 9 -- src/test/kotlin/Renderer.kt | 27 ++-- src/test/kotlin/examples/Default.kt | 41 +++--- src/test/kotlin/examples/deps/Generic.kt | 5 - 13 files changed, 160 insertions(+), 132 deletions(-) rename src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/{TextAlignment.kt => UITextAlignment.kt} (95%) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt index d5a13e5..02e5044 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt @@ -1,10 +1,12 @@ package net.prismclient.aether.ui.component.type import net.prismclient.aether.ui.component.UIComponent +import net.prismclient.aether.ui.component.type.input.button.UIButton import net.prismclient.aether.ui.style.UIStyleSheet /** - * [UILabel] is a component which draws a label, or string on screen. + * [UILabel] acts nearly identical to [UIButton] however, the size of the label is automatically + * updated to be at least the size of the font within it. * * @author sen * @since 1.0 @@ -16,6 +18,19 @@ class UILabel(text: String) : UIComponent() { style.font?.actualText = text } + override fun update() { + super.update() + style.font?.actualText = text + width = width.coerceAtLeast(style.font?.cachedWidth ?: 0f) + height = height.coerceAtLeast(style.font?.cachedHeight ?: 0f) + updateAnchorPoint() + updatePosition() + updateBounds() + updateStyle() + style.font?.updateFont() + } + + override fun renderComponent() { style.font?.render() } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt index b14eaba..70147de 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt @@ -90,15 +90,14 @@ open class UIContainer : UIFrame(), UIFocusable, UILayo } override fun renderComponent() { - if (style.useFBO) { - renderer { + renderer { + if (style.useFBO) { + color(-1) path { imagePattern(fbo!!.imagePattern, relX, relY, relWidth, relHeight, 0f, 1f) - rect(relX, relY, relWidth, relHeight) + rect(relX, relY, relWidth, relHeight, style.background?.radius) }.fillPaint() - } - } else { - renderer { + } else { if (style.clipContent) { scissor(relX, relY, relWidth, relHeight) { translate( diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt index 5fb02d4..7eccfe6 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt @@ -114,18 +114,19 @@ abstract class UIFrame : UIComponent(), UIFocusable { } override fun renderComponent() { - if (style.useFBO) { - renderer { + renderer { + if (style.useFBO) { + color(-1) path { imagePattern(fbo!!.imagePattern, relX, relY, relWidth, relHeight, 0f, 1f) - rect(relX, relY, relWidth, relHeight) + rect(relX, relY, relWidth, relHeight, style.background?.radius) }.fillPaint() + } else { + if (style.clipContent) scissor(relX, relY, relWidth, relHeight) { + components.forEach(UIComponent<*>::render) + } + else components.forEach(UIComponent<*>::render) } - } else { - if (style.clipContent) UIRendererDSL.scissor(relX, relY, relWidth, relHeight) { - components.forEach(UIComponent<*>::render) - } - else components.forEach(UIComponent<*>::render) } } diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt index ded9ef4..a6d6fc9 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt @@ -135,7 +135,8 @@ object UIComponentDSL { * * @return T The component */ - inline fun > component(component: T, style: String?, block: Block): T { + @JvmOverloads + inline fun > component(component: T, style: String? = null, block: Block = {}): T { pushComponent(component) component.applyStyle(style) component.block() diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt index 38b6ba9..67efc0e 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIRendererDSL.kt @@ -5,7 +5,7 @@ import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.UIRenderer import net.prismclient.aether.ui.renderer.impl.border.UIStrokeDirection -import net.prismclient.aether.ui.renderer.impl.font.TextAlignment +import net.prismclient.aether.ui.renderer.impl.font.UITextAlignment import net.prismclient.aether.ui.renderer.impl.property.UIRadius import net.prismclient.aether.ui.renderer.other.UIContentFBO import net.prismclient.aether.ui.util.* @@ -92,7 +92,7 @@ object UIRendererDSL { * Applies the given font values to the active context. */ @JvmStatic - fun font(fontFace: String, fontSize: Float, horizontalAlignment: TextAlignment, verticalAlignment: TextAlignment, fontSpacing: Float) { + fun font(fontFace: String, fontSize: Float, horizontalAlignment: UITextAlignment, verticalAlignment: UITextAlignment, fontSpacing: Float) { renderer.fontFace(fontFace) renderer.fontSize(fontSize) renderer.fontAlignment(horizontalAlignment, verticalAlignment) diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt index a989fdd..1b09c7a 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/UIRenderer.kt @@ -1,7 +1,7 @@ package net.prismclient.aether.ui.renderer import net.prismclient.aether.ui.renderer.image.UIImageData -import net.prismclient.aether.ui.renderer.impl.font.TextAlignment +import net.prismclient.aether.ui.renderer.impl.font.UITextAlignment import net.prismclient.aether.ui.renderer.other.UIContentFBO import java.nio.ByteBuffer @@ -192,9 +192,9 @@ interface UIRenderer { fun fontSpacing(spacing: Float) /** - * Aligns the text to the given [horizontalAlignment] and [verticalAlignment] based on the enum [TextAlignment]. + * Aligns the text to the given [horizontalAlignment] and [verticalAlignment] based on the enum [UITextAlignment]. */ - fun fontAlignment(horizontalAlignment: TextAlignment, verticalAlignment: TextAlignment) + fun fontAlignment(horizontalAlignment: UITextAlignment, verticalAlignment: UITextAlignment) /** * Renders the [text] based on the [x], and [y] position and all the states diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt index 1ff39d8..480a86c 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt @@ -1,19 +1,13 @@ package net.prismclient.aether.ui.renderer.impl.font -import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.component.util.enums.UIAlignment -import net.prismclient.aether.ui.dsl.UIRendererDSL -import net.prismclient.aether.ui.renderer.impl.background.UIBackground -import net.prismclient.aether.ui.renderer.impl.font.TextAlignment.* +import net.prismclient.aether.ui.renderer.impl.font.UITextAlignment.* import net.prismclient.aether.ui.shape.UIShape import net.prismclient.aether.ui.style.util.UIAnchorPoint import net.prismclient.aether.ui.unit.UIUnit import net.prismclient.aether.ui.util.UIColor -import net.prismclient.aether.ui.util.extensions.asRGBA import net.prismclient.aether.ui.util.extensions.calculate -import net.prismclient.aether.ui.util.extensions.px import net.prismclient.aether.ui.util.extensions.renderer -import net.prismclient.aether.ui.util.middle import java.util.regex.Pattern /** @@ -46,8 +40,8 @@ open class UIFont : UIShape() { var textResizing: TextResizing = TextResizing.AutoWidth - var horizontalAlignment: TextAlignment = CENTER - var verticalAlignment: TextAlignment = CENTER + var horizontalAlignment: UITextAlignment = CENTER + var verticalAlignment: UITextAlignment = CENTER /** * The spacing between each line break. @@ -93,9 +87,9 @@ open class UIFont : UIShape() { // -- Shorthands -- // - fun align(horizontal: TextAlignment, vertical: TextAlignment) = alignment(horizontal, vertical) + fun align(horizontal: UITextAlignment, vertical: UITextAlignment) = alignment(horizontal, vertical) - fun alignment(horizontal: TextAlignment, vertical: TextAlignment) { + fun alignment(horizontal: UITextAlignment, vertical: UITextAlignment) { horizontalAlignment = horizontal verticalAlignment = vertical } @@ -167,6 +161,11 @@ open class UIFont : UIShape() { cachedHeight = (maxy - miny).coerceAtLeast(cachedHeight) } TextResizing.TruncateText -> {} + TextResizing.Auto -> { + textResizing = TextResizing.AutoWidth + updateFont() + return + } } // Update properties @@ -201,9 +200,8 @@ open class UIFont : UIShape() { TextResizing.AutoHeight -> renderer.renderText(text, x, y, cachedLineHeightSpacing) TextResizing.FixedSize -> renderer.renderText(actualText, x, y, cachedWidth, cachedLineHeightSpacing, null) TextResizing.TruncateText -> {} // TODO: Truncate text + TextResizing.Auto -> throw RuntimeException("Auto should not be the text resizing mode when rendering???") } - color(asRGBA(1f, 0f, 0f, 0.3f)) - rect(cachedX - cachedAnchorX, cachedY - cachedAnchorY, cachedWidth, cachedHeight) } } @@ -248,7 +246,12 @@ open class UIFont : UIShape() { /** * Cuts the text off at the point where it exceeds the width of this, and append the string [UIFont.truncatedText] */ - TruncateText + TruncateText, + + /** + * Automatically figures out which one is the best based on the properties of the font. + */ + Auto } companion object { diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/TextAlignment.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UITextAlignment.kt similarity index 95% rename from src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/TextAlignment.kt rename to src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UITextAlignment.kt index b3f125c..092f1b0 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/TextAlignment.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UITextAlignment.kt @@ -7,7 +7,7 @@ package net.prismclient.aether.ui.renderer.impl.font * @author sen * @since 1.3 */ -enum class TextAlignment { +enum class UITextAlignment { /** * Aligns to the left on the x-axis */ diff --git a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt index e6c4355..0ba34e5 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt @@ -6,7 +6,7 @@ import net.prismclient.aether.ui.component.type.layout.UIFrameSheet import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.component.util.enums.UIAlignment.* import net.prismclient.aether.ui.renderer.impl.background.UIBackground -import net.prismclient.aether.ui.renderer.impl.font.TextAlignment +import net.prismclient.aether.ui.renderer.impl.font.UITextAlignment import net.prismclient.aether.ui.renderer.impl.font.UIFont import net.prismclient.aether.ui.renderer.impl.property.UIMargin import net.prismclient.aether.ui.renderer.impl.property.UIPadding @@ -15,13 +15,11 @@ import net.prismclient.aether.ui.style.util.UIAnchorPoint import net.prismclient.aether.ui.unit.UIUnit import net.prismclient.aether.ui.util.Block import net.prismclient.aether.ui.util.UIColor -import net.prismclient.aether.ui.util.extensions.RELATIVE -import net.prismclient.aether.ui.util.extensions.lerp -import net.prismclient.aether.ui.util.extensions.px -import net.prismclient.aether.ui.util.extensions.rel +import net.prismclient.aether.ui.util.extensions.* import net.prismclient.aether.ui.util.interfaces.UIAnimatable import net.prismclient.aether.ui.util.interfaces.UICopy import net.prismclient.aether.ui.util.name +import net.prismclient.aether.ui.util.radiusOf /** * [UIStyleSheet] is the superclass of all styles. It holds the general @@ -46,7 +44,7 @@ import net.prismclient.aether.ui.util.name * @see Styles * @see How to create styles */ -open class UIStyleSheet() : UICopy, UIAnimatable { +open class UIStyleSheet : UICopy, UIAnimatable { var name: String = "" /** @@ -137,81 +135,55 @@ open class UIStyleSheet() : UICopy, UIAnimatable { // -- Control Shorthands -- // - /** - * Shorthand for setting the position and size as pixels. - */ + fun plot(controlAlignment: UIAlignment, width: Number, height: Number) { + control(controlAlignment) + size(width, height) + } + + @JvmOverloads + fun plot(controlAlignment: UIAlignment, width: UIUnit? = null, height: UIUnit? = null) { + control(controlAlignment) + size(width, height) + } + fun plot(x: Number, y: Number, width: Number, height: Number) { position(x, y) size(width, height) } - /** - * Shorthand for setting the position and size as units. - */ - fun plot(x: UIUnit, y: UIUnit, width: UIUnit, height: UIUnit) { + fun plot(x: UIUnit?, y: UIUnit?, width: UIUnit?, height: UIUnit?) { position(x, y) size(width, height) } - /** - * Positions the component at the given [x], and [y] value as pixels. - */ fun position(x: Number, y: Number) = position(px(x), px(y)) - /** - * Positions the component at the given [x], and [y] values as the given units. - */ fun position(x: UIUnit?, y: UIUnit?) { this.x = x this.y = y } - /** - * Sizes the component with the given [width], and height as pixels. - */ fun size(width: Number, height: Number) = size(px(width), px(height)) - /** - * Sizes the component with the given [width], and height as the given units. - */ - fun size(width: UIUnit, height: UIUnit) { + fun size(width: UIUnit?, height: UIUnit?) { this.width = width this.height = height } - /** - * Aligns the component's position to the relative point of it's parent - * - * @see control Shorthand - */ fun align(alignment: UIAlignment) { x = x ?: px(0) y = y ?: px(0) - net.prismclient.aether.ui.util.extensions.align(alignment, x!!, y!!) + align(alignment, x!!, y!!) } - /** - * Shorthand for [align] and anchor. Both values are set to the [alignment] - * - * @see align - * @see anchor - */ + fun control(alignment: UIAlignment) = control(alignment, alignment) - /** - * Shorthand for [align] and [anchor] - * - * @see align - * @see anchor - */ fun control(alignment: UIAlignment, anchorAlignment: UIAlignment) { align(alignment) anchor(anchorAlignment) } - /** - * Anchors the component to the given [alignment]. - */ fun anchor(alignment: UIAlignment) { anchor = anchor ?: UIAnchorPoint() anchor!!.align(alignment) @@ -224,6 +196,12 @@ open class UIStyleSheet() : UICopy, UIAnimatable { background!!.block() } + inline fun background(color: Int, radius: Number = 0, block: Block = {}) = background { + this.backgroundColor = colorOf(color) + if (radius != 0 || this.radius != null) this.radius = radiusOf(radius) + block() + } + inline fun background(color: UIColor, radius: UIRadius? = background?.radius, block: Block = {}) = background { this.backgroundColor = color; this.radius = radius; this.block() } @@ -234,14 +212,57 @@ open class UIStyleSheet() : UICopy, UIAnimatable { font!!.block() } - inline fun font(fontName: String, fontSize: UIUnit?, fontColor: UIColor, block: Block = {}) = font { + inline fun font( + fontName: String, + fontColor: UIColor, + fontSize: UIUnit?, + lineHeightSpacing: UIUnit? = null, + block: Block = {} + ) = font { this.fontName = fontName this.fontSize = fontSize this.fontColor = fontColor + this.lineHeightSpacing = lineHeightSpacing + this.block() + } + + inline fun font( + alignment: UIAlignment, + x: UIUnit? = null, + y: UIUnit? = null, + width: UIUnit? = null, + height: UIUnit? = null, + block: Block = {} + ) = font { + this.anchor(alignment) + this.x = x + this.y = y + this.width = width + this.height = height + this.block() + } + + inline fun font( + x: UIUnit? = null, + y: UIUnit? = null, + width: UIUnit? = null, + height: UIUnit? = null, + block: Block = {} + ) = font { + this.x = x + this.y = y + this.width = width + this.height = height this.block() } - inline fun font(verticalAlignment: TextAlignment, horizontalAlignment: TextAlignment, block: Block) = font { + inline fun font( + verticalAlignment: UITextAlignment, + horizontalAlignment: UITextAlignment, + textResizing: UIFont.TextResizing = UIFont.TextResizing.Auto, + block: Block = {} + ) = font { + this.textResizing = textResizing this.verticalAlignment = verticalAlignment this.horizontalAlignment = horizontalAlignment this.block() diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt index 9c91118..81c02d5 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/Shorthands.kt @@ -13,15 +13,6 @@ import net.prismclient.aether.ui.unit.UIUnit import net.prismclient.aether.ui.util.extensions.px import net.prismclient.aether.ui.util.interfaces.UIDependable -/** Font Alignments **/ -const val left = 1 -const val center = 2 -const val right = 4 -const val top = 8 -const val middle = 16 -const val bottom = 32 -const val baseline = 64 - /** * Image flags */ diff --git a/src/test/kotlin/Renderer.kt b/src/test/kotlin/Renderer.kt index 0cdd08c..337f67b 100644 --- a/src/test/kotlin/Renderer.kt +++ b/src/test/kotlin/Renderer.kt @@ -2,9 +2,8 @@ import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.UIRenderer import net.prismclient.aether.ui.renderer.image.UIImageData -import net.prismclient.aether.ui.renderer.impl.font.TextAlignment +import net.prismclient.aether.ui.renderer.impl.font.UITextAlignment import net.prismclient.aether.ui.renderer.other.UIContentFBO -import net.prismclient.aether.ui.util.bottom import net.prismclient.aether.ui.util.extensions.getAlpha import net.prismclient.aether.ui.util.extensions.getBlue import net.prismclient.aether.ui.util.extensions.getGreen @@ -139,9 +138,9 @@ object Renderer : UIRenderer { return imageData } - override fun deleteImage(imageName: String) { - nvgDeleteImage(ctx, UIProvider.getImage(imageName)?.handle ?: return) - UIProvider.deleteImage(imageName) + override fun deleteImage(imageData: String) { + nvgDeleteImage(ctx, UIProvider.getImage(imageData)?.handle ?: return) + UIProvider.deleteImage(imageData) } override fun createSvg(svgName: String, data: ByteBuffer?, scale: Float): UIImageData { @@ -199,11 +198,11 @@ object Renderer : UIRenderer { imageHandle: Int, x: Float, y: Float, width: Float, height: Float, angle: Float, alpha: Float ) { allocPaint() - paint!!.innerColor(fillColor) - paint!!.outerColor(fillColor) nvgImagePattern( ctx, x, y, width, height, angle, imageHandle, alpha, paint!! ) + paint!!.innerColor(fillColor) + paint!!.outerColor(fillColor) } override fun fontFace(fontName: String) = nvgFontFace(ctx, fontName) @@ -212,17 +211,17 @@ object Renderer : UIRenderer { override fun fontSpacing(spacing: Float) = nvgTextLetterSpacing(ctx, spacing) - override fun fontAlignment(horizontalAlignment: TextAlignment, verticalAlignment: TextAlignment) { + override fun fontAlignment(horizontalAlignment: UITextAlignment, verticalAlignment: UITextAlignment) { var alignment = when (horizontalAlignment) { - TextAlignment.LEFT -> NVG_ALIGN_LEFT - TextAlignment.CENTER -> NVG_ALIGN_CENTER - TextAlignment.RIGHT -> NVG_ALIGN_RIGHT + UITextAlignment.LEFT -> NVG_ALIGN_LEFT + UITextAlignment.CENTER -> NVG_ALIGN_CENTER + UITextAlignment.RIGHT -> NVG_ALIGN_RIGHT else -> 0 } alignment = alignment or when (verticalAlignment) { - TextAlignment.TOP -> NVG_ALIGN_TOP - TextAlignment.CENTER -> NVG_ALIGN_MIDDLE - TextAlignment.BOTTOM -> NVG_ALIGN_BOTTOM + UITextAlignment.TOP -> NVG_ALIGN_TOP + UITextAlignment.CENTER -> NVG_ALIGN_MIDDLE + UITextAlignment.BOTTOM -> NVG_ALIGN_BOTTOM else -> 0 } nvgTextAlign(ctx, alignment) diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index 0c1bc5b..0bd816f 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -1,13 +1,14 @@ package examples import net.prismclient.aether.ui.component.util.enums.UIAlignment -import net.prismclient.aether.ui.renderer.impl.font.TextAlignment -import net.prismclient.aether.ui.renderer.impl.font.UIFont +import net.prismclient.aether.ui.renderer.impl.font.UITextAlignment import net.prismclient.aether.ui.screen.UIScreen import net.prismclient.aether.ui.style.util.UIFontFamily import net.prismclient.aether.ui.util.create -import net.prismclient.aether.ui.util.extensions.* -import net.prismclient.aether.ui.util.left +import net.prismclient.aether.ui.util.extensions.asRGBA +import net.prismclient.aether.ui.util.extensions.colorOf +import net.prismclient.aether.ui.util.extensions.px +import net.prismclient.aether.ui.util.extensions.rel import net.prismclient.aether.ui.util.style class Default : UIScreen { @@ -24,22 +25,24 @@ class Default : UIScreen { ) create { - button("Hello\nWorld!") { - - }.style { - position(20,20) - size(400, 140) - background(colorOf(asRGBA(0f, 0f, 0f, 0.3f))) - font { - anchor(UIAlignment.CENTER) - x = rel(0.5) - y = rel(0.5) - size(rel(1), rel(1)) - fontName = "Montserrat-light" - fontColor = colorOf(-1) - fontSize = px(24) - lineHeightSpacing = px(10) + container { +// button("Hello World!\nHow is the world doing?").style { +// plot(-20, -20, 400, 140) +// background(asRGBA(0f, 0f, 0f, 0.3f), 8) +// font("Montserrat-light", colorOf(-1), px(24), px(10)) +// font(UIAlignment.CENTER, rel(0.5), rel(0.5)) +// } + text("hello\n\nHow are you doing?").style { + font("Montserrat-light", colorOf(-1), px(24), px(10)) + font(UIAlignment.CENTER, rel(0.5), rel(0.5)) + font(UITextAlignment.LEFT, UITextAlignment.TOP) + background(asRGBA(0f, 1f, 0f, 1f)) + control(UIAlignment.CENTER) } + }.style { + control(UIAlignment.CENTER) + size(500, 500) + useFBO = true } } } diff --git a/src/test/kotlin/examples/deps/Generic.kt b/src/test/kotlin/examples/deps/Generic.kt index 7055220..21a604a 100644 --- a/src/test/kotlin/examples/deps/Generic.kt +++ b/src/test/kotlin/examples/deps/Generic.kt @@ -1,16 +1,11 @@ package examples.deps import net.prismclient.aether.ui.component.type.image.UIImageSheet -import net.prismclient.aether.ui.dsl.UIAssetDSL import net.prismclient.aether.ui.dsl.UIComponentDSL import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.style.util.UIFontFamily -import net.prismclient.aether.ui.util.extensions.colorOf -import net.prismclient.aether.ui.util.extensions.px import net.prismclient.aether.ui.util.interfaces.UIDependable -import net.prismclient.aether.ui.util.left import net.prismclient.aether.ui.util.style -import net.prismclient.aether.ui.util.top /** * This is an example of a (depend/include)-able file. In essence, [UIDependable] is a single function From c1bf06fea1ad01ad764448c3092b1d67d0d6a9ed Mon Sep 17 00:00:00 2001 From: sen Date: Mon, 18 Jul 2022 18:32:00 -0700 Subject: [PATCH 31/37] Fixed UIAutoLayout UILabel layouts --- .../aether/ui/component/type/UILabel.kt | 6 +- .../type/{layout => frame}/UIFrame.kt | 0 .../ui/component/type/frame/UIFrameLayout.kt | 19 +++ .../ui/component/type/layout/UIAutoLayout.kt | 148 ++++++++++++------ .../ui/component/type/layout/UIContainer.kt | 11 +- .../type/layout/UILayoutDirection.kt | 20 +++ .../ui/component/type/layout/UIListLayout.kt | 27 +--- .../aether/ui/dsl/UIComponentDSL.kt | 26 ++- .../aether/ui/renderer/impl/font/UIFont.kt | 52 +++--- .../aether/ui/style/UIStyleSheet.kt | 15 +- .../aether/ui/util/extensions/Units.kt | 31 +++- src/test/kotlin/examples/Default.kt | 81 ++++++++-- 12 files changed, 298 insertions(+), 138 deletions(-) rename src/main/kotlin/net/prismclient/aether/ui/component/type/{layout => frame}/UIFrame.kt (100%) create mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/frame/UIFrameLayout.kt create mode 100644 src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UILayoutDirection.kt diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt index 02e5044..a4bafb7 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/UILabel.kt @@ -16,11 +16,16 @@ class UILabel(text: String) : UIComponent() { set(value) { field = value style.font?.actualText = text + updateFont() } override fun update() { super.update() style.font?.actualText = text + updateFont() + } + + fun updateFont() { width = width.coerceAtLeast(style.font?.cachedWidth ?: 0f) height = height.coerceAtLeast(style.font?.cachedHeight ?: 0f) updateAnchorPoint() @@ -30,7 +35,6 @@ class UILabel(text: String) : UIComponent() { style.font?.updateFont() } - override fun renderComponent() { style.font?.render() } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/frame/UIFrame.kt similarity index 100% rename from src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIFrame.kt rename to src/main/kotlin/net/prismclient/aether/ui/component/type/frame/UIFrame.kt diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/frame/UIFrameLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/frame/UIFrameLayout.kt new file mode 100644 index 0000000..e970ad7 --- /dev/null +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/frame/UIFrameLayout.kt @@ -0,0 +1,19 @@ +package net.prismclient.aether.ui.component.type.frame + +import net.prismclient.aether.ui.component.type.layout.UIFrame +import net.prismclient.aether.ui.component.type.layout.UIFrameSheet +import net.prismclient.aether.ui.component.util.interfaces.UILayout + +/** + * [UIFrameLayout] extends [UIFrame], and implements [UILayout]. It is an abstract class which all layouts extend. Most + * layouts will extend the [UIContainer] class as it has scrollbars, however, certain layouts may extend this class instead. + * + * @author sen + * @since 1.3 + */ +abstract class UIFrameLayout : UIFrame(), UILayout { + override fun update() { + super.update() + updateLayout() + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt index a731d21..e4f9362 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt @@ -1,12 +1,16 @@ package net.prismclient.aether.ui.component.type.layout +import net.prismclient.aether.ui.Aether import net.prismclient.aether.ui.component.UIComponent +import net.prismclient.aether.ui.component.type.UILabel +import net.prismclient.aether.ui.component.type.frame.UIFrameLayout import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.component.util.enums.UIAlignment.* import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.impl.property.UIPadding +import net.prismclient.aether.ui.unit.UIUnit +import net.prismclient.aether.ui.util.extensions.px import net.prismclient.aether.ui.util.interfaces.UICopy -import net.prismclient.aether.ui.util.name /** * [UIAutoLayout] is a layout which is designed to mimic the behavior of Figma's auto @@ -26,8 +30,17 @@ import net.prismclient.aether.ui.util.name * @author sen * @since 1.1 */ -class UIAutoLayout @JvmOverloads constructor(listDirection: ListDirection = ListDirection.Horizontal) : - UIListLayout(listDirection, ListOrder.Forward), UICopy { +class UIAutoLayout @JvmOverloads constructor(layoutDirection: UILayoutDirection = UILayoutDirection.Horizontal) : UIFrameLayout(), UICopy { + /** + * Defines the direction/axis which the list should flow. (Horizontal or Vertical). + */ + var layoutDirection: UILayoutDirection = layoutDirection + + /** + * The alignment which the components within are aligned to based on the empty space within the layout. + */ + var componentAlignment: UIAlignment = TOPLEFT + /** * Defines how the width should be sized. [ResizingMode.Hug] resizes based on the components and * the padding and spacing properties, and [ResizingMode.Fixed] acts like a normal component. @@ -60,74 +73,91 @@ class UIAutoLayout @JvmOverloads constructor(listDirection: ListDirection = List var layoutPadding: UIPadding? = null /** - * The direction to align the components within the layout. + * The spacing between each component within the layout. */ - var componentAlignment: UIAlignment = TOPLEFT + var componentSpacing: UIUnit? = null + + private var layoutWidth: Float = 0f + private var layoutHeight: Float = 0f + + // -- Shorthands -- // /** - * Sets the [verticalResizing] and [horizontalResizing] to [vertical] and [horizontal] respectively. + * Sets the [horizontalResizing] and [verticalResizing] t the given [mode]. */ - fun resize(vertical: ResizingMode, horizontal: ResizingMode) { - verticalResizing = vertical + infix fun resize(mode: ResizingMode) = resize(mode, mode) + + /** + * Sets the [horizontalResizing] and [verticalResizing] to [horizontal] and [vertical] respectively. + */ + fun resize(horizontal: ResizingMode, vertical: ResizingMode): UIAutoLayout { horizontalResizing = horizontal + verticalResizing = vertical + return this + } + + /** + * Sets the [horizontalResizing] and [verticalResizing] to hug. + */ + fun hug() = resize(ResizingMode.Hug) + + infix fun space(value: Number) = space(px(value)) + + infix fun space(unit: UIUnit?): UIAutoLayout { + componentSpacing = unit + return this } + // -- Core -- // + override fun updateLayout() { - if (components.isEmpty()) return + if (components.isEmpty()) + return // Calculate the padding and spacing val top = -layoutPadding?.paddingTop val right = +layoutPadding?.paddingRight val bottom = -layoutPadding?.paddingBottom val left = +layoutPadding?.paddingLeft - val spacing = if (listDirection == ListDirection.Horizontal) +componentSpacing else -componentSpacing + val spacing = if (layoutDirection == UILayoutDirection.Horizontal) +componentSpacing else -componentSpacing - // Calculate the width of the layout - var w = 0f - var h = 0f + // Calculate the size of this layout based on the component twice. The first time is to get the initial size + // of the layout, which might change if ran again because the components might depend on the size of this. + // The second time is to get the final size of the layout, which is the size of the components. + for (i in 0 until 2) { + // Calculate the layout size + calculateLayoutSize(spacing) - for (i in components.indices) { - val component = components[i] - if (horizontalResizing == ResizingMode.Hug) { - w = if (listDirection == ListDirection.Horizontal) { - w + component.relWidth + component.marginLeft + component.marginRight + if (i < components.size - 1) spacing else 0f - } else { - (component.relWidth + component.marginLeft + component.marginRight).coerceAtLeast(w) - } - } - if (verticalResizing == ResizingMode.Hug) { - h = if (listDirection == ListDirection.Vertical) { - h + component.relHeight + component.marginTop + component.marginBottom + if (i < components.size - 1) spacing else 0f - } else { - (component.relHeight + component.marginTop + component.marginBottom).coerceAtLeast(h) - } - } - } + // Adjust the width and/or height of the component based on the calculated + // size, and ensure that the size is at least the size prior to this. + if (horizontalResizing == ResizingMode.Hug) width = (layoutWidth + left + right)//.coerceAtLeast(width) + if (verticalResizing == ResizingMode.Hug) height = (layoutHeight + top + bottom)//.coerceAtLeast(height) - // Adjust the width and/or height of the component based on the calculated - // size, and ensure that the size is at least the size prior to this. - if (horizontalResizing == ResizingMode.Hug) width = (w + left + right).coerceAtLeast(width) - if (verticalResizing == ResizingMode.Hug) height = (h + top + bottom).coerceAtLeast(height) + // Update + calculateBounds() + updateAnchorPoint() + updatePosition() + updateBounds() + updateStyle() - // Update - calculateBounds() - updateAnchorPoint() - updatePosition() - updateBounds() - updateStyle() + components.forEach { it.update() } + } + + val w = layoutWidth + val h = layoutHeight // Calculate the initial position based on the alignment var x = this.x + left var y = this.y + top // Update the other direction's alignment - if (listDirection == ListDirection.Horizontal) { + if (layoutDirection == UILayoutDirection.Horizontal) { x += when (componentAlignment) { TOPCENTER, CENTER, BOTTOMCENTER -> (width - (w + left + right)) / 2f TOPRIGHT, MIDDLERIGHT, BOTTOMRIGHT -> (width - (w + left + right)) else -> 0f } - } else if (listDirection == ListDirection.Vertical) { + } else if (layoutDirection == UILayoutDirection.Vertical) { y += when (componentAlignment) { MIDDLELEFT, CENTER, MIDDLERIGHT -> (height - (h + top + bottom)) / 2f BOTTOMLEFT, BOTTOMCENTER, BOTTOMRIGHT -> (height - (h + top + bottom)) @@ -137,7 +167,7 @@ class UIAutoLayout @JvmOverloads constructor(listDirection: ListDirection = List for (c in components) { c.overridden = true - if (listDirection == ListDirection.Horizontal) { + if (layoutDirection == UILayoutDirection.Horizontal) { c.x = x c.y = y + when (componentAlignment) { TOPLEFT, TOPCENTER, TOPRIGHT -> 0f @@ -146,7 +176,7 @@ class UIAutoLayout @JvmOverloads constructor(listDirection: ListDirection = List else -> 0f } + c.marginTop - c.marginBottom x += c.width + spacing - } else if (listDirection == ListDirection.Vertical) { + } else if (layoutDirection == UILayoutDirection.Vertical) { c.x = x + when (componentAlignment) { TOPLEFT, MIDDLELEFT, BOTTOMLEFT -> 0f TOPCENTER, CENTER, BOTTOMCENTER -> (width - c.width - left - right) / 2f @@ -160,6 +190,31 @@ class UIAutoLayout @JvmOverloads constructor(listDirection: ListDirection = List } } + private fun calculateLayoutSize(spacing: Float) { + var w = 0f + var h = 0f + + for (i in components.indices) { + if (horizontalResizing == ResizingMode.Hug) { + w = if (layoutDirection == UILayoutDirection.Horizontal) { + w + components[i].relWidth + if (i < components.size - 1) spacing else 0f + } else { + components[i].relWidth.coerceAtLeast(w) + } + } + if (verticalResizing == ResizingMode.Hug) { + h = if (layoutDirection == UILayoutDirection.Vertical) { + h + components[i].relHeight + if (i < components.size - 1) spacing else 0f + } else { + components[i].relHeight.coerceAtLeast(h) + } + } + } + + layoutWidth = w + layoutHeight = h + } + override fun requestUpdate() { super.requestUpdate() update() @@ -168,7 +223,7 @@ class UIAutoLayout @JvmOverloads constructor(listDirection: ListDirection = List /** * Copy the properties of this layout to a new one (excluding components). */ - override fun copy(): UIAutoLayout = UIAutoLayout(listDirection).also { + override fun copy(): UIAutoLayout = UIAutoLayout(layoutDirection).also { // UIAutoLayout properties it.horizontalResizing = horizontalResizing it.verticalResizing = verticalResizing @@ -179,9 +234,6 @@ class UIAutoLayout @JvmOverloads constructor(listDirection: ListDirection = List // UIListLayout properties it.componentSpacing = componentSpacing?.copy() - // UIContainer properties - it.scrollSensitivity = scrollSensitivity - // UIComponent it.visible = visible } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt index 70147de..fbd9d52 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIContainer.kt @@ -1,6 +1,7 @@ package net.prismclient.aether.ui.component.type.layout import net.prismclient.aether.ui.component.UIComponent +import net.prismclient.aether.ui.component.type.frame.UIFrameLayout import net.prismclient.aether.ui.component.util.interfaces.UILayout import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.impl.scrollbar.UIScrollbar @@ -9,14 +10,15 @@ import net.prismclient.aether.ui.util.extensions.renderer import net.prismclient.aether.ui.util.interfaces.UIFocusable /** - * [UIContainer] is the default implementation for [UIFrame]. It introduces scrollbars which automatically - * resize to content being added/removed. It is considered to be a [UIFocusable], so when the mouse is scrolled - * within the container the focused component will become this. + * [UIContainer] is the default implementation for [UIFrame] and [UILayout]. It introduces scrollbars which automatically + * resize to content being added/removed; furthermore, it is considered the base class of layouts. The container does nothing + * to the layouts. It is also considered to be a [UIFocusable], so when the mouse is scrolled within the container the focused + * component will become this. * * @author sen * @since 1.0 */ -open class UIContainer : UIFrame(), UIFocusable, UILayout { +open class UIContainer : UIFrameLayout(), UIFocusable, UILayout { /** * How sensitive the scrolling will be */ @@ -36,7 +38,6 @@ open class UIContainer : UIFrame(), UIFocusable, UILayo override fun update() { super.update() - updateLayout() // Calculate the distance of the components // and find the largest of them on both axes diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UILayoutDirection.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UILayoutDirection.kt new file mode 100644 index 0000000..e9db6d0 --- /dev/null +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UILayoutDirection.kt @@ -0,0 +1,20 @@ +package net.prismclient.aether.ui.component.type.layout + + +/** + * Defines the axis/direction of a layout. + * + * @author sen + * @since 1.0 + */ +enum class UILayoutDirection { + /** + * Renders the list horizontally + */ + Horizontal, + + /** + * Renders the list vertically + */ + Vertical +} \ No newline at end of file diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt index 1b6a63f..7faf045 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIListLayout.kt @@ -12,7 +12,7 @@ import net.prismclient.aether.ui.unit.UIUnit * @since 5/20/2022 */ open class UIListLayout @JvmOverloads constructor( - var listDirection: ListDirection = ListDirection.Vertical, + var layoutDirection: UILayoutDirection = net.prismclient.aether.ui.component.type.layout.UILayoutDirection.Horizontal, var listOrder: ListOrder = Forward, ) : UIContainer() { /** @@ -23,7 +23,7 @@ open class UIListLayout @JvmOverloads constructor( override fun updateLayout() { var x = if (style.useFBO) 0f else x + getParentX() var y = if (style.useFBO) 0f else y + getParentY() - val spacing = if (listDirection == ListDirection.Horizontal) +componentSpacing else -componentSpacing + val spacing = if (layoutDirection == UILayoutDirection.Horizontal) +componentSpacing else -componentSpacing if (listOrder == Forward) { for (i in 0 until components.size) { @@ -39,10 +39,10 @@ open class UIListLayout @JvmOverloads constructor( component.updatePosition() component.overridden = true - if (listDirection == ListDirection.Vertical) { + if (layoutDirection == UILayoutDirection.Vertical) { component.y = y + component.marginTop y += component.relHeight + component.marginTop + component.marginBottom + spacing - } else if (listDirection == ListDirection.Horizontal) { + } else if (layoutDirection == UILayoutDirection.Horizontal) { component.x = x + component.marginLeft x += component.relWidth + component.marginLeft + component.marginRight + spacing } @@ -58,10 +58,10 @@ open class UIListLayout @JvmOverloads constructor( component.updatePosition() component.overridden = true - if (listDirection == ListDirection.Vertical) { + if (layoutDirection == UILayoutDirection.Vertical) { component.y = y + component.marginTop y += component.relHeight + component.marginTop + component.marginBottom - spacing - } else if (listDirection == ListDirection.Horizontal) { + } else if (layoutDirection == UILayoutDirection.Horizontal) { component.x = x + component.marginLeft x += component.relWidth + component.marginLeft + component.marginRight - spacing } @@ -72,21 +72,6 @@ open class UIListLayout @JvmOverloads constructor( c.update() } - /** - * Defines which direction the list is rendered in - */ - enum class ListDirection { - /** - * Renders the list horizontally - */ - Horizontal, - - /** - * Renders the list vertically - */ - Vertical - } - /** * Defines the orientation which the lists order from. For example if * it is [Backwards], the last component is ordered first, and the first diff --git a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt index a6d6fc9..0af88a7 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/dsl/UIComponentDSL.kt @@ -8,11 +8,7 @@ import net.prismclient.aether.ui.component.type.UILabel import net.prismclient.aether.ui.component.type.image.UIImage import net.prismclient.aether.ui.component.type.input.button.UIButton import net.prismclient.aether.ui.component.type.input.slider.UISlider -import net.prismclient.aether.ui.component.type.layout.UIFrame -import net.prismclient.aether.ui.component.type.layout.UIAutoLayout -import net.prismclient.aether.ui.component.type.layout.UIContainer -import net.prismclient.aether.ui.component.type.layout.UIListLayout -import net.prismclient.aether.ui.component.type.layout.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.* import net.prismclient.aether.ui.dsl.UIComponentDSL.activeStyle import net.prismclient.aether.ui.util.Block import net.prismclient.aether.ui.util.interfaces.UIDependable @@ -253,39 +249,39 @@ object UIComponentDSL { component(UIContainer(), style, block) /** - * Creates a [UIListLayout] with the given [listDirection], which defines the direction that it + * Creates a [UIListLayout] with the given [layoutDirection], which defines the direction that it * lays out in (vertical or horizontal), and the given [listOrder] defining which direction * the list is ordered in. */ @JvmOverloads inline fun list( - listDirection: UIListLayout.ListDirection, + layoutDirection: UILayoutDirection, listOrder: UIListLayout.ListOrder = UIListLayout.ListOrder.Forward, style: String? = activeStyle, block: Block = {} - ) = component(UIListLayout(listDirection, listOrder), style, block) + ) = component(UIListLayout(layoutDirection, listOrder), style, block) /** - * Creates a vertical list via [list]. + * Creates a horizontal list via [list]. * * @see list */ - inline fun verticalList( + inline fun horizontalList( listOrder: UIListLayout.ListOrder = UIListLayout.ListOrder.Forward, style: String? = activeStyle, block: Block = {} - ) = list(UIListLayout.ListDirection.Vertical, listOrder, style, block) + ) = list(UILayoutDirection.Horizontal, listOrder, style, block) /** - * Creates a horizontal list via [list]. + * Creates a vertical list via [list]. * * @see list */ - inline fun horizontalList( + inline fun verticalList( listOrder: UIListLayout.ListOrder = UIListLayout.ListOrder.Forward, style: String? = activeStyle, block: Block = {} - ) = list(UIListLayout.ListDirection.Horizontal, listOrder, style, block) + ) = list(UILayoutDirection.Vertical, listOrder, style, block) /** * Creates a copy of the given layout and creates a normal block of [UIAutoLayout] where @@ -302,7 +298,7 @@ object UIComponentDSL { */ @JvmOverloads inline fun autoLayout( - listDirection: UIListLayout.ListDirection, + listDirection: UILayoutDirection, style: String? = null, block: Block = {} ) = component(UIAutoLayout(listDirection), style, block) diff --git a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt index 480a86c..4ee5442 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/renderer/impl/font/UIFont.kt @@ -6,6 +6,7 @@ import net.prismclient.aether.ui.shape.UIShape import net.prismclient.aether.ui.style.util.UIAnchorPoint import net.prismclient.aether.ui.unit.UIUnit import net.prismclient.aether.ui.util.UIColor +import net.prismclient.aether.ui.util.extensions.asRGBA import net.prismclient.aether.ui.util.extensions.calculate import net.prismclient.aether.ui.util.extensions.renderer import java.util.regex.Pattern @@ -105,8 +106,23 @@ open class UIFont : UIShape() { * Calculates and updates the bounds for this. */ fun updateFont() { + val component = component ?: return + + // Adjust to the right resize type + when { + (textResizing == TextResizing.AutoWidth || textResizing == TextResizing.AutoHeight) && (width != null || height != null) -> { + textResizing = TextResizing.FixedSize + updateFont() + return + } + textResizing == TextResizing.AutoWidth && actualText.contains(NEWLINE) -> { + textResizing = TextResizing.AutoHeight + updateFont() + return + } + } + update(component) - val component = component ?: throw RuntimeException("Component cannot be null for UIFont") text.clear() // Update initial properties @@ -118,27 +134,15 @@ open class UIFont : UIShape() { font(fontName ?: "", cachedFontSize, horizontalAlignment, verticalAlignment, cachedFontSpacing) when (textResizing) { TextResizing.AutoWidth -> { - if (actualText.contains(NEWLINE)) { - textResizing = TextResizing.AutoHeight - updateFont() - return - } - text.add(actualText) - val (minx, miny, maxx, maxy) = actualText.fontBounds() + text.add(actualText) // Update component size cachedWidth = maxx - minx cachedHeight = maxy - miny } TextResizing.AutoHeight -> { - if (width != null || height != null) { - textResizing = TextResizing.FixedSize - updateFont() - return - } text.addAll(actualText.split(NEWLINE)) - renderer.calculateText(text, 0f, 0f, cachedLineHeightSpacing) val (minx, miny, maxx, maxy) = fontBounds() @@ -148,14 +152,10 @@ open class UIFont : UIShape() { cachedHeight = maxy - miny } TextResizing.FixedSize -> { - if (actualText.contains(NEWLINE)) { - text.addAll(actualText.split(NEWLINE)) - renderer.calculateText(text, 0f, 0f, cachedLineHeightSpacing) - } else { - renderer.calculateText(actualText, 0f, 0f, cachedWidth, cachedLineHeightSpacing, text) - } + // TODO: Fixed size newline + renderer.calculateText(actualText, 0f, 0f, cachedWidth, cachedLineHeightSpacing, text) - val (minx, miny, maxx, maxy) = fontBounds() + val (_, miny, _, maxy) = fontBounds() // Update component size cachedHeight = (maxy - miny).coerceAtLeast(cachedHeight) @@ -182,19 +182,19 @@ open class UIFont : UIShape() { } override fun render() { - val x = cachedX + when (verticalAlignment) { + val x = cachedX + when (horizontalAlignment) { CENTER -> cachedWidth / 2f RIGHT -> cachedWidth else -> 0f } - cachedAnchorX - val y = cachedY + when (horizontalAlignment) { + val y = cachedY + when (verticalAlignment) { CENTER -> cachedFontSize / 2f + (cachedHeight - (cachedFontSize * text.size) - (cachedLineHeightSpacing * (text.size - 1))) / 2f BOTTOM -> cachedFontSize + (cachedHeight - (cachedFontSize * text.size) - (cachedLineHeightSpacing * (text.size - 1))) else -> 0f } - cachedAnchorY renderer { color(fontColor) - font(fontName ?: "", cachedFontSize, verticalAlignment, horizontalAlignment, cachedFontSpacing) + font(fontName ?: "", cachedFontSize, horizontalAlignment, verticalAlignment, cachedFontSpacing) when (textResizing) { TextResizing.AutoWidth -> actualText.render(x, y) TextResizing.AutoHeight -> renderer.renderText(text, x, y, cachedLineHeightSpacing) @@ -202,10 +202,13 @@ open class UIFont : UIShape() { TextResizing.TruncateText -> {} // TODO: Truncate text TextResizing.Auto -> throw RuntimeException("Auto should not be the text resizing mode when rendering???") } +// color(asRGBA(1f, 1f, 1f, 0.3f)) +// rect(cachedX - cachedAnchorX, cachedY - cachedAnchorY, cachedWidth, cachedHeight) } } override fun copy(): UIFont = UIFont().also { + it.apply(this) it.actualText = actualText it.fontName = fontName it.fontSize = fontSize?.copy() @@ -216,6 +219,7 @@ open class UIFont : UIShape() { it.horizontalAlignment = horizontalAlignment it.lineHeightSpacing = lineHeightSpacing?.copy() it.truncatedText = truncatedText + it.anchor = anchor?.copy() } /** diff --git a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt index 0ba34e5..d37059c 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/style/UIStyleSheet.kt @@ -6,8 +6,8 @@ import net.prismclient.aether.ui.component.type.layout.UIFrameSheet import net.prismclient.aether.ui.component.util.enums.UIAlignment import net.prismclient.aether.ui.component.util.enums.UIAlignment.* import net.prismclient.aether.ui.renderer.impl.background.UIBackground -import net.prismclient.aether.ui.renderer.impl.font.UITextAlignment import net.prismclient.aether.ui.renderer.impl.font.UIFont +import net.prismclient.aether.ui.renderer.impl.font.UITextAlignment import net.prismclient.aether.ui.renderer.impl.property.UIMargin import net.prismclient.aether.ui.renderer.impl.property.UIPadding import net.prismclient.aether.ui.renderer.impl.property.UIRadius @@ -176,7 +176,6 @@ open class UIStyleSheet : UICopy, UIAnimatable { align(alignment, x!!, y!!) } - fun control(alignment: UIAlignment) = control(alignment, alignment) fun control(alignment: UIAlignment, anchorAlignment: UIAlignment) { @@ -198,10 +197,11 @@ open class UIStyleSheet : UICopy, UIAnimatable { inline fun background(color: Int, radius: Number = 0, block: Block = {}) = background { this.backgroundColor = colorOf(color) - if (radius != 0 || this.radius != null) this.radius = radiusOf(radius) + if (radius != 0) this.radius = radiusOf(radius) block() } + @JvmOverloads inline fun background(color: UIColor, radius: UIRadius? = background?.radius, block: Block = {}) = background { this.backgroundColor = color; this.radius = radius; this.block() } @@ -257,14 +257,13 @@ open class UIStyleSheet : UICopy, UIAnimatable { } inline fun font( - verticalAlignment: UITextAlignment, horizontalAlignment: UITextAlignment, + verticalAlignment: UITextAlignment, textResizing: UIFont.TextResizing = UIFont.TextResizing.Auto, block: Block = {} ) = font { this.textResizing = textResizing - this.verticalAlignment = verticalAlignment - this.horizontalAlignment = horizontalAlignment + this.align(horizontalAlignment, verticalAlignment) this.block() } @@ -283,14 +282,14 @@ open class UIStyleSheet : UICopy, UIAnimatable { /** * Sets the padding to the given [value] as pixels. */ - fun padding(value: Float) = padding(value, value, value, value) + fun padding(value: Number) = padding(value, value, value, value) /** * Sets the padding to the given [unit] as the unit. */ fun padding(unit: UIUnit) = padding(unit, unit, unit, unit) - fun padding(paddingTop: Float = 0f, paddingRight: Float = 0f, paddingBottom: Float = 0f, paddingLeft: Float = 0f) = + fun padding(paddingTop: Number = 0, paddingRight: Number = 0, paddingBottom: Number = 0, paddingLeft: Number = 0) = padding { this.paddingTop = px(paddingTop) this.paddingRight = px(paddingRight) diff --git a/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Units.kt b/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Units.kt index 1ecd82a..50ed8b0 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Units.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/util/extensions/Units.kt @@ -49,20 +49,35 @@ const val WIDTH: Byte = 5 */ const val HEIGHT: Byte = 6 +/** + * The width or height of the component's parent, or the screen width/height. + */ +const val PRELATIVE: Byte = 7 + +/** + * The width of the parent of the given value, or the screen width. + */ +const val PARENTWIDTH: Byte = 8 + +/** + * The height of the parent of the given value, or the screen height. + */ +const val PARENTHEIGHT: Byte = 9 + /** * The border width of the active component */ -const val BORDER: Byte = 7 +const val BORDER: Byte = 10 /** * The width of the screen */ -const val SCREENWIDTH: Byte = 8 +const val SCREENWIDTH: Byte = 11 /** * The height of the screen */ -const val SCREENHEIGHT: Byte = 9 +const val SCREENHEIGHT: Byte = 12 /** Unit Functions **/ @@ -85,6 +100,13 @@ fun rel(value: Number) = UIUnit(value.toFloat(), RELATIVE) */ fun percent(value: Number) = UIUnit(value.toFloat() / 100f, RELATIVE) +/** + * The relative value of the parent or screen size + * + * @see rel + */ +fun prel(value: Number) = UIUnit(value.toFloat(), PRELATIVE) + /** * Creates a unit that represents the font size of this (else 0 if not applicable) */ @@ -185,6 +207,9 @@ fun calculate(unit: UIUnit?, component: UIComponent<*>?, width: Float, height: F EM -> (component?.style?.font?.cachedFontSize ?: 0f) * unit.value WIDTH -> width * unit.value HEIGHT -> height * unit.value + PRELATIVE -> (if (isY) (component?.getParentHeight() ?: Aether.height) else (component?.getParentWidth() ?: Aether.width)) * unit.value + PARENTWIDTH -> (component?.getParentWidth() ?: Aether.width) * unit.value + PARENTHEIGHT -> (component?.getParentHeight() ?: Aether.height) * unit.value SCREENWIDTH -> Aether.width SCREENHEIGHT -> Aether.height else -> throw RuntimeException("${unit.type} is not considered a unit type.") diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index 0bd816f..f45baac 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -1,8 +1,10 @@ package examples +import net.prismclient.aether.ui.component.type.layout.UIAutoLayout +import net.prismclient.aether.ui.component.type.layout.UILayoutDirection import net.prismclient.aether.ui.component.util.enums.UIAlignment -import net.prismclient.aether.ui.renderer.impl.font.UITextAlignment import net.prismclient.aether.ui.screen.UIScreen +import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.style.util.UIFontFamily import net.prismclient.aether.ui.util.create import net.prismclient.aether.ui.util.extensions.asRGBA @@ -25,25 +27,78 @@ class Default : UIScreen { ) create { - container { -// button("Hello World!\nHow is the world doing?").style { -// plot(-20, -20, 400, 140) -// background(asRGBA(0f, 0f, 0f, 0.3f), 8) -// font("Montserrat-light", colorOf(-1), px(24), px(10)) + style(UIStyleSheet(), "button") { + size(260, 50) + background(-1, 12) +// font("Montserrat-medium", colorOf( 32, 32, 32), px(18)) { // font(UIAlignment.CENTER, rel(0.5), rel(0.5)) // } - text("hello\n\nHow are you doing?").style { - font("Montserrat-light", colorOf(-1), px(24), px(10)) - font(UIAlignment.CENTER, rel(0.5), rel(0.5)) - font(UITextAlignment.LEFT, UITextAlignment.TOP) - background(asRGBA(0f, 1f, 0f, 1f)) + } + + autoLayout(UILayoutDirection.Vertical) { + hug() + autoLayout(UILayoutDirection.Vertical) { + hug() space 70 + autoLayout(UILayoutDirection.Vertical) { + space(10) + componentAlignment = UIAlignment.CENTER + horizontalResizing = UIAutoLayout.ResizingMode.Hug + verticalResizing = UIAutoLayout.ResizingMode.Hug + +// label("Welcome to your dashboard!").style { +// font("Montserrat-bold", colorOf(-1), px(24)) +// } + label("Customize the dashboard to show your statistics from Bedwars to Skyblock, or even Minemen statistics!").style { + width = rel(1) + height = px(20) + font("Montserrat-regular", colorOf(1f, 1f, 1f, 0.8f), px(14)) { + width = rel(1) + } + background(asRGBA(1f, 1f, 1f, 0.3f)) + } + }.style { + width = rel(1) + background(asRGBA(0f, 0f, 1f, 0.3f)) + } + autoLayout(UILayoutDirection.Horizontal) { + hug() space 10 + + + button("Show me how!", "button").style { +// font { +// fontColor = colorOf(-1) +// } + } + button("Maybe later...", "button") + } + }.style { control(UIAlignment.CENTER) + background(asRGBA(1f, 0f, 0f, 0.3f)) } }.style { control(UIAlignment.CENTER) - size(500, 500) - useFBO = true +// background(asRGBA(0f, 1f, 1f, 0.3f)) } } + + +// container { +// text("How are you doing? I REALLY love line breaks! They make lines have breakings!").style { +// font("Montserrat-light", colorOf( 37, 39, 51), px(24), px(10)) { +// width = px(150) +// anchor(UIAlignment.CENTER) +// align(UITextAlignment.LEFT, UITextAlignment.TOP) +// x = rel(0.5) +// y = rel(0.5) +// } +// background(-1, 8) +// control(UIAlignment.CENTER) +// padding(50) +// } +// }.style { +// control(UIAlignment.CENTER) +// size(500, 500) +// useFBO = true +// } } } \ No newline at end of file From 5ccfd6a9a95e328a958256e46bed9e963ddcd0fc Mon Sep 17 00:00:00 2001 From: sen Date: Mon, 18 Jul 2022 19:12:36 -0700 Subject: [PATCH 32/37] Update Default.kt --- src/test/kotlin/examples/Default.kt | 58 +++++++++++++---------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index f45baac..c1410f9 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -30,54 +30,46 @@ class Default : UIScreen { style(UIStyleSheet(), "button") { size(260, 50) background(-1, 12) -// font("Montserrat-medium", colorOf( 32, 32, 32), px(18)) { -// font(UIAlignment.CENTER, rel(0.5), rel(0.5)) -// } + font("Montserrat-medium", colorOf(32, 32, 32), px(18)) { + font(UIAlignment.CENTER, rel(0.5), rel(0.5)) + } } autoLayout(UILayoutDirection.Vertical) { - hug() + hug() space 70 autoLayout(UILayoutDirection.Vertical) { - hug() space 70 - autoLayout(UILayoutDirection.Vertical) { - space(10) - componentAlignment = UIAlignment.CENTER - horizontalResizing = UIAutoLayout.ResizingMode.Hug - verticalResizing = UIAutoLayout.ResizingMode.Hug + space(10) + componentAlignment = UIAlignment.CENTER + horizontalResizing = UIAutoLayout.ResizingMode.Hug + verticalResizing = UIAutoLayout.ResizingMode.Hug -// label("Welcome to your dashboard!").style { -// font("Montserrat-bold", colorOf(-1), px(24)) -// } - label("Customize the dashboard to show your statistics from Bedwars to Skyblock, or even Minemen statistics!").style { + label("Welcome to your dashboard!").style { + font("Montserrat-bold", colorOf(-1), px(24)) + } + label("Customize the dashboard to show your statistics from Bedwars to Skyblock, or even Minemen statistics!").style { + width = rel(1) + height = px(20) + font("Montserrat-regular", colorOf(1f, 1f, 1f, 0.8f), px(14)) { width = rel(1) - height = px(20) - font("Montserrat-regular", colorOf(1f, 1f, 1f, 0.8f), px(14)) { - width = rel(1) - } - background(asRGBA(1f, 1f, 1f, 0.3f)) } - }.style { - width = rel(1) - background(asRGBA(0f, 0f, 1f, 0.3f)) } - autoLayout(UILayoutDirection.Horizontal) { - hug() space 10 + }.style { + width = rel(1) + } + autoLayout(UILayoutDirection.Horizontal) { + hug() space 10 - button("Show me how!", "button").style { -// font { -// fontColor = colorOf(-1) -// } + button("Show me how!", "button").style { + font { + fontColor = colorOf(-1) } - button("Maybe later...", "button") + background(asRGBA(87, 164, 255)) } - }.style { - control(UIAlignment.CENTER) - background(asRGBA(1f, 0f, 0f, 0.3f)) + button("Maybe later...", "button") } }.style { control(UIAlignment.CENTER) -// background(asRGBA(0f, 1f, 1f, 0.3f)) } } From cef614ac49a3caba9f9017cf16aa2d7f229a3a0e Mon Sep 17 00:00:00 2001 From: sen Date: Tue, 19 Jul 2022 08:51:05 -0700 Subject: [PATCH 33/37] Update Default.kt --- src/test/kotlin/examples/Default.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/test/kotlin/examples/Default.kt b/src/test/kotlin/examples/Default.kt index c1410f9..9de3909 100644 --- a/src/test/kotlin/examples/Default.kt +++ b/src/test/kotlin/examples/Default.kt @@ -1,11 +1,14 @@ package examples +import net.prismclient.aether.ui.animation.ease.impl.UIQuart import net.prismclient.aether.ui.component.type.layout.UIAutoLayout import net.prismclient.aether.ui.component.type.layout.UILayoutDirection import net.prismclient.aether.ui.component.util.enums.UIAlignment +import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.screen.UIScreen import net.prismclient.aether.ui.style.UIStyleSheet import net.prismclient.aether.ui.style.util.UIFontFamily +import net.prismclient.aether.ui.util.animationOf import net.prismclient.aether.ui.util.create import net.prismclient.aether.ui.util.extensions.asRGBA import net.prismclient.aether.ui.util.extensions.colorOf @@ -35,6 +38,16 @@ class Default : UIScreen { } } + animationOf("blue-hover", UIStyleSheet()) { + UIQuart(1000L) to { + background(colorOf(asRGBA(87, 164, 255, 0.8f))) + } + } + + animationOf("blue-unhover", UIStyleSheet()) { + keyframe(UIQuart(1000L), UIProvider.getStyle("button", true)) + } + autoLayout(UILayoutDirection.Vertical) { hug() space 70 autoLayout(UILayoutDirection.Vertical) { @@ -59,7 +72,6 @@ class Default : UIScreen { autoLayout(UILayoutDirection.Horizontal) { hug() space 10 - button("Show me how!", "button").style { font { fontColor = colorOf(-1) From 1ee0c5b8f0a466b42c3ed42ccd393f78b3d58ecb Mon Sep 17 00:00:00 2001 From: Decencies <66835910+Decencies@users.noreply.github.com> Date: Tue, 19 Jul 2022 21:09:23 +0100 Subject: [PATCH 34/37] Update .gitignore --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c28f76d..a26c788 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,6 @@ gradle-app.setting *.DS_STORE *.log -src/test/resources/ +/src/test/resources/ -src/test/koltin/examples/prism/ +/src/test/kotlin/examples/prism/ From 9ad4f0a8d82b59deea1e09731edbebc48807c0f9 Mon Sep 17 00:00:00 2001 From: sen <102120097+senxd@users.noreply.github.com> Date: Tue, 19 Jul 2022 13:13:00 -0700 Subject: [PATCH 35/37] Update UIGridLayout.kt --- .../ui/component/type/layout/UIGridLayout.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIGridLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIGridLayout.kt index 2c39452..e3651f9 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIGridLayout.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIGridLayout.kt @@ -1,9 +1,9 @@ -package net.prismclient.aether.ui.component.type.layout - -/** - * - * - * @author sen - * @since 1.3 - */ +package net.prismclient.aether.ui.component.type.layout + +/** + * + * + * @author sen + * @since 1.3 + */ class UIGridLayout \ No newline at end of file From 963f7753e2ca3820280175758bc9f037332aad2b Mon Sep 17 00:00:00 2001 From: sen Date: Tue, 19 Jul 2022 20:52:13 -0700 Subject: [PATCH 36/37] Fixed Auto Layout & mouse propagation --- .../net/prismclient/aether/ui/Aether.kt | 30 +++++----- .../ui/component/type/layout/UIAutoLayout.kt | 60 ++++++++++--------- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt index 28cb9e5..72ec829 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/Aether.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/Aether.kt @@ -3,9 +3,9 @@ package net.prismclient.aether.ui import net.prismclient.aether.ui.Aether.Properties import net.prismclient.aether.ui.component.UIComponent import net.prismclient.aether.ui.component.controller.UIController -import net.prismclient.aether.ui.component.type.layout.UIFrame import net.prismclient.aether.ui.component.type.layout.UIContainer import net.prismclient.aether.ui.component.type.layout.UIContainerSheet +import net.prismclient.aether.ui.component.type.layout.UIFrame import net.prismclient.aether.ui.event.input.UIMouseEvent import net.prismclient.aether.ui.renderer.UIProvider import net.prismclient.aether.ui.renderer.UIRenderer @@ -165,27 +165,25 @@ open class Aether(renderer: UIRenderer) { * mouse coordinates. Index is the index of the component. */ fun peek(list: ArrayList>, index: Int, clickCount: Int): Boolean { - var component: UIComponent<*>? = null + var foundComponent: UIComponent<*>? = null for (i in index until list.size) { val child = list[i] - val check = child.isMouseInsideBounds() - if (child.isMouseInsideBounds() || !child.style.clipContent) { - if (child.childrenCount > 0) { - if (peek( - if (child is UIFrame) child.components else list, - i + if (child is UIFrame) 0 else 1, - clickCount - ) - ) return true + + // If the mouse is inside the component + if (child.isMouseInside()) { + // Check if the component is a UIFrame. If it is, integrate through the children + // of this frame and find the one, if applicable that should be invoked. + if (child is UIFrame) { + // Break the loop by returning true if the component is found + if (peek(child.components, 0, clickCount)) return true } - if (check) component = child + foundComponent = child } } - - return if (component != null) { - component.focus() - component.mousePressed(UIMouseEvent(component.getMouseX(), component.getMouseY(), mouseButton, clickCount)) + return if (foundComponent != null) { + foundComponent.focus() + foundComponent.mousePressed(UIMouseEvent(foundComponent.getMouseX(), foundComponent.getMouseY(), mouseButton, clickCount)) true } else false } diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt index e4f9362..a3f9ebc 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/layout/UIAutoLayout.kt @@ -143,51 +143,55 @@ class UIAutoLayout @JvmOverloads constructor(layoutDirection: UILayoutDirection components.forEach { it.update() } } - val w = layoutWidth - val h = layoutHeight + layoutWidth += left + right + layoutHeight += top + bottom // Calculate the initial position based on the alignment var x = this.x + left var y = this.y + top - // Update the other direction's alignment + // Update the x or y position of the layout based on the leftover space in the component + // and the alignment of the same axis of the layout. The other direction is updated when + // the component's positions are updated below. if (layoutDirection == UILayoutDirection.Horizontal) { x += when (componentAlignment) { - TOPCENTER, CENTER, BOTTOMCENTER -> (width - (w + left + right)) / 2f - TOPRIGHT, MIDDLERIGHT, BOTTOMRIGHT -> (width - (w + left + right)) + TOPCENTER, CENTER, BOTTOMCENTER -> (width - layoutWidth) / 2f + TOPRIGHT, MIDDLERIGHT, BOTTOMRIGHT -> (width - layoutWidth) else -> 0f } } else if (layoutDirection == UILayoutDirection.Vertical) { y += when (componentAlignment) { - MIDDLELEFT, CENTER, MIDDLERIGHT -> (height - (h + top + bottom)) / 2f - BOTTOMLEFT, BOTTOMCENTER, BOTTOMRIGHT -> (height - (h + top + bottom)) + MIDDLELEFT, CENTER, MIDDLERIGHT -> (height - layoutHeight) / 2f + BOTTOMLEFT, BOTTOMCENTER, BOTTOMRIGHT -> (height - layoutHeight) else -> 0f } } - for (c in components) { - c.overridden = true + for (child in components) { + child.overridden = true if (layoutDirection == UILayoutDirection.Horizontal) { - c.x = x - c.y = y + when (componentAlignment) { + child.x = x + child.paddingLeft + child.y = y + when (componentAlignment) { TOPLEFT, TOPCENTER, TOPRIGHT -> 0f - MIDDLELEFT, CENTER, MIDDLERIGHT -> (height - c.height - top - bottom) / 2f - BOTTOMLEFT, BOTTOMCENTER, BOTTOMRIGHT -> (height - c.height - left - right) + MIDDLELEFT, CENTER, MIDDLERIGHT -> (height - child.relHeight - top - bottom) / 2f + BOTTOMLEFT, BOTTOMCENTER, BOTTOMRIGHT -> (height - child.relHeight - top - bottom) else -> 0f - } + c.marginTop - c.marginBottom - x += c.width + spacing + } + child.marginTop - child.marginBottom + child.paddingTop + x += child.relWidth + spacing } else if (layoutDirection == UILayoutDirection.Vertical) { - c.x = x + when (componentAlignment) { + child.x = x + when (componentAlignment) { TOPLEFT, MIDDLELEFT, BOTTOMLEFT -> 0f - TOPCENTER, CENTER, BOTTOMCENTER -> (width - c.width - left - right) / 2f - TOPRIGHT, MIDDLERIGHT, BOTTOMRIGHT -> (width - c.width - left - right) + TOPCENTER, CENTER, BOTTOMCENTER -> (width - child.width - left - right) / 2f + TOPRIGHT, MIDDLERIGHT, BOTTOMRIGHT -> (width - child.width - left - right) else -> 0f - } + c.marginLeft - c.marginRight - c.y = y - y += c.height + spacing + } + child.marginLeft - child.marginRight + child.paddingLeft + child.y = y + child.paddingTop + y += child.relHeight + spacing } - c.update() + child.update() } + + updateStyle() } private fun calculateLayoutSize(spacing: Float) { @@ -195,20 +199,20 @@ class UIAutoLayout @JvmOverloads constructor(layoutDirection: UILayoutDirection var h = 0f for (i in components.indices) { - if (horizontalResizing == ResizingMode.Hug) { - w = if (layoutDirection == UILayoutDirection.Horizontal) { + w = if (horizontalResizing == ResizingMode.Hug) { + if (layoutDirection == UILayoutDirection.Horizontal) { w + components[i].relWidth + if (i < components.size - 1) spacing else 0f } else { components[i].relWidth.coerceAtLeast(w) } - } - if (verticalResizing == ResizingMode.Hug) { - h = if (layoutDirection == UILayoutDirection.Vertical) { + } else width + h = if (verticalResizing == ResizingMode.Hug) { + if (layoutDirection == UILayoutDirection.Vertical) { h + components[i].relHeight + if (i < components.size - 1) spacing else 0f } else { components[i].relHeight.coerceAtLeast(h) } - } + } else height } layoutWidth = w From 7c0703067345921ee59244fc68f15cc62a6c187f Mon Sep 17 00:00:00 2001 From: sen Date: Tue, 19 Jul 2022 21:12:59 -0700 Subject: [PATCH 37/37] UIButton scale to it's font --- .../ui/component/type/input/button/UIButton.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt index 26fcf0f..452391b 100644 --- a/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt +++ b/src/main/kotlin/net/prismclient/aether/ui/component/type/input/button/UIButton.kt @@ -14,16 +14,28 @@ open class UIButton(text: String) : UIComponent() { set(value) { field = value style.font?.actualText = text + updateFont() } override fun update() { super.update() style.font?.actualText = text + updateFont() } override fun renderComponent() { style.font?.render() } + fun updateFont() { + width = width.coerceAtLeast(style.font?.cachedWidth ?: 0f) + height = height.coerceAtLeast(style.font?.cachedHeight ?: 0f) + updateAnchorPoint() + updatePosition() + updateBounds() + updateStyle() + style.font?.updateFont() + } + override fun createsStyle(): UIStyleSheet = UIStyleSheet() } \ No newline at end of file