From 72caa3ccba0f8d24994a346661bd260a1e5afdc1 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Tue, 25 Feb 2025 09:50:38 +0100 Subject: [PATCH 01/23] Fix TextLayout.Width calculation (#18305) (#18310) * Fix TextLayout.Width calculation (#18305) * Use WinSymbols3.ttf --- .../Media/TextFormatting/TextLayout.cs | 49 ++++++++++++++---- .../Avalonia.Skia.UnitTests.csproj | 2 + .../Fonts/WinSymbols3.ttf | Bin 0 -> 189020 bytes .../Media/TextFormatting/TextLayoutTests.cs | 13 +++++ 4 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 tests/Avalonia.Skia.UnitTests/Fonts/WinSymbols3.ttf diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 39a8ff870e..590d2f2133 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -679,20 +679,47 @@ namespace Avalonia.Media.TextFormatting private void UpdateMetrics(TextLineImpl currentLine, ref bool first) { - _metrics.InkBounds = _metrics.InkBounds.Union(new Rect(new Point(0, _metrics.Bounds.Bottom) + currentLine.InkBounds.Position, currentLine.InkBounds.Size)); - _metrics.Bounds = _metrics.Bounds.Union(new Rect(new Point(0, _metrics.Bounds.Bottom) + currentLine.Bounds.Position, currentLine.Bounds.Size)); + // 1) Offset each line’s bounding rectangles by the total height so far, + // so we keep an overall bounding box for the entire text block. + var lineTop = _metrics.Height; - _metrics.MinTextWidth = Math.Max(_metrics.MinTextWidth, currentLine.Bounds.Width); - _metrics.MinTextWidth = Math.Max(_metrics.MinTextWidth, currentLine.InkBounds.Width); + // Offset the line's Bounds + var lineBoundsRect = new Rect( + currentLine.Bounds.X, + lineTop + currentLine.Bounds.Y, + currentLine.Bounds.Width, + currentLine.Bounds.Height); - _metrics.Height = _metrics.Bounds.Height; - _metrics.Width = _metrics.InkBounds.Width; - _metrics.WidthIncludingTrailingWhitespace = _metrics.Bounds.Width; - _metrics.Extent = _metrics.InkBounds.Height; - _metrics.OverhangLeading = Math.Max(0, _metrics.Bounds.Left - _metrics.InkBounds.Left); - _metrics.OverhangTrailing = Math.Max(0, _metrics.InkBounds.Right - _metrics.Bounds.Right); - _metrics.OverhangAfter = Math.Max(0, _metrics.InkBounds.Bottom - _metrics.Bounds.Bottom); + _metrics.Bounds = _metrics.Bounds.Union(lineBoundsRect); + // Offset the line's InkBounds + var lineInkRect = new Rect( + currentLine.InkBounds.X, + lineTop + currentLine.InkBounds.Y, + currentLine.InkBounds.Width, + currentLine.InkBounds.Height); + + _metrics.InkBounds = _metrics.InkBounds.Union(lineInkRect); + + // 2) Accumulate total layout height by adding the line’s Height. + _metrics.Height += currentLine.Height; + + // 3) For the layout’s Width and WidthIncludingTrailingWhitespace, + // use the maximum of the line widths rather than the bounding box. + _metrics.Width = Math.Max(_metrics.Width, currentLine.Width); + _metrics.WidthIncludingTrailingWhitespace = Math.Max(_metrics.WidthIncludingTrailingWhitespace, currentLine.WidthIncludingTrailingWhitespace); + + // 4) Extent is the max black-pixel extent among lines. + _metrics.Extent = Math.Max(_metrics.Extent, currentLine.Extent); + + // 5) We can track min-text-width or overhangs similarly if needed. + _metrics.MinTextWidth = Math.Max(_metrics.MinTextWidth, currentLine.Width); + + _metrics.OverhangLeading = Math.Max(_metrics.OverhangLeading, currentLine.OverhangLeading); + _metrics.OverhangTrailing = Math.Max(_metrics.OverhangTrailing, currentLine.OverhangTrailing); + _metrics.OverhangAfter = Math.Max(_metrics.OverhangAfter, currentLine.OverhangAfter); + + // 6) Capture the baseline from the first line. if (first) { _metrics.Baseline = currentLine.Baseline; diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj index d006c8c189..d092e3b0b9 100644 --- a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -12,8 +12,10 @@ + + diff --git a/tests/Avalonia.Skia.UnitTests/Fonts/WinSymbols3.ttf b/tests/Avalonia.Skia.UnitTests/Fonts/WinSymbols3.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4cba3d13a16c30c8b5536575aaef88eee51def76 GIT binary patch literal 189020 zcmeFacYIV;{x^Qkxifbrb!IY|mNMxRNJxOpBmoja=tx%yB?5xLgd&1Ui3$=swp~;} zlf|+MNVAgNbzRh~71zX77F}J@HLjY3qM*1S$y|Q#&$%;6EL)!MKhN`fUQdQ|%jx%= z&-wIoV4N`)&+3@M=8YLMZPKJ;z7?VT^{?N&)KdN(OEV*au}>!txm&*vJw|ske)1+{30X$Z*?py&djAArgPHM74Buw-8|#ti)$ht*Sx}&iV8*DP3Te0^e^Y~_7qYrmcJ|rs z)-hdr&kTA`(-}t^#)SG#f%~~1o%geYczS@f;Hd@o^Sq8{@&-y}Ev%Wnh3oSOU+@OX zBZ54Lx5)1eJndza5{{L%&^to-_bN(N@@lG+YLN9t(q7c_Aot$x{7CC={dKB|-cahz zaE{Y>yI;nk4bdWdDC?$l)kUP>lYONviLs|L7Vj-ooH+d0v$a3flQVt^a0ba+Rm-RMa zG$>xyrPs%)R13eKy}{lP>3F7=Y5ZT*bK(uX3FnpLL49WDsYj7txIK!e+M4leVUHtx zNGbR#T=Aok-piV4?nO``AZcMu2!E30vN6uD;!XC4y+N&`Y!hAa+!;=JMdk&Z&^!A= zRLa?*aU4L9Amf)J1#^Mx`xtq;Qw`L&x^OuvtN-Nei*$xD!3?1u#J`E2eTCX+?DDCs ztf3H3@=L-ak7o(R;o9~7cGnxJ|L5+nvmgB;bBW&VQR>8vd>>nf-{Z3WFR1VMJwPkr z2dhB#U4EAwBkC!AAH~Qq@A?sJUo5}l2C7YmY059*SdJ!Qdmvas_fMgg1%C{yXp!Lb z`N%lo`8)zaWw6(G^^C*o4C|u+hBer)}me@zefGVy(m(Xey6dB{HW)VFV&w> z!b7RD=QNJ=jFqdGkn3foXu+3ui11yOJ5M7^@Q3n!FuRyv-h|$~B+JPaCRU;hl`N7y zqOqX_-uKGyAvo4kHt_4^38-O`7q_lzy{2_t>+P*~w%*set@ZKN zH(TFt{iyZR*3ViyKHJi!w8gX)whe3>)i$kdPTPXEC2d!>t!i7>R@ZiW+g)v&+U{+u zZ`;-OMBDzh-?cs8cA)K*w%6MZw*8~6z3rQ}(`})4wLQ8$uHDjZYj?J%wP&^a+WWQ- zXdl#G);^+rbo;9IYuY!oZ)<<3eRum4?Z0n-rTz8x=JvPS|JwdZ`_cAq+P`Z*+uqrJ zzC(0GcUU_TI-DIT9cdkXItF$GItF))?3mawrDJBtB^`4+F6*f2Slw}L$EJ=g9St2j zJN9+_spIXALmlsReAv;^@mGqYIB-e)O87 zbw_VKy6Nb>N4FfUKl<{~u4BeymSeVKBaV$aHtyI(#}*vB{@A8t_aA%c*q&p5Irg_> z?Z?f>qmMg}=NvCOKK%H|<71DHKYr2iX~$ViKG*0Cvr~oIWhFas1wsp%s6q$iMb~pJ@M3u-<&vdqV+`QSH)jdel`B9 z1z+9wb^osieqH|cvadINz5AqcGU4Q=lTV-g&B?b<{^jHcC;xHsi<509J5GLoD(Y13 zsr*xgr-q+ecxv&fD^5Lp>WNd$-^6}%)i-y3JMG(NPOGQwr`Md`bGqrf72njOPyxaa^`={;4+fTQjX+PH<0(=-cVgVlx2_G39 z{W|;-K1O#;4#UU1j$p^~j@pi2cHH|P;G^{DFu=!zqmw0kT-FUAw*x*l{{%k99-9z` zkFCcZJobo$kGA6~;KKp<7 z!|<{H#Cs<`{Q-RR1ALVAf{(jTJ|W@b(8>2se%1{i=2JciAC-WQnp4Y8Jp}l8s~3D2 zC4B4#d|VNRj}zVSkplP_c4nl6k2}shCE;Vw`SWQ?8c$>1^WKoR%lp0etoMxfJMTB% zQ{JQA4(}J<&%OWfzTth{`rDuib3eR%S<(`W?Q$15WlRc9>V?CohBRwNL!#%@1 zWu76P!Jbl2iKoA(z|+r@=SlaZdfc99Pn5^xQ9VYF!6Q5%4{psJa(B5q-RIojyU)7M zxW99sc7N;s#(mO#%-!K`cYopj-2JKhukQEV@3~vtZ@J%eA9TOwZgRiq-s9fw-r;U= zZ*kYUSG%uruXHbW&vjqyp6ae}``ra@x7+1TbX(nV?pU|Ub>7wG`p(ti`iJWi*Adr8 zuD`kd>Uzqx#&v~jk!ztV;PSf$y864)Tt??<=hx1o&JO1voqupX<-FHF! zp(QiEDgGDn*Tgl)Jr{R(+>E%3;*7E5VhdyQV*AEgVxwZsF~?*69P>uZ&KPU-$I(ye zbO#jY|NZ^H5%}2=;A&V1M=A$90ula)f5fl+-~T7|J@PYW;u;A@FedX5nlLQT&nO0j z51ljP=oUzo|2&mV4gt2s_W#BzC z9RcOD=0W8&5#canIjG-fMnGKd<&5>IW2~87oG73DQefA|P!L!r*4chD>FwY$L)+XsA&4 z&<7Z+K%QZUtF$sU+|Aes)IXw~v5`$s_k7OS=v9o3!8eSpMCf8{+&acCK>qOq5I$vW z!ePcHE=EAwB(y!boUtj>7@OM2*oCh$b`k2o81K`v8LNs#K)usZ*L0Mff$x}!KFpfP zSoJu@E@23dFgCl6u{mgC?mUD8j9nT)c$%?!c%J_OW0##~YysL_aD=giD7z5fwg~M6 zFK4XAjqoXBiyIkRG8F;eu(X!3WvG7{@?DN{%g-{lA_W0;twj8l==W9A5D>pA5#b=k22q+8MjkjqnI#cTGdshJe1_y$S(s{Q~)Z@hW4RHZgV& z`f$%a#_mJ;%?tt0TksuQkZ4_EpCAqpe2N``VF54#vUu#&MCkp5yH#$KArSku#t{c$^EFB=fh&MPSM$~eYe z&1UR1Hv;N?4RyVae!PL_gLRC(iT6L@{jEnBYu*RZ6VGq=LpaUYJ1F<=0EEvOJA|}D zhZ*||(hnyhoMr62Q3yzTe-mSWMLi!bWbAJ<8T*I=;*YFi>|+DMtBidz8v*yfFNVe! zeQh1Y*k{>{{UaUWX2w28-@icImlWnP)@ER=9r-)fF?MtuW272Bb~(a3P*~|>l9qRw58KH}@Gj)ueeFSRAnT(x7dgp8e z)Y&zPu@LfTs6*QY{$n=dydT0QkQclw6B#$S88<%7xLS_zD&wY`q4{0McvK<+@Bsm~1HwkeFSH_{+(oGKV!Th=#CR3joQ`&YNBK-M0@7!#V!Rs9)#$?|NSi%~ z@j2rdpNqQYb}@b_>Yj&o=erS}WBjs4#up&p!r2ITUo-##-xI`h&0&~fPBOk^A;JfY zFGW4eMj@b|m#<=cIoer{_$yHMinENb+{pNqD0@{q<5#2nDwJPS&iL9vjMw&I{F?2I zUwaqh*CF3^?=ZgZX2#byGrqyVcpdWBoo4(7)O7>yH{$&!)OBk%0?OUyMnKt(7{84l zFn;?1#_vG=ch)j~*GvS&-JOmw6`_&wUyOn=WF_PG3}XCVl)3i-#;5?m6eRYioCC)>}!J%4lw>k3c_KSzV1RmeQ(Z0K>0sSLqPer zkltL5faey}-?EDFx92hbXXJkeZNG~?A3~W!uQL7@Jpbh^i7udK03|#5#&E|gz=A2=f}qx|6~BdI>!Ir#dxcg@qY|LKpmg=K|tIW$o~cU z^yPNO+sY9(G2Y(Jct`)Q1y8^w6%bBuRA&3Fhw+sOF&dEmOIF~O%YAsP`* zGNH^wz}QB6j2}cS7%D57la8E=)8=iekB*((k*u+HYW~e1mhZkkjkS~2D6B(6E zWLgnEWg=@k6WObn$Z;cVW5S0t-{(x^9$=!+JSO_$o`?4Ho0;f`_=3BbD4fbf|3OR) zC}(0I;)-^`#MK7@`2zTkV$@TD_(5ibgG>zWhk$+!X+-E^0{o;XN7-`JRgN-4(dJN; z8G4$D3e;P%4xx#OVFrXFOjIspVmRs;j`AaBBOG924bO)!9tU-p9loGXmblX)#7)R^vw?|Q7BX=w^4xluiQ7^T@O~TW*oeN}j`r_xBjEXt516NIAT_T5=1Au z>MJ+?A}%R8P6@`us117a#sE5T*}!ozNul4N2cf6($4xRkWsbLW2CeZ?;zD!-0{)NRc4@SY6i0>`r_*iNl&GB&4>OZDT>Eb1eF1TP(iSl+*QWx|1{T{=u{Tlm) znmq|sgAKvKRSBMuC%z(H_~X5)oeinpcxC{z)1tPR{LI4ASbsJQZTR(p&gU3ve5(mx zR>q6u03%M#`48!WwN%yCR?)>)mfwJ{8=7%_ z^j{@lUSQ`rXeu@pGb_do#_^H)8OlKQaP=9=Y@4O_N#>i!QD(y< zqqNUP4cS>XRJ_^MlGayyeq0o?(i~w7xgW6F!fdPs&)iquR2XwcjIYI$Pnu?q#Qq{Fnj%~0kzl-IUaTKbq)Bs>S$7n%t)*0ZSERxUxUF+<58ThUT(n?UE_IwJkhm~55p5( z`D1Eaj>RM_!b0~rLyQ_ag=gI9J#;E`DiSX&ct*TB^}ov2yIPF7kt}~EzbS~P43^JI z*+}#=BjC$aO%hXJj1$#-uEcX1+gv3p(}$5q$7z5|?HQ~ao7e)zUh7&3@<4T6b#Uw9 ztwIjV<=4hPxmqh+{bc;Lm&;+A7`19LKQwt&)HR(u`SlXt@CJR&tC)!D%B5SkF7@Vl zJ3n5vZQCkC7CqiHX~~jF2G@D!b%g4v7E$BqdGdNO-hKrTWbgyuL2)BtM1lqP$N7u_ zUtFB;T(eL4+r+rIiE+rm$rdWtZ8BbL@@um)!ry@prWz!k0vV7?AFQ z)*fG}ZL8Cs_{3GGjJ7H%dDgB!ESVX&WHuK(W(i+!k}#=7d$KWyym$pbN_%2OsBe6J zx)Luw(6cR>>Ays4L|IXM&IfgwSMy$TsmXj1k5mqd%){-L7Z6fJ9_6q2S|u23O;DVGaclPgFZa#?7dhUsMzV2_uSmT@P(BAx^M!mAIH zTgdrvajV^Hj!H=sx5o4Iy!g;dwEkX-&r9cq>EczVqBuk47+r1^YtPkETgHu{+pRn$ zPp<7Ca2sbS)P~dLNK_p};R?|nkAPAk(6L?6s z__?*BmO9Q?iQDX{TotOoXWvE(FFw=+*bX%TJjv_*v&-Kpm0 zNa~G|D=E!UwkM1qpYS?=zVASTSNr3H=$b*=YHjtPn&^pIlUEtomp`xmH77YaM_V~u z9XdHxJE)BtHa_*8cT&d>x$*4#5g@Yqu`#YN4`< zf9ck1atm(Ul2TpSB`Z+;mDMSmQ}JETvj)=`(7s9R7Irt=!Wh_|5^x*14#NOqVd0Cs zT)w%SE+-M|G~qOSNRT{hKtt9h7|Q;Q^rO)pf?$XYTOCjET?gV~B0ZG}-N@%$B?XW+BGJc5~1T-LBPM4%z00K$801lXRKu98bjLEk^Oz;yFCI|ou zVJLznkQ~)uauI9*U;>7FKD6X}Ef1S+{(gSlC70BB^K7Q1JX>MT=EaLQ*Kl!X(BjRv zd}z`Btmoqfam(4C$*F0Poa$X45^aB2ESz7b?bdeJdDG+aGS#@OoXz}Be&^tx;o4Q8?^w;|H<*%Ad6&|`D)@% zy$;HxUsXnh$MH3igZ5cWD4}pWKWNF9Otp5W;dc%qt^`MZ2}c&dQ6IghJ*2h7AH}is zL85fhsQ`#l!Lt_!d@2FW(#{6D8d+#kqT3vrR9$HRdG|j628-0S*?$mpAVGskdvh&BHfVS0EY%Hjqn6lvof=VH6bUHITDd> zWpErU;)eTg;*Vn(=o_madaGUw-G4vd_M-L;P}e$c(Y{ez?$<)~Z#|?{hrgXae$)Nj zfTGF(?VA^0LQV^ELT(r2^=oW^JXQdC4DDmi;uYXPC6yCrH zucu7s(@rL(4a><<`sI~4K`bLL#m+{p5qv&>zOta8vOsM)wc9^u=(x;u#n*TA_(DH% zZ~opf8yXuo5PjU;IRp6%D$y0d6TB@|%v2bjGO{4d6KjZtg`RM9MZuyYEDIX!$MV4v zB6Lms1;$Thoej|m?lgxX7?t4mhMseHxpt??;)}E3^AB1)F_swKti~APOqQHD?M|+w zs4YsW9odu=T+cxQG5k)ulX74j`5TE`iP8L&nWFfb&T*(0X7aFn(3I1{WwHKb?zF#jb8 zPCS<&&X=8TP({6xndecW7u_0+Ry=uGiXpEePyDJP&+vozOvTXkj#BTkScF|`*Jf&M zc44vb3wX(U>0N(Me~VA#13tR(#*egjv_|b6dOCAe6F@ziM>Sn_Rg-o`JEEOwGQ8&S zs9BC-7hN>Wk)?VZ?kxPde-!tz&FXqtJIB?RT~?bGeB!p-4C%cqhKD*e7HXAMqVk?* zXs(I>&~dvBYcf~I?mD9lem2bemf8rH*s>tPl>n`TQ9Gw~%GIE*8&OsaE8zDO)Fx>4 zm9ya8nqS`4qA^mHfL*T#mwJ(Q9dB4$r39+h7HBIADz&_-wavVNxZ5sPro0C6zd64Q z0$v)wMdAt;G@)lKz$)1y^ps{Mm8Mfe4$ zOUyCR=6fGY?wj=56S3MIE}!6YP2=JOzwskO`nkjF)*Yscb}^y&MzdltDCS1O=CdCf zzXZ(Xj@1vD{4^^&llms#d9HCO&GEel9*eU?X@8om2w}J=D&U`KnyaR|%x1nO_O4&` z$=Zo&`~Ek2&8S}3i6ZSc!!OGe8)e>4P0e01K8aRu(&`-;AsxRqm>MvTel`Ou!B$e* z8^30#Cd^(c=cP^;aaj>m%fiD1rqjpN#5A5oxxxbjHq)fySu$+_V#j=H%iw|tI_95Q z4B9|K!J7jmpZ7w{A%Az*`2nxxl807r2clXs{-H~#=lk_R+HVnlzCG2ac?h)=J6YOa zBI+4gj@x?Bw=$=Dj0Mydmf71^>)$f@MhB1E49p;^Rsbx1+CMx;{-xRar{P zIhLB_F~9kynHTIZ4mkH#Ux=AQD#AkG&z|hYq)Z;^c{##EOOAw z&vbwrgxH#B!cTQ%LV$IU$N?>(z(O%exjzgI!*~Y8gN*{MwyNnqD`<+8B1{fHF8xI;GzV6`kw(T;$C~Pow8II%7#og`i`3Rf;aeSMq%jA*!db| zOwt=Ll!Y4zt>P(fyrF&m27lt|_3NKrZ>&pqMTPQLJfiZUw*GcAwXs*lvNfn^+$gvj z8_k75cEvzFGJwMod+c^(HdRa+5+~kE^6@v-z1;YtLR1)kkwS`r_bL_2Px7H*@m{Zn zLmKt#4W2R<{aXJtv{u-+VhXAaQh!wf%j62SmTds!$+7cg`og0Lxy|V>f;3mbhYEr) zUoJo_l@Cl6QnLWwJ}VOvF{qM^<%uNSnbdTW_Hung#lTWWqSHa7z?qRwQemlH56D$_ zTT=G(1$fJCFK*oUVm?oNmjH0>#m6tB?P!aux>6lD_o&YdXiQ ztr87YwVaz0@^232uI zYN&}mNzWppY0~3CZrWhXM{d0D%>RalF5-~P=#)TqE3~&y~D{@;P5XIS&$7Cl& z7h1&?>zA3`Q>W(VCgd5+8HPktrY*r8uSO}V+v+qrxyfKP_RTN4Scy{xq@|>I67%YY zy4Exrm5lVn+w)^R(Q0r^{}S#|l9OljPtEnl8&q$iD>kFhkSs7vX^%d{UEiu!k$zA+ zyzk|mhFHU*fZqa4VTyJ}yIuGQ<@)idxp7z}(J5(@u7{`@krh@zjhdR&={RU$T0L2?fF&sM0%$Q>x>{fjX3eT%F?UST6?(SqUnq;*V#amSJ8g%H;a&Jj1_iXhp_sto>F zm0dw8&^A=g!uUt`TK694toyQokb_1-61tdVq3p066Tt%vR1HiZR=2UF6iC2dV#-jh zt_+{mo(1|ytDY7uEk=+~Yc3@ksYnG#AH6%RCWRKbzmOYt{)oY$#d+z{;VfCaJ zXho?x*B%_+ncQD%@86%h6Z>=yKX`3UYW@@?E|>Y7FfhG}LEDs^FY^xUkGzSA+mI<4 znSXBG8?Rq5f9&kpW9JtSE6U%o^2)!i*Uqh9&sEK}az}pAu(?+vX#r&kuHlJm)@aAq z1h1Scvj&lCJ>^z*QofbMGE2Ov%Nc$)o=s<00j2;SVO^^)7eD~@DtKP7g+OmTs#d2< zGDf8C2coh=*i=o0#5jWr$;VGUBR{gG|-PMPi4aw2+WIdepLogL~;; zNewI2s{AvnJM>S215(!>|K%qc#RHPJFRj~u!$OOC+tXLPV2JAGwzbc|S4kix21|c1 z`K8uY+mM^ehe{1?NrQ4GS6lZ+$%M~wN5ps6(-ZvB6 z_Ed`6r4NLzM@eQytQ;ds7EP(I7>aKN*H}in1ESgfqW`F6F*LwD0;rcugYZSOL%Zm| zQM?$QqP$RU^pqCtztzP+kHBH4l{|_a9tQc4p33kg)O(nI} z%J8|<%gUygHxM3VVG?uslE9|NKx_REp?ZRjV=mLo~^PA%j*!)$scK za;P!zy1yVTy`UgH?f;-L{~y9^Sa1X|K1{{zhSKyO;wF^p{4^#4_WC0X+z z)*0~%`4U0^6+Om+GHx5_BCVoC91$Kur{so#{OssUFCE?aBwe=;&79_O483C8wkr%5 z(N)V6Yv+y-PmP#6cSPvI5p%Tz7kOeblQTbQST3Gg-q5f-bm8&_l6AxS=UC=I``xM$ z?koj@c9HW4w4Sm~G&CSn^aw*TRqcMomea>(TWTHAN>iWgSw-d)$&2oqmp=!X3A zviz5qFB3%r)aypPc70WCBUR$lq+&s$uJzqifnT`!(7QbE&>?&<#`sln&{Sq@054L7 zF;0irEk$-HUx|ulf|rO#jI_&v%gE+nH|QTJ<%s9-%)XD6cF=4l7OFN zdY1#9u#ZFUoF8kASvYK0$>aq8h8x!2?(o_M6+K?DEWu+>FT94|DlQg~trT}uLcd*} zOoP>wvWRB|+JiBTxKZxkU#A4> z_V2IjYOdSQ{Z(s)KPdcbtC~YCyrybxs3z31w#wKL$+5rAz}8l2^}^pfy;=CR`l_{{ z8=?}OYEqk69M%DB>QI4`<_ij50>tce-Rhj<0QSuSk35!N#Z4bSR`=M)VvKhB`R94; z^IB8M_`0oXllJ}ZAMbL7o)=?WU623%w&(TaYwO0BEZqt_LN$9?U2GZ&>NFa#Anmjy zB+41bAtr>UZ$xOiM{?pnT=@8Lsh9`@dx zl^)}Ct{ks*Xjf`iFG)!W1d=AFHKj%mSE81zTK7=#!pwQFh>{HszD_~lHW%s9nDiSV zr5%tfR7Zqj{TaDFM)t=r-yn^R(hvu4KrlNN?zdR97Sd-P>2eU)pd345(Z=CbIYzHt zjw7TxMHb4cKINrHxDT~Z>=AHFA6Y`{x*wq|MlP67{G=4^@u4I?gDgl1gq>h>B0q!T zfYSp+KB6Pl+pqOvuydxpw;M0kIpt3<0t*( z2pPih5UfFj%L0XvETu8Sf!^qX8lEJl1DQo9Cc0*liWHNd91UDe4P7fu@rz(9lzoUAhy73o=WS+A;wBCO!c$ zfR$<*WEupYn0|nF@*V*eBd1T30&l7)o>dI@6*!N;ZU$quf)oy~h#a_q8sgGc-!yH>bD_FXPez{-*&{!Gqd*E9X9wR^;+_hrxEm-V;hs4opI zPaL4lA8^x}v}cn_w0r!nTplCK@tEA@iT62*`IH=6f9;_e+4qHh%~w?Sd&admhkS&> z{-7@OR0mI;2czMXU|Y%+$-62vhIwh7Ib?q@98rTLmV(`)F%{2sW|OVj@DaWrvZ!Fv zzDqEc3@Fn%>5-5{7a+YC?;PQiv%@@yAt#J?rs7nddOo0*@E~l>+I$tA*Hq1>8Qt3E64L=0^Gq4@QZ>22X0uZlwkJ zO6t(uw_736LP%XAcw9w2X(Vf_LU4dH!EgWjHKLBA&IZ5e`*wtwb3C3flNx<^9WE~T`RBK1h99%kAaLc5_C#R%ZibA$0}K{TSoh|r6Og0pZCxD*%Q z*U*ch7m%i$3N;`O7sQDXVkF`iXD_lBO{Jz0YzT&3BzU(KpA=Vh=Dtl@#|MT}+NZc}rdl>#FvgJ~QXI1;?C37Z$MfU^Gn|gQ zmVYL3BdzqjG~3mi6B8%F=F$rvlYhcc;0Rio#W1T%Rgxo7n7R=&vFaq?OU15kCXcLd zReD*eiL~j#+oQ$(vPm+M8gY_&-ru+OSjLphJ++YI&XR0VrdyWC4l4D&J3rTMT!#EG zCQDu6A7=77f4R5YWd3WEtKDNmxw7fqYb*$_>kG(!3=WfwJ-{Qwcswy3FeeaEE~+qs z&ro$QA!CtOxWJ#93WZGNg-R~e(%@IRorv^0O4S*l!je|6u2?g7@wkOU0*2BdE2dtv z{I2mdpu?}5fB6M9WhF|F1>B+8Un267C-;+b)!?w>89yh|FQ=8j13DK8)`?U zTD^6l$5s;{MXFV2Gy}Lm+Vd@|xhri*;l(8b6o2t-|L{yVX55ilRye)5NbwiV9ymNR znXduxHRoM4qdeK1Vwx~-Bqp<~Mf96mRhDc?S$VBtH6d@}7MiIM={xa1(RuuDl^>1W z`i>Ov(1n;;+6jUwl**+Z^a*g3N+*Aj0(CH{-Et@gF)^7u1LTdE>Ku73DlxFpRiNMb zWgz4=8yd^&x@Ipw$Ii4vhnN zhV!hxo{)XnXu6*Zj+v$eyRez00XK5n1yaMQU$ARd;WW%lXv)T;=kTTpBg0(6eZ{t7 zNTHD^DgPR~k+Mz?;LIz{no!Ea#{rxKSZJ7s-Wxpj@Yb$96$=vgD>LWct{t1Ya^=*{ zy;H8da*Cm!)|21!1qe00<1uiZOKU*JQ{Rs`V$nZ+4^(0{}c40nMRXzISMaT;zM)0ee@tK zq7G>R=&e5bF-zc!2-B!IC#;wxe8nflU@3Sf6nls^L(ean#2N>8z@MUc0+OuJNe=Od z^%t+cyM>nY(yg~Gh^N&9;y~o$9DqaANcC)2gkO-nlH)naE`$i@;h)N^(H^S6BWu+o zaix|iHrhJFYX0*tx4etEtxFf)hy7I`JVb7Y+Gq}9C9r!2SC~+A!-F&w1k)UnKMOjp z3(_ahi_~%G;~xV%34DnqKuH!^@?VnX!Jok8bPS9TLJla?IUMPEb)iNS&%zs`U;)*n zQz9SH`IJKiBSTgYX%PuM2HX7zLMmDi&KG6}aS%bW!jSR=4bOyQgq#FanXK7I`&pSCBZ?vg&S#nwa!axx4cV= z6Rp3r7ZZ0w@_mME#Aj)F0|t@H)Y_^xdj?Mi7gjGTi<4j!s?a(KSh#WQ zA>QZEq0MV=4;zI4Z}ltJ{Z+_sGMTg}-JEk)FGZrC#6w?kPH&bPliIRHtt`&W5;T365mf5LN();26wEkgJ7sHKV%_Znq+C{ z`_P9{Mw-55fGo*GL!IHUkG zdAjeF&b?@o|KncCd_=b7X@7QGc~tN1cRic)3J1~VyCDT&$%7GRp04rT+Cc42c&M4U zDb^Gv^%83UsFL{rUg(xNX&)z6V?>76iQA9oOZ9hS8?U$X7IOBjt|V_?c>4CjMBbWk ztaJya`T75&Zrs3d9;s;}{2g7unS%6LKhNf#p_itIf$127o* zuS@?{*l2x<^mEOnon|g4#T%N-CuVD_;+)!dG=@CJ8OQI+o>;EM^()uDBaF^t%KP!V z`F8Avxqbfp+iB-b{BM67A54geE?0&{$7JV4Pg2H5_f?zEEyfYHRDQkXx+HBD>kz^<_-ogeBixnb6l6lW>Rjew{rU zbOI+5!_UpnyjVwALrf}oEgYLVh(IN>EI6uT4DdbN$MMb3MnQN(8WbocF4CwM2e8XS zy6j`ahtj~~UtDmBJKBEimSgrnOkeGrmj(|Cts4H^1sBJU7x$WZ^X5aKRiR7r%>(k5 z<%nOqQUm2%4w(;IB;$S9XAHi?fQQQ1Sm`VKA6Rb%&f+eJ5z_4p z3s5?p!SfKP+HcITa2v4nFmB7W|7g~Xa-)>)y`ePp_-@QPQ{wT&g-m^pV(c3Pe@2j7JRrnvqLHCsf>z)Z73KR`Q7Q0LS_4~j4{;s>jTSx#aY5cmL z{UlzzP@E1+|K_Wkd&Wk<`2X+Tn7Mn-^?kHyz_LWWCtw_O-C~L$YMSk?HW*Tf&| z%X%m`5K}5AIWtR~B}+aT{>!Ixzf@sJZG`?@&LKEP0AcA?5+4*e@pM9hFf`M%%=@vd zqUI;h{A^Frty)(|Zi~kW(6Gh({n8Z(c;`*BubKj&A^m!^=zv*lFxQnGU@N38LV+8s zUG9!hGN76!1Y1&MkK$5Zkcu72zeu@3Y2o#bv}KRYB0^D9FE^UrjsvRqn617(=7&NJ z51IS;tY*RDMzvw*!WlzuUYb;%)_B^0j~{O%ewEsUwt1I=p>T5Kp%T47)YB? z>VgFVYS3Kl=BACnI^(H)2|M?13yXEDcX6T9Dc%V!;$oZF#*0Fel9D1Dt<=}8R_)T1 zIVGVNq#b-@$()qzY(7|9$+Z>y9zI*U%xdKih5ksAD)mQTM;h$FY0%PKsN)r)W?>I0 zxk*}o*LSHAv)=n%(so}ibO7O9Y>^Da+S9hS3{t0qZbA9{cx@syiS)etWb30&B_MC% zs9wpTdXmqPkBk?%DFGuKjtKMYuea+BoUw)A#o^$aTn|~`^Lmf z0y&9`?gnp&&9t39tB zeJnx-{-ii$K=GJf+?=MJ8-TZq~} zX>D8H(mqN_7<=Ku`BnX%XE#g1O8EUMZEk%7G=w z4Zb)~8UwK>5sy~-QoB6ifQ7~5?68+N_}B`7qe;va+s93Xxd0ZyiO)Vev5|Ha*G(Pw znl?JdoSi4W*|T=-p4zS!Tx$1-B}4h5n7nLrj47q_>pm#6zj4FWGkAjb)r_m*_5h1w zA8ZERlaS{$pEFAB?;fc==5Ngvz67%~F9CM0`+17GLp?-xmath10=yM$BrqajJ!ua> zH}Pzxz5HL%3Kk=4vBj=7GPD?vd7E?{$aGSBf--GAOqsAq zL-hr9)s%^yZ|s&8^|NdISO>b#YB%#9Hf+-3EW`MK!G~idX){UJ4#)u-3LgzZ`a#_W zo9K*aF(Pt-E3%ZsA~2W!8M=yGlP0J~ty7BNVl~D3sN8~)=Aa0z1rhf|vH`dAam3I= z*d@{u*UPEZE9EUxx#xMYMI2QY3?5uiT^ZIsdFhRe5J2vrX`~ASHG>_kUbhiSofyFp z&Px)okgE)5&}7lBNBJeaS=@{t#gt-$nZEAjww&;~$D)(VH_WjL7V?wLkJdH1MC-bt zvz{z>c%*}ja5Dx3bZ0$m1?@=dVLu=u)2`{IcVZqrZ`JX7v3|0eE5JysK`BqsIeS z?Y)~3n2P)>;7|kq3IlLbpu`F4FYT|{;d^fhBjW2Dkt@2ner#q6jabFUUXuWgC2au~ z3$(M^nai+3gh}#1nq+3mdP^4Frl6nO6xFqFwfS-z_KMO*TXET#u?s-=I6o@+DxJGx z2K)~)Ek=?ZEPi;>5`0?d@XUHTwR3}ju z1{}6O{4Yi(rfy()@=T2IUSfIXP(*K^Q6ve3ZcHl_xmg(;8-gzhZz`Tl-RzpzR0iHn5Nh5^(a%Z--WAz)_i3g-o`Ic{O@7xDqO0$DQ@_7WLgno)84AXNDKA|1Tq*ZF%pw&yHLMzVeXwm97G$Q_9eK)5{ zewLwzy2h}-dUIo)2x4ejLefSp!gg8s$6y~2>Fq#2>|yK+f&uYCT4sCBCJEzY2>%c< z!U}7kND@p=Np0r>t1B)B=Vx)La@I~^c#IZG(H^w8Wr_xt3Cd`oWJ&>I<_qj9PL)AC zsY@-LAUBI{vt*Uy(J-L52nho5~`F-+kqdn`!U21z3(FQ79KG8u}rjFIlPHhB%0Bm4`6ZAQP7XLUtn z9`X^1f{a?lY5;Y*2nQ7X11}g_#$h-EG!h^In({>kjeJ7jU#1mEf9G>==qKLcI50m(52Fa`gMlC(_)SG?R^D`YH958%WDJotgJbq z1}GoJ>hg_l*CPEWFbJgfVw*hqgP1tlAxmmNn{E%JRT-wgKcLfqJp=aE?3PG?0gG}* z>wk9_+)U_jKm9yEd_wquzkC5Bo49bdL?+B)InPJ}+Z}q!p*V;*OW*k_NbYj)y6!{Y z%`ao_(&T;|=oqjX^d4bGNZT#(MFubeeFcyK8G3d65INt=PUleIBlORNS2Q`t;usSz z*EK3ML{J_8CD0}$TG_gG3CmiWB?<1D(DvS_8 z0BVV_Mv3r&mM=9Jp?j@{_#xVInsI*Lka&X?)zrnt*2Tf>Lhy% z8(nsn!)40NfXbP4w)WCudkORlz@Y?~1ds%Qh|%8_{hciM_82aLS#jS(t}oOso@eTl zXiqRN=ikTO#pgUF?&p6RO#P5F$hRi+x2sdg;JHXFqT5K`6q}vZ{Pj_jKRvVo2ZV2` zJ&V$&+(eu{$;Ay6`Nz$j(=ZhHN28@b8TMsB=ddDlkNh6sd7bC8!?U0QcwV=ihI<~K zhyc!vAhbr?C+V;OSm3jv0mLl+bSv3U01qZsX+0%x!VvZ_IJ}1B)d`01|tIW1UR8w=?=%v8+q)~#& zSn}btGVJ{W**Y77_nCyrTIh>e)W;Wp+d(xp#vJ+{DXM?Nr8ZlL1@AR+yyB1-LwXE< z&>6MpFUBa&YbZXlo`a@-@Ring*pV+Yb%4eVEDR1k>UoBu#hz1a6k(1tA^{s)2Tmi~ z?9yptLVo#BHM&na%yJ=rQyumUR||$qb)6~2S`p~{6bt(B)_}@!Whmz_i9!6Oa)Zm^$TDWWrG2a&&^~?(|3N6M%Gw8q^nJ$4Y-B|Np31M%exdyX zSy5mIH{E-$_I>lAEt_G1-jXkk+0?I|vqeY;q|3f!*uhw#dq5J{DF+pZWcOr`10}|P zmk6uS4=5i&Vz(@!IVl5%A@CA+97`v)l_ zhuzURytjiTeC)$Mhk(W8G*=N(jO18b!lm0TO|Zo#6V)hkrDfdv&b?8Qq`42x{ZUdz zn#)QKnV|;hmI=FUge8_vnGrwGptNPvN+bjyKBOw_<{~NFFt`N%$1ewFupBu;ox!#D zv&R1AMpVa7GV2AeBYHx9&7cfnzh~Xune=*Lyl;X$F#~-LV-MQfXBDt-Z!GJmap|aQND54{L|7HNGab>D-U<+Ml%0mV^Y_ z(^V#&d>_MsHSfCehgAHEE81nH{pM3mj>ibMLa7SWGus<)iOZH1T}ylHp}%0SOT9op zRzg0Jx_3?3fUgTE@SP`S9swFAe6o$0Fu2m&0`_hfS9uuMP|h#ROVf8317{B|Tw7HU z2yMiFcUHBwa4>WMv>y-jsW1tri2#R7E;6$BxM;Fk=dQScsqX=Q;(wvUnTCG&rH3U`;+1e_(+^HGl#&)mCN^SppL&?B zN(Slm=${^MSgp+gm>HUkp~^dY9ffGhJ_p9iYk7Uhk54=F^T&|Tc_KqJad)qi5yLP= ze)CDN1jBDO;%@Upr+L}GC=SF-4)fB19&|WYb{A|Boau8bI1l$&^Q``{2Ys%VgbpkB zYD=|e#HGNIasTj)Gt_r7BI zF5=9cTpt8g0kOFsZEgY_0 z3Y&jq1MvXGQ>*Ok?3O}DK<`^&%Z+fZB5u7lXP-&Cg!v$`JZHiZ0Z=Mu~_`8bfyhIn|WmeF0(R}P{8ugwniFh4%ApQfuKO8C06J!z<)-RBN#KGdmqe4ROCaD7pv07%f-3#= zv=_8oK9$f+h!OwFNAeG}XXsrX%ibB#b*TnYr!YEO-LEphj)&#Q5yK`x5*egG5zo^l zZoyk`*;fRyLGUCPsoRfhM~~l*hu*TR+7Mc{@7r(p3F*}sT6Wi0ci#EcUGnYRqhawA zd&K-aAlr3dO%m5d-%&)~m?kh0@j@y$^^FQ7(T6v(3;%>-UbE(6C+LvFBm^ouan>wj@LhbX5?_9C|{Uv*AN|FCdtzrI!7hT5LCF^F>c#{8w z?qBskUN7hc3bqp>KOu-oN&k0`JRRY@Pa{ z=^9{e7!-1_6Hbwbu*e;eIdBJZksTLD*~qhRS~zO?%4ilme@yQ5glcPH96&*eA#YFFO} zV=D6-2VIez+GpshUoHvG-<=w3OO5V3^TK%}r$?u$W*qFIkIhDm&CUAQ@Ctbl96l4H zfHQ;q00Qj061g~Q1`c`j$rVaz3HJ{LNYI`PIRe0@biPHrA~&YWQ=*K{>{5FPAz4`J zjB#aSCV5kAc~Pd==y+Z|_kjy+W86=CUl)3L%!2sr#M(;-dn@Nm_Xi3vd!grqT&G_ojS&L-Mj|3)oOC&6g%u`DM=MqS1+AD z5yP=)*BoubO-V-xepdnrF)?aofUGjs2`r+nu-f8(0T@+tY6hcT?YpL9ufAAvfIQUUqL zy5wi?(<9`RA$yar=}i*I2nFFbac%=(n7GSiAKx*qNEpU{ByT9y95Jq+25o|}43p*= zD-hiXXC^b4!tC={4GPnuLW^&wBklrWFN`Ifc||s?8)n-|z(;a(AqD@DJ@bOdL(D>78iSVN zs!2_Sm&I(UKkl6@VldGN@C8RAy&n88{FfkZ6Zkj@k=WHc>!QLh`G`LwsK=<;Mzn!h zatfVbX9p71zHW9)#d0;v{pj`Sh3Rungrgg+|6yw0dDQx0*~N!9exKrXrXbyT#Wt(8 zY*h8g3m->Xy(e&^-9Eo86VE(ic|m;0}txfOm?M@v2BLA5G ziGM6lI)nG~A8+w-WTd=(k#{qzXLV<6YHt_s=7@Zu|4Fvcapwhnf!rUX-^AE; zLXUSrGX(EUvfo00aTRjN3&#^>V38R|2s3g8N8y_>RByn~k&woBkGHb{_&>enwr_LG zw{7L#43~`!+|3?*LxXz)3ObZ-kra1P{$MZm!oKY5n#Rw^e%M>-*5^qc^yMSkB=8-E z%ba{lHSFkZ^2;IXcCreL=8%G~AEQY%Z?Fed5loEd=-oB<7)|K?ANCX1%|7bKulT- z4dNK-?v@Mnj$nI`TZ6QJ1skjwB?}w|65xOZ^CH7D+9G{UPnf`x4aB0 zL!PxGO_eYEk6xa=X(1Zu#Y zK;vD#-r+%F-sKOy$cCk`yEW(F_Jk=mCcSukK`K1^sorA#G^wfVJ6T9Dp?5HyE#X0^SnAS#gYtCrt2FWKyj%zm+H*VT>L}6%-bFHmjabWNl?B)7kdg zTDE;!N-1mO6YI17c7YUj5?@V z?HV+21{_K<$m4{(;sZm#bN*Ta(zj^QVURw#H}2pkHj~4A{IcOR=U0n3aF;mg+n-sM zdy}9PMid_-Z}5Fm_YB%gXqiU3RvVs;Z27?Qf#%UXcxpxkKM}bR;cW^EACX^=suf01 zpKzLnJQsDA;sU*{Oo1B`M#;|Ab3oY;c7ns9m!|Qt{72IO#`H{^rhkA1d`y$;s(PyQ z538yqHx}?wg{UP8FbP3nS-n$au@0fCrK+T3aiUkJvFEGk_@t8vTQ?C0A#Wz|cBm2| z{)QNLk5)lIc@^aVUS*Z+PFAU#g!9P^<3}q503V>(pW|3XOWyl)+5M?L!jsx4%$3O) z9m<&`bPi3{6q1iu{UdJ?dRu7`kaM5Aogt=(Kr05nz$9Rza=-}%wk`;dGUT2e6?wys zioB7|{f%SHsFiJN9Rum3o2Iv|zizrWQ+ZJ-@tvHJsk~>!^hKfUgD=vj^<SLB6Xv`MqYohdJn95)(UBl}UCl#3MEgkj44Vp&wP?PHGlpe#_dWahh;6PA8 zqe2}PnD$%$a0NcE_`_BSN!5~|u;G@T8MsA3CIhC&twG=8{3GUkoI2E1pMJ~!5-q%B z|JJ8hA?3u3nUS6uq8uT5f=OvkB$R~K2H{-;;f-trM4+ItiQNP+p~hBUTJMMA34j0{ z3VFu5pcy7In{;3q4ZN(Ai|i-Blu_^Rani%|kJv>7PsS>pYiTyCB3n@*O0f7W~F~*{$x~=!m6sOtXgfd1paYs;Ai7RxYr>rDB^hu z<=ccFFY>z%yCK{Ua+~A#W8S(FmDZ%FHk?_JB?DqTcn*6%-uiE9sm3;Co)IC9QGD*2Mb+)twt%J=8hJP}TvC0VWm(G z@?#Zju{WSk@ObnI0WVw3@{qmOFLg;>@XY*m7^Maoo>HV6#P$jVqw-^q=h+BK74fKk zH~%wAwb-Tkj7ok)#@|%Wm+GH<{4wYxzf*rxHrgn7aRpC+5yx6;x$ukk(xlfW`-I00 zRvx*u5W*nzGz#C~7fs%zIl|7)q`(H2;07jRvCNFj84z6B3jvZZ)~Buh#M|EMNgX-5 zskNz+yfVS*6~lmCt0lonIJJuWU87n?R2!SZWy7qgLz{w`TI2C_{(}n>_M|k&nW_XN zmgTP`%`O;p%K*Z|YfqR{eLqa(Y62PrgC0pZ>qQ%Uu5=ltp+s#c#1J9B4NlgP@NZIW zMWp_~h9+*q<6vY>u8Tv}DabSgYnVu3`yw2y(j$qRI*Uj@Ch1SIzbxByoYbp4H}Uu* zlU})Q%Thj%{bln0>o(zb;(aXXK0ChfT_$xg3w0B-N10G`v;TmC`3tblTCIsT^QV(= zj|KqpxEN<+{fUPr%{hX0ypN`6%v*h|4^t-Tw-z=uK?|2OMkOW@G2;Z~D9v^NBGL?l zI2Mj2zRo_mPO%8xIQeLi)tr>&EY2n;P253#VCd8e6~8m5VMj(k%`-ZI>aaUL-z&{84vs1eXl z+bt*JohH?KA(D?upU`1sNngIyI1mRJg6>%uauTiZupC(R{jhvNnEz_)ooL@ezM|$)L3WBzRk?; zN|W~U4u=ZOPIjVh(2j%i^Vx2Pn$G(-NtKEs6`nq)a?a`N<`h^@IDIGo^T`8VP;q5U zP0MnpNZn(9zBQnABwqsw3UuAr8r8<__GJs$h2A^Xf0v5dN#*Fxs-^|g^_QOf@Ad0X zJq4<-6n&@@6_ui6=ALD#Kf9{ooI-{3bU=Zu*_Va6$ibwzNiC{<>UZ?A)ul2|s@({v15jI3)+%I0?ZB`j;2 zsF8=3*oUg(nQZ*9S>hD@Sd=ImV$R@zh_)*iimyRfIn7}s7YC4PtlZpT{0cYg2;ZAd z_Cyll>tb@I#KD%ZE}+$O;zvt3>ies>y6}4( zjw?9MczYto4qYL!JhXI7Pyu0xd$ks|3cbC~G95h~Io3v9M<_$ZAPR?`gWwW$9>n#i zGzpb_{LH50Ed@!(6iC8m1{waNW1{J{!f^WV#=CeEA9+`#KE0yQmR{d_Ia|**u=Smj zqN(=M^3?LSHT+lnoi%M`!sA>$<9yc6+G`s^f(;=sY%xEyxI6$x1gEW_;Y>FFjF~e} zpE=W-K4zM`BFpBU)OgpHEq6txyKPxv_l(xdckJMIO*+Y2p5gG$9=&Gz^fhfWz4nYU z?~)nkFIvRE-Z0fW%;oZq7`AwE`3Nv24D(KDIFtX5PI)?|C;+Fm((;t`ThNV*j947S zJqQ2;45c8;Aq=XfQpxHnJSU1R0E+4chLXvT6lAeJR6cg|@3MBcBsq9jRzc`ju5h;M zVqX47mzo`R{i=)Q@^|S^tF`CwsN0w605S1j9I&%^G{hUEB?F)oGI zNGxW3>r*;nyNq-3qj}3Ig$r#TVk%TwOOmIeG#ZZ2;h@~4gid)DN;!90< zD=70Sk|}@II%%Llx?!DYiqbc-8l$axByuCp`;2i?k*oJ5Y?EPN+2{fYSd8x10sMup zRl4XHmdUdo{>{TolkA&_tND4;e@Q+Vs(I1~Z_+HwVq}_~fp?Z9(~>PmMN}hKT_u7~ z#1LW0`$Bg#gdB#IKyA|SLYzwEdm4m|n&3bumlb{kYL@eMB<4~? zUWq1NU+i@ui1gmo^yXG0nwSqugoqOHv4HFWk;Ex~WG-|$;~Rq&grhc3G&m#e2gix4(g&X1zat(&Vm`~$nyf{@UrK!U zKunOB1-%%`W<$~_#e_>HDU8C}SP)o4$sre8|8Py=#Cy&=>xyvM+*$mOKW{(dJOXR$ z;hVeNp1gNi`jnY@QLQZhhr7?X@1$V;#ZUkCQ_iK_7QFVTTMlkkX5+D8GUJ-pH(ELnD2~3hn3{ z6V;aCHh1+554>C1GdJ7V#(CgfKIEBEojQCXoA~Am@0gNsi>rC&%w|_hxMYlX1@oo5 zq;|gf>uJ7|GxPH^Pqw*iUuT!Lr)a5#b|luU7&cOBF6ZCjzqTv)Ns-|*R2Hew_oT|* zGo9MJjj8&cipa#_DOM?>?GBZ;gm*Gq=axt4egdXCAYrJalrGb%nB+CnEWY| z9S7|C30i8tv^{NPxjd|arAgZn9TjuA=}(Tw^9`{J&mMx*ST@A+G2;0qx4uX&<5Q01 zB9=`OR2kJO%xuY{_=DqI^qD}k;YC7wB4;b~S0lwsDDHR#h<|Bl6Es!io{`fPj^Z+J zNg?)f~4Uv|~H^jAn=fIi&mw4y?)Q-9IzmP9s@{-NY{(s^VH&eJE(V=$6n0ta>( zo(7fe1rLOQE-nW=p_y-eMa6*$bvT64V zu^01GCbMzjkUU&{b?BM2uD2ODpCZxg52~e7_W#Z-lCFEoZdwof1rPaW%%0-U%CEKe7%Gx zP3zU!n&FQB{N0jIiw4T9NA6L3lpcXc^J5KJ4!Bvx{ElN*q@HQ3r%hWe;g7y7^f^tb z7hhu4v_ls0O<1CbEq&TV$hH}li!7I0u0xhRq%CGnd}1OM_&Y)`i(inwA}j}6kBmD^ z!1zSW4%uYHsenE}zer7{pdCOajdTRoW%@~z#^^KZJ<~&N05!VEJ}w2*B610FGOC0J z*;s=lEqaBxE6I#FJw5VVf59{5GoV$LXrcN0=7+QrZDK{b@=SsM+>xn)T?Mw>^j(2e zW4GxJg?-*`n=VREYjm_-RM~!u(>?EUXMW4YE%}Nyw{P|>?UkRijom6(aBD?wsx|$A zZGp6jj^c79eV|bDdbPrVidvf_O-u`Hd!W#FNp5>V`X#=?_+Ba6=j>IqJ}tfN?#A^S zyPH(mxqRO4)YRQx$Lz^@P2C%hy@_XtoJeNmy%J{Gl)C|Bl$xv@d3bX|;MRzV3Nn~N zVhpLJp(7&yY(Ro*{x8w>>!W``5TB|;Djqd_( z-7AVmQly_}AF{khg-eWTCX3q8JE=X$$`O8)JWDyo>*1n53P}V z$8Q(}@W{$nC;WDCk6Ii1dgU~4%RQtW4hkMKDqbzcYg#LU4{IQwid^&a^R0aem)v(v zWS3+SRjY@T$1}^SE{NBy27M8<-P|2{u%Z949G{ZqN0z7HcN%4J!1jh+1_PcF5l~Fs zE0;1kg?aHv@hsoT&QmS9xd$zI*#X=0&)ZnlZPw8Te|Vd8J6XV5xzDLvoUQcX^A$*c zmi}B2dsyEg?UHusJ7687%fPOVUki36@g$hnp`wCyWrWB^;xF?LU8XmsKzckgQy*K9 z(rXK3Yt_k@7#*^1R4SB>){r#1z(+xKMJk|TavD{&b;4H(BsMimUU$6Z1-U~0>dm*#U9x-C}2)@VZeYvPUHS$j7ffi3~ za5}sGMt);q`t*pjq4K;+HETmCf0=E^jSp)U9CYDc1;>n-&40(_a;>)L&iPyBXSUv{ z^4s-(_Pd%!)|qWxzauL``$EVGQ<{A7_KNkJbBFh@sjTtR5q?%_Q(v zzU|z2;eufC%L6R*8~*kHD}3+bwng*iv0eO^>_Poyr#$ZJ*v4S-EAk2*m1fxe{8tyl zCPR)}v|9({*vM`>WGN1Ae9YO8|A2{lm%q(+=`X)r9F)h#Hag{#8lTgv+5K1XcgRSS z!Qt^s^f5nt9`zzuPq1_t{R+lnSsF}oD=-2{@m65yqhBOv`}z|H09kM=(RRGV zUZc*(!+8<=OG!xmTM&~<_wo})bf-?|`-1ZDVeDb{K@uyhkKhhCkUmWy{}Oc4{A_k> z5@!DwB53wj-qg`-FB_3JbCf!UuNRJXGpiKu4pVfzS0IGJ>k{CBAUZPnpKwOxmgI_Y0(iaZBaaw85;`3@ zpzwTJnKEB`8cB(=Pj7LBQMp3ed~gfhn02duDe6~*)j#WJdV@ANo$7{Zi{z zy4u0b(iRuXtza9ZOT4H;B&kwvhIENTLPZXJA7W7YK1Ucs^WUbvAdOe8PW>tTgXM_p zf}0thrF!x(!N&yZs**h* zH9Z#6F9Lxg+fQYy7I`wKw461u1)0*%i$gw@x45d^+P&T@)<6G1uKgE44=#S0IdJ46 zD*K5O87UEBsKe_a@HX(UppR3wfZ)&u7ESOrCVSb5q!GO{accyHj>}&`5FVwM+;GI?D#j7TlTez-GqNZ{kJo;hGg8OVkP!GJ(b_ z4OiobHL;Ev{y3;TaMB@6BwiYd6Qi@4>+q&TWC`DxWK>qK;|PK&rno!GyfxYrc88@ zO<~_4$l8W-Gn}7o7aK?}iUv=*sE(GXgO>bW4eaTW@AZiLDmh~Nv2Tl6l41y-b4TuY zn1Oc3>pbjz798b{1ackOfUq?v2-SF46Xe_IG4f(T<3+`6kGQhI0bVS4x{#w!*JGAL ztY5@75T{L%=w(3(EJaEo^k=q^ZBp#?xCaMZIB^%LTk?l|iIPrHTdYLh&H(f@>8pGo z%!mlXlVr#Tv6N#1!89+^|YN`yYDz2{@TtDUps34R8)I%0Z&+47n#`>8c&lK#! z$S5XOVS`~8ZD^k8ojb$GC@6mHzpof%dLPaG?39DZ%DgY=1{mNoY zYVhG;_+Wr1pm}inp|1gMgKCmT$pKL;BTvj}iR6``Oi9b`TUh$aa3vqu*L?;mW@9Mz zJMj-gVUyHySb_76`qP`7N6Q2mS(_5dZVo?r+9~o$^4VjbIj>`jJ|{uYf^z6)EpwuU zft22~i3Gjj+a$hxs&ipf^fQKN!o~u-y`eRXB9tQOHO&&tDdSPdg#LnXqrD9fDyd*J zKLTDPzT@Cj3Az@|X0Yl&adfZxLOddQh@dXO99FsBFH5G4D|YY6{|s^O6p znbRg}j*=9DSf!dy>J$1|xQZnFBWH>0NMqvuk+{P{_QBsl-j#&EV~ezzyeoKh#yHm+ z!QWI2{MQIb#YvV%;hrXi!!`C zAjN<%ssT#{;mDwmkJ1LIk6Vh370BDLSbH(%Kwm8Xv67!Dzg*1 zD2|~9{>KUp-9YPNPNIxIO}q^BU77)8vtX2LSg?3p=+$5(r;G)f5Jbl}=*npX0F?|J zA8;~6@NGEsPCd(U??zs|@m|MSr;^ZS(^EFgWcz1sNI7%xc6Oeif&*!v_*T29i;|NA zFMRM98*aE^gHl9`-)PN&*wVgy3#OH5pG zj5iu;7*1knLi0{4u51)rq77ctjzYgoR7l>^5ktbaD7j>e$#8R0$z-a&gh7hoMWQrb z{5W>I;QsC!DtQbes}9-gi;@c>$N3`$>zEMhybjj$zbC&f?#sk0Fvj1uU98su>B^r# z9*=2+_dJBBBwOyV?A8`)^AQyiK6}C@()diwLm{MIlAkz&Cq$2jo?HAe(Q|A-`w9~d zPw0Efdg+N^TH%yx{(fhx{(5UCf4_Q4VOmgnqH1fq-j(_|_7tfi#a^1tIvi;! zdYhWTcS)~jmzHMh75LN&W3Lr;PL1pv)!8{}Uu0@$ksPku8hb4*s}$g{l=O6z0nIMW zN|VF3gB_)HrT7QCEx{-1Ar5iq7F4MYxwIij6q3LIZ!iq3$#?FMzScoLyQd!@em&Ulo=oGDQuMN_w{SIY>49V@m|f z3{{aD{HTl6nr zVDNIVjIo=g105t@=|k%<7xVM45QVFHq1X(5go=V;k-Z@Me9j#$E&3Zc@3l^x9%sKB zr)Pg&t504B9x|ych}`v0`1jw(`8#j{I{))zj*!g(ybSR?!uRY$rU&@?%*T)jO{m5= zMb0=ZWJ+;Hf;q-(q)|D$>ijD9U{zJ@2a`yYrc3p<_(SXc_WjffM!! zFTUtKhx_#Li<0+!RMZcr1@Q}tn}CaKAL{?1@4dzU zN=wpREQ)uG50c4vRaB5-ACfg|-yAv+OGjfl68-eB`-s~e^m?L(Jt9Ku#Z*assIDQ< z6o=o3A`*e#LYXE~y61>;7`Q7ZyED;vw87*~iamC;+emJ|lM^}Q3-Jnw=5aW~;0ei9 z5Y49`^4$cUmN5Ofh;G$EPFYA2#eIgN#7Cx~)CiO0SVYCeWQZjnCoO<2Wn?ssB_etZ zfuq(XOJ2+W8YBxWag;h1zA+M8PMjhb?iGTK<6Yq&HyIf51?$S(>rRYQ&9WP;dmUC;+qP)u1?nz9DZq1lVkV)l%Qk;ufzi{eugpA5ev6QgR!`dP8pX0KZ}TMo}!-&2|$ zdyV#&!`Y>=*Nu6M-7R*d6GSF5ZJ=$6g7U|I$ZErr-20R3*i}hM9$jhJ{Gf^oeZmR(K zb+mLca#!Fl@3^CWl`k@eUJw93)a$&kP1)L)_WwUTKJB4D)am0rJwm?}_P@cE z1DBoQsfio;2?j~B`h)KmKjf%ZmU~iO)JI}dg;ik6n<-i3w?LK?m%yPN1b$&s%{^$m2Y<8V) zTee{t|IO-v`Gw zvm2zAY*h1_d*X%@S1854i%#qeomsXDS(%6Vy#lxkx`)D2R|8e%BPcMrB-B!}o?%BH z$*C<5c?U`<4lVaxxBn-o&^L)$MPPJr>90hhS=~?w{GsWGMWZeIS@}=)U$=QFA0*?I zStl8l{iIhO>M>&DbSMX)C=od)u7G&WHz#DXQ327={^o%%z*3x~iNl&u!6Ai*9YWHe zwV~0$v}l7CQe7aMae0ftLxc5=RD%XyNpc&w%4nmN4rxV_CjHKIX+-4Vk5)6~_RnhF zeCg`bstN*s;DfzMb}4cN_IvnH5_eSKor%)KE9B3v0}5 zNoY5C>x~Tpu<4^ZK7`*z<~gc+p>hq);of>wuc0(dcv-A!rXNf`2nAvg1mQUH3Jhov z+i@6K%#BnTjdIgb*{xJX>AoJ+h^%;Zr}oov+gt0$^OyJKHf`IQdlRs4GPrrzuH3EL znsWE?m&ez)ZXfqkjo)NP1f(`Mr0q)&G&QI1^0aq&Zc1-%3Z(DTLbW0$I&@?C0*x_k(rmm)Ki!alHp? zY96#_(sN|staw}-tBs`^aTckX6+@p%Sh&U{P96l}AW$;1nWUz(;!+9yjp7uV>JLgv zaVt}QUcTdJ{P$N~`_~RTn;|9)N;|H|U&f^CZey*V++DfgQT`kLJ^th;cW0(qfeFZ! zl+=tJKZ86e_~cgk75bc!k+I_pH@R3${*VKwUFORiIuPezIk!z)=*yKtzKK(=!V$PE z{Af>0&uPub_YR?|vxOB+$G1g<9R3E&XB14VVi>8`Yuqwj2(>4~2I_05<1g6?d1X-_mdqq}9I)vZoyZf`rcYSRsTXNnTAr7p;8QCw2M+fh6< z=dYKa(lEUvD=4GthT<;ETxAO=sev+EQ?puxLLM6AOEOm<`ATPq+LA|)U=n@4;KL6^ zAW0JOOK>U)d(ulbnHdLHOv+2-)0cw4f!Irc^gdb?Q|c+s*PlWF^G=qLQJ!7e(q_ak zmB~VrH`zh>5Nl=Tepc}c>YeUCS`0JXLIKP*QlwTjBA9!GAC(KBzwlR`n;^(zb9U^1 zg;ng|Pko$xbpzVJaV^9|T#;<-A|Uk1mrUHgaD}5TtqWBQU8M!&wt`DnQ_a}4hER1u zBls7Md2c?-+lg$E?mc`D7-IO>z0G^GZzQlRYj6K_UH3N82YfgI-m{_#s--cruc_Pol=Q|f%5aTT< ztH(%yKuZ#eH{=NYgruS3z{RqJnWV7+Umj~FNfeSs*cDTe$o!|MDV8*~gx-trSP6@X z>?aXBk-j}Uyt1}zMr&1SurnAPzi?0W@^CP3)wr=YP2_LA_VZL}-o#5DwY~5>bDz-N zRqe=Fk-MgTLd9vNrx&M(D_X0w@>cpvFIc(K`Uhe@79yESBLZYkkS?t(W6C}4k2lp6 zR$Z_ln2}vLX5PKi=giogA4;uxlC1?h*V6glD4Wh7;E(g$`5oUMT~l*baq+#8vW#r6 zqvX_^>Q@xh6n!mnPjN~?mBzcc@4H1!h8$7fqWEJe(99Bha1EP zaj*9T;XIp>#1E-OkK$m?O?Hr*hsSO+klN1?i8o@;_@Xc}v>IujWuLW8Gi@>!br;c} zw6CW}vh?(5ZH8$Eoid&?Q-atcVU5uzjgm{%Ft=4L@5t^)sRdy5Npxyqs>PpyVmbY8 z9~J*Dv^u^vWzHs5N=h*3+9PFuHp4)RZeN4lanPg_K#T*Cs@Dng4Yk|{&flZ=fp8EU z@u=_9A8z5jA{c`EGr1-VoQ}nmCt{SKV52$E2-j8*w;)wdMrDkI)x)Azl!$K*?>V3M z8i7_qs{00b@A-SM{u;y7Q3nuD(ffM#Pytnp_5M9QBB(MDFTRbgVX-Cf2SYKUM(7>z zXq&#>c&5P7%XnlE(nbV7O>hGFLJR{Kasv;arYh+Z5yTNe8x~E91c~Jq1`JS4Fmq7R zw1XIv1kN4=##;d(Pe$km)jfV8PSO3DW%kL?p*wku7`AZ?oLUCGHr=Cy@EnMcRV#dI zUi1)H3vp;g16(jzimxIlO8lZK5dvAF{PkV2J~%PZuMJ<_0QG943IbKkl;((n!c@iR5U}3B zZX@s{`^bPc&FDU8B8dVB#$036x1}C8!?FfXQ+U8JB^%`Rm~N9GE*Q=tq9S^lq5Qf- zMxE%PUYA@6O2!lp1(>LQ$c+G2d?P4t@P-eeW2%~xz%VF_-W-OL?87nDyvLW%rhxQ) zSO>C*3Y;{dlc%of#5KU}qLL27Cu7;~w zp0Zq{Ezw3=QqZ6Cz_VBb|HH+2dLn{A(qkxL$4^!OBpxawo3R+wgf-WoSV=Y$$`2_x zWvC`WAEe;W|FjOE5YmW>Q8|~map8=}gwE91KnwUk12pr-QYP)^kxEJx`8|=ent5j8g^Yv9}^`Z0HUwC)dxSDj(#=#VKFDsn2W&UnVYoulZA%JmdHLeg4B+);u+K*UZL^bGO9=qciDU^eN9+HC6ATGf90@SK;=75iPX*SnbxvoAtqsa0*R|B+Xcl3#<}ci~?b1!Y}A5ixY4c zdNo2YSlGF%P?%F%vN+ngD)tC4Ub0J`(Bprb-_ygt&DN1}VO|~)hSh93?Y9c-w|t3S z?vN=H_@`^xJZAB)c&m5DWhH*fb$}fYNI)QWX_gpZ!dMsIymPgn&l4Ga`ulC< zoYRrVXZRHe15{<|i-t5b%c1%*BrTzS8BYg(G!IDs4RA<2-mRLRX-2e;L!Q(3_6f&^ z2*OkZfAFgzjaN46&VC}4ALEX0kAp#Of0tjd2Is+06>*zKI@t+7xh4TA`HraT<2DpW z+439kPUol=k)cKyCZnjR2eAOuqari5RDy<&KXM!tK!RtQURE~KTIBQjl@|7dG=e?R zLTYZ2weZLM7yMEFiyxzKX!-vA{Ac?OPq@4UyW#-2S--}w;J?PMIN&y>U2_fpw159~ zJNCop6BcfEde5@d2EK%S>OSaLKFmq5IzZonipE85@@D`P=OV9Di4Oz12$^(~*w>i$ zi(7hb`Gqu*fBf*nEd61=uVHG>PIVvu^appu{QASvM1Sm#A6)XVvH7f?sST7rY>FW9kfnwon$S2gx(XWs|CTWFs-;F!^AIQyeT;f}yd0hU6TJ)JP-`8+p;&6Y zUFyr!N+wkt?5~(qqGd{bxog&pptn&G%ocN%`ru$Rd-(8dWgsVWFj_iiY(>S`Ii<=# zU0Ip_doGMhLeB>E?ZOU9_MHTQ5o`eO!c0655h65tK1Es~8pu)+EZs$g!(g@;dNkQ4 zL(9n#Bx}-o7M)hFY0B4VWDZ`TX-hA;WNC9(e!8uASbE9CdiJ{{Gz>qW2qQsQ#QZ+w zJH~&q$%(~a(E1;%r{SJ2dHj;b8qcsSn`fl)5I7Ay{G+;3_yS~=zHmbrOI>&ueQH1A zH-ten@pN$IL(pr(osv)^3P_K@%vbL&i@yy}ZWfl6xP|5bZx;S{! zl75?=&EIn}$!7P|x4p)Q*livnOUO~Oum|t8TWcEkusioO)>!SltLF3?y-VuD zm(-`;T3*gOoOwa#cVJY~S<)QC3Qs$&aF`>_=JB!qjl5JqqM``z#>&fI6aFUZMZ=h1x{xk^99oL-PX!jOF=(vF}7;k{6lf&@N_V2Q|uDA-N@ zYBGfyfjT$mIaS27G9z(3O#CGY%s25cPmlsW$GhBan?7a4IA7KvZvgM2p4&(N} zAPO7;rSKUP4Ow`vc#p};#=X{BPEudSy^=kcgSgxz(qiK6kK=ig`zY$aGnRd!4RwFCPUc z=_qfx%jx}i!!G$-2+0lBZSLro*!8g&<0-c$_;5hjnDR45C+$K~F2Z@6?@d2V5Ya#P zoXuHUZaXdg-p$C=9Y98|VxhbSm?(7bcvA3vt;M~2!LlzbJ7bEZ+oRqI-sBJ!{0uQc z+Tl}r^Yh&N9aiAZ%OC7hv_l?`<~L@ zC0LyqO0j9Y;h7{1y4a7%tV3C8CQ57Zb3}P*m~Hap3bNjSnYb&yKc&NC746N>q5EPN zioJ0Lt@s!lVjmstqI=1XJjzwY`47LO6TZkH-wx@OG}ocn?~v_I(=%aJXow}$2;?Nw zDj-H)2Q2KWD%17^Fh#BlH^+-g$nW#B^L>HXWyHUq+4mi`#?ItKU*zO zszu(9xKa(gBVpiot`ofq_L+E{5MLSs&)$+!&FV$y85~lhX_Q6b#%h2o&Swp|sp_vc z!!K;%Mln;XWHe*0qnQq00`4Qe#UwB0H;BMi)SK}cc5@VfADBO*5S+v)CYZe+t|HoT zG^?i=tzU*z@&!qGy%R3cM~WK0t@kuG>gRuJL}+hnN=nw97=@M~JHWd@dSYSs%$v*Z zjlh)87zJYgJl_qACDkf9q?P=?Rywrwlogj=veK5q?2EregN-r+5GbuGAW5R;@69O(&TU2ukvlXod$VXTJ%WBk0!1~Km*yIPD&IkGT z(}uCFC?BTQu$z3rm@mj*dH(q>e%}_qe~W&lEHO_gXD-V_vh3MXiY8}BXP)Uv^`xG8 zW{O?%oQ+O5H&EzO5FdACN*)xeOHIuWwzuaLW+)O1h0wr^l0bWVFh4a_WiF4DN$4-K zub&ls+Lh3(;O{l^xlAEIdY+hvpn4XO`bp$tE6}mIdgOp1Ghzc$b#{c zbtpjrT~OcKO-`h4KA``A`R&8=UK3;EIgxun72_$w7OzrN1a}27op1A(O6){!vRz^qyeXl=SBfW#lIo)3`)O`5eoMx8 zvU9#uJ5kN^qki9}^vk|gUPLBI?DtI0D@BzjCg+x-f@YohZLCGZ(KTO(O@}fP(81wh zguqQG*;e#q;ZUM@B>)HL(pV$(K!2vi=;IhA0*tAcl=y7=uTDOBeCYC(FX{jN;>s&R z<5#Xc;2D;dn&x?G!ThEscFL~~?0K-cY5szzy=iHw!$64MCzLnRyzl$^e|iZFR6uWBvF*Ag#4Fy{Z5lKGS@j%ak`UWx|awuO&OUr8D-2#`&oumG6yAov#jTSafn(ZfagwVIv)L>yQ+@&=^I= zCy0P7b#omf;mbRD(FVO2!%_GJNKW=A<($a22@U)J3pPwh(zOaP=IX_)O)Tz^5~LUr z#H)X$tP{Jvpiq&OrSjg`D>*0wc9P6^KeNtkbW-~38pV3qZ?|?o zJaJ0LFVFdiE%&me1VqYOpnQ7P+JiI?9~7z*D|Vl}ZQIFiS#dNM+3oML_4N}v9K;GO1M4(pS2#NW*gBZ zX2~gZf{BiAu-n(oZmC$VX1O1|KD{t~&WUIQa?^j9ns*+xepq(#;f>#?c%3Qm#I4w7 zwU&*l9(m#8fbe(%H`?v<%TQN4;SFyFRFCpq`N&!Z&>Par{_?AkNERL*Svg93UyT4bmEOC9KD1)C0Ckvblh~b$k69KCKA%ddXJY`)TiYS`eq5r6v*88!s`@!%vafs9;% zD_~d?&OOvxtF>i2>r~3okT?lLZbm-hcF0KGl9#*6ir1WN+3X>T|hbx zu_%&Yk*^s$cZJQVEuDC2ijoZ#N|CRcxKwl6md_nK=cW-azf!kj2H$ai;nLybN-7mV zbqjW+HP=_>H(y#1mK{n}$yi6#g8T>MyBR7r@jd}gbs~-iwN0ACKf+7Y!mf-9JT5#Pg=@K#01aOT8CNna6hPNSw|7^vtWD_?|rkIdLf z_EaGTe$*)8AYy0<$CR~?B7+7eAG`s?#z#SNqz(*1DT6weWKUEfcMLic^9f;)2sp=` zwwT~Vn@mmJ^wkD1#+sRX7})7xAYytG)Dee(tsZjNZs{=n#oa@$+Lmy?0@V%Cdc}eu z2(%590hGFi+_lt^@j9SscO`jgd3U%)f;*uF4-5Uy9RL4ltoT1uJL0u9sLI+us6YKP z^yzbD?7Hlvg&0?RU zum6j3Zrd!tplQTuYvjvLKz98?WXg79REc1s5$};qeLCSokI$&)KEh0k+h|5Io;kWi zM3f@oA({N>FiK31x>zcM&LsDl1#k27=L#3N=~y4^-G80bf8Bl&r9vzfJT*GFn($?; zu>yLf*R9^E^!E#BR0*)>(H--lQDLUu-OPr);wPhQ{az+vH1qns!k-@$`sCq zpAdRAkLrEGnO|pob|E62%u7z(nv_#a{x^e-U>u&)VN-Y{lhk9HyCqEDFu`!UZW*`f zgfd?InOUfP!r&~da4XT=SG0eb*^@r@RQ#z?gcAoYrrdkS@# z!o}@E@EhXFtrDikH znaQ~ihMWE4tJ%GFJ6D~4y_%#l6XZ->NN8!l-RQw9n_pm7++Ygx$nLRJoe&EVRPe_MYH z7~iREOkr%nfk>Mp=fZh#qOe=xBAIt#j$>>zJtuY-yX=yR$r<{F)$Om}Sh!^(TQu+D z?DknJn`-P=EyLb?3HDxgmA$43If)d+i`hlyk@cH3O?;rc1%8ur0vXenZ&7Og1JSDmxqQvXPnH)`UtS+T&@_V3m&^qqMAhR)6n=bz|XI1~9xB8rGl zde3qq&Q9D7#dtcDW0e$Lr-%i;RjAe;GpH$_iO_lO;o)?--4G&XDE-$6n%4~l(WO7o z3o`tw{Gb^=hY!GFxmDe*eFKs6HI{2}ZAuR#qpPTxz6>0iA1}vO%|OP6|H(jWY_LWZ z;cYj(07TVBuB`@g0w5g-Uu60rQj5qmnQ=8#s+DR7lHGs^A34eh{N`l&GL?sq!bgv{ z+<<-I%tQyktKSk`+;-|l{)mtNU{hyVK($v)yVUBkYFXO#i;c-)m2zk@8A((K~jT=~t;7Cx6fmSSJC!LKZI%&c>~)Vw-Rlk%F~H`!92FO=kS zI(z-6batG4aAd&`S_;zTn_bSE77b66Jmo1b6*}cbuGSXylZ-sMtwk%W7_q3jz~MyM zYP;Xzmee#?4*b78Omc)W3c{m0>x*P(L+I3fk)G?CR+1^fgxNN>Az4qbsX4wsPs_)0d?4w!%{T^n6!d>Kt3jnwArM zuzZ8of)qBZWsOU!Qt~oV|1e+lmk%r-XdcZ2R4i4twwPkT@YOWK7YySVX13t*pfDw* zBRS#X^3jwN2Hg(jxhrvI&N)I1_Bx$H2#)1Hn)d26$un)5{s9*7F-@+k>Z#H{tg4dS zSinb>f+;SJ16sXPXk%b1ata%FoK%%`EKc<5H1>QI9iMa(l5_w2v0w?YsK>N%T8pJs z9UOAdY0O%;< z2fQ~0M>DNZT1N|JQ~}gjMVeNm)nP;Gf!Cs${_*GB@a*Zg(n(t;-_Ja^p49Ejlk$Bl z=03sS#!1by7iITuFhU7hQ3~rt!+1ANk-~e2*7o5#!ge+>WBZwo2tvb&peU{fk7yAl z<2YL6@uJKJm@O|a*EXMZ7TWDu{8P)Jg3MBb?qJ&;URn0M$q!@|XcM3D3`<{&Nrz82 z0l(6+s(gw@W7P-Xj!38VRy7;2*>FYNDNYq4C5x=C!inz7*j;K`s9w&%y0RJ=M`ZbT=WJ# znS(fc(Dw$rD5a6#CZEMGIPVEIoL2FN8ko=p^g%6JQFw4V9t!?h_Y_tfL>VTXLWP96 zN%9F&ep5YK7LTJ2W#0=9uXuKoRfGo67zzhdTOk>NKRn*trU+#<(L+Hzl@EaQ6p`TL z%xzjshahY$SX05FDoBHD&@!Xyeys`ksxst{)Qb@}RUiau@ZjN@>Lj(5vUeMyaUug+ z%$G18!pNKo4F{^26;b^SdfVs$MR3Q$4x(h65 ztyo%6)LA4gD7H0Jrs@~#PskmtLw_OzFZfY8O^#Xc@#uhi_(Y|D;&A0nOn=s$>f&2JdXdfb8<2z zEVEy;UuWwt8zx*I@6z$7{>YIZi)Oocuh+}w@_Ri_N4`9e?PA?tFTa<~_3&{l7`vYW zgYs;CK!1@wWB1b-i*vb9?e`M2DkPLm)Q3g|8sShjAO-X~{2>GmL?4n*VYO0j*}Y}C zI(*s$^a8_iUE8Aflsf)6B&xxG*mZ~UDV;#rRSE2X;R7M_iJohHqc2o{Se#4XHH1LH zR?IlCIcQRlx)3VpqJV+k7jEg6x?nzO)|2M65XS;3~e%T zkcZt%ayAlp>}im~h#(&h96991L1=;qLalw&Om+hti*uU8Y<*rSpB`r(X{>3EedmbV zmAVsINc;fqKp3|K%{3N~+kxAuX$-fFr2Hb0v8BK71lA_pA_$t|&mneyBH>5?0f$b3 zbKy~7e8lrB^TA0y9?u~Xlzc8Sl3++_iIF^sQ# zDFjoB($=db#d7YiZ(IJWRI9wqv9#5(XY@&4Wtr9fdYZEG)?c#e{KNuTE6+~5KW#*Y zeE$plO{bLdgItHTguPmBbp)QAbLnGR^u%3noyiZqmSVT@wav4vlSew()t>YhKF-S6 z^lIOtg9DeQ_zPra9iD1k#1B4>T>9tl%PDo;c)PR6zxUOZ-@6cSY{+!hp;^@3kVk}W z1rMqSzZ%M$aNL942(uHbRJFgc zC2f0JN8{l4Jt@n+zH>EmM=EY*AFSRv$l4aE7p1i~Djx7MGmrg2yZh1YN-#^A;>}gI zEBRSCB-dK$UfHZi8zxF=SFfP-%KWo&4gefuU5;$&yA2cD7NuU4%17zZtdcB`G=NWw zm3&$7E0I5w_+_KQ(__x(G)X1XG#*w-FYeJaN3(H<;(l=F;5&xs5wksgVj4;O8d?el z-!~KyLn(nOWauFHR81gy%<#!~qle<3EGnwUORpbR`USo!%X7erje@ix4~7EL7@r$J zvocG7Cf4GT2I576X){!3Mag9qY_T_>Pw;s32>~w(nt>O7yYR$q7w}o)z|_LeM&vv% zT%C9GKLb4lvT`t(=gE<0J^C);U)wGqvY?KnF=X;7S}?=Z2%J2{QT$M%54Am;(f>hm&Zx${{lRi|T^mVYbtjO-+S?L{dNkUPH24GEv7;2Oz=j59r5P_;lG zL_-()Nl`s4IVvp*HwD&Fg-NeLGNjn^H-4j*-Bg>=GK$|=@4GhRv@#?{-8Y5J;eRR1 zjIwnN+3bOmQ~VE9F5*{9ORp_nz*f|_OVlSuwPe)t<+b0~l>cCM1HU@zFJo!qIF?p+ zTE?}$dN!-XUBho&P<*Za9kzB+-a}f@K0Cuj2V69M@S4|$Y?o#!yUJ8I3K_YLOz4Ynm*`|k8Zx|#pU0h^c{3} z(wC5P6-CaKQTy$Sj^5)Nr^nlakCZ2bi}->qLL8T*i7>^FSBiv%v_0NNo-E*b2EEAN zLH?1oX~;j=LWcZnX2;v|6@`!zkYn^k(y#D%Z{lde@5{?_+>0su{wMdc)x3$s_m{Yt zzstRRiT7eoOY}wWwx1DFpPl&HIwRlo{}D?hq8pg{f&ngQz2iG(%<&f-#6O#?5(h#R=-5(U@&<4l2tWPcH@l0g;y| zrEAoyUC_x(tQBd#v@MsN?9{vcd3kR9^JO>%$F)iYzZx1TNw7iC_eT7l5@UkKybG_`XC~-Iav=ipXH3v?--Oy!HN~E% z!l^w)W`%()Kw4DUYE!a;OWw+?&dBheG4Z7Jwv_f&y)DO8r|hF*UPry4RSS zE&kkGznqO(6z_@8ZfvBfS*coheT7Z7s%iEVyY{^Y0!54!WJ!_oGH-UsK4MiYc{`P6PdHl{`e3Bj>INBs__z}K(KB>FEdOlzEpRdN3unoSFKVQk` zujVg;j3LzZ)%^bNuK$3WkoAGRG74rXidhKv7o6Y%k!4nZCbU#g7FD3(wGbp00yibm zv2q>4kA(Y5K3aqW43rnb&T)Jf)v9LsKEX-N517={IJIISRF;9~qVO{npC%At{7S$( zAgR9#bvTXsSby_Mr10i~dF{SRb0B1EGj735|pD2_TB_Os`A_)Kkqqb&Y4V-Su>M;vQI(+fn>4(31L_a$i9UoEP}uU zK?N$C3uRPPTR`a;w`ri!zKqyRH&oB|3SNVD4zGoIOXMXC@vr%bnqwSr2}+e7-K&))RahkR_^ z?ZO?&+;8z5u_Q<3Pq3$djj}W+RFIi6P|Dt7www@rjQmP|9A8M>gcA~mKxQ@u%k7??*%BA5+CzS=x%)}^y%$q&=#r9_Y@$?`R*&vt|IJ)ll#i~~6&=n>09 zg-H7_OZhc66w1~aY!wYfb~pIkvWG3@r{7T@g$ixwgzH*jo-_wd5LAiUx9zORUmGu7XO(eeH z4;-a&ToYv=A_twW;r9&UI|V!oV~>F6`dQJ`1RFGYECiNX!01dIq9BL@J^Fwsd#Hdz zgF$>C#Q~Xu9Y7@tvUCOTZ^G1tnIv%CDd4>z)(eb&LO*(nSR<%`Aqg7278sZi+63N3 zxuFqb#`3?q3i5;LWr9T(s8iw&MuH=92>v6lCfoRq$lyY3>c1&REbv&z1$P(afELBu zKmPD_4L-C%5ms%)QUqi$5TCuO$lmLx+`f~5bAq>Vphu|_G+i}1aBLhUUO_g4EL4Jk zwr%gYR4{U-2lGWc2hq-N0LPXk5MYF_wvZ15KSHVo0jq(~L$PzDW;BqCi70)Ee!|wZ zCo<4EnvGVXBbH3OZJ;wXRf(qO=4b+9X>uB=ANEIQPOXyXhW3Qy^K(+C<{8iEdAjjr ziqoxjxt!*^KK&lzzzf`p4ON+l8CpE5FGO&_0Pd%*9B*CRLtk{_k`YQ2H!;u|*>hWZ zPHwsqO-OQaT)sK(p``RVolHNZ1B0B*q@r(#u>r)x4zdbTw5o{)lCK)4K6TPv`c1=QJ;wg(gzFs6b$+az$JR5B3L zLq@n*Z9|x-bi81$hnJDEU8UQ; zyKddy4kq6$lJrly_>auvm?~fQ=~<5CJbt8h%Si$XJs_`H72}8ap;)jmy|P$KFDcv0 zZe};{?bvJ=kfW2X%D$6t6MpakCa+!B_n|RY9myTa3AborV=qTF6Z&sf%b#kz5q7n(PuEeE{ObpzoQ=tNs z6f_mt3oQz0a}=Z{K$;4`5!g%3EC+>ZSLGCpFDp}qS2hF?^o+062T;rk{hxhTB+qEQ ze>@akK5lApp%NN)-n8m4Nxg9L79toTSm~kuMI!6QY(vW5Bc5-w4U0;M#RrBaIuq^$ zu;qFM9Sxm|BtRHl2HnQ=RwL`c34ZqeE{8X(0GtkpnFft#!N$mM)?J}=u;idAvQu5D ztVc^#({)!_I=>lFRjn70P#lK%sy`?gBc9cMg|ooY#2k?F6vSK1 zKlGtS6g!_ayjeK#^TOB8t(d)f+@B39#_{v!jrW&m+5W5&{FgOk+yC;oGr!OKnD+fl zw}(fcxZwu1@GMC-#xKq+K1A<&kZa2W=J9g$Mu~~(R5%tO*Gc3znnd%+jj2tl{sW!? zF!XCtvnH{!ar_8&$A}$X-WgfaOY{0bFDkGSEfFdQBa?g{zmkFGkw{cW@J%Bkys~Y3 z54e~kEQ+)P<+xejM8?F-t`jkKE03T~iVy-pB}0BZHz8> zCKaSq3}!IAZ%X$RSmi<5Dv)J2prQoq1dKM3OF)SP@x-QZoqfj({M4;?^1pX~Wf`(K zXR!u*s>6Qg!@0w9&fttx3lnJod_IY_V|E41RrX^9i(Gd9w2Lqw9^87rnfmld&am8@ z`;M(A#-ux+dpO;d!e5=G$g(;wr7nCHLdnmbPUI@cK9a&m?I~$?u#(!3(xsMNG%bfl zZ_d_(Xd%Qbfp6{7yYL>vRQr&5kO4KQB7m%pN@e6T))>gtwUHq56e%JNlGRMa=OQS(J)A(-`GwB9iz_OCZ_y?^Tg2&#*w-M^IX{HWPTM_qtc96-iKwm@K`8bRa8_DBI=x~Rl|Lz-P>cN-T% zM^Tn1+E`vs&wXI4`P|bkT1L%$6m&7}G&>r_?9)f2LwcLONSZ3`mA)<=KucH_A7%mw zF)?SWnLtC!&*>bQoC^YM5mJ4tTI#sQo zdQUN;a^PstGIadY{KbAv_hS;qdB@9WY>^mmnx{m2P(<4?{XpKkwZf1)&TpXmVM^Y8 zsk{Gpi$`&4o>4yih>dBeo=L@BPz6=|r($Y7;;Z-3AuTFsJ#C?n^dn3?%Ym|*II#aX z9Y_`}E}%A97cjQ4yq90*zj~QH@zl8z0ozIL}C*ovv|2!>d<8cgt)t z)6yJF2D>c&Beq3+Fik$}^Wk8G-k<+mj#!{F#!i2P&7Z&Gp~+sryp5Nskl zrl=VG)M6Nc;7(*=3I0+a#)qaOZ|9f@lnA?e_;g?M6YKX^k69AwEpSb9 z74Ti+3#G_lW4!OqGX6%-@x(aawBLU1XGQg+au2MJ9ld&uOS|EzOP^O$64K&qNm+Wh zx*Ulbd{=os8y9KM+nb-aFAp`xaPqMbMbt!>myh5bPu-xo)?9rww*ElwsQRLxeGNrB zjX*+noFz?*?8zEoPv(jYZpvp$5WZsETEI1$C_rCYNIYLuVjIc?#3!uHQfLTe#l-YQ zeBEjnC|Drx#S11oq{k1|idv@LeJy(wIwL8B?f1Xd$&I`3W?z4S{|UOvRm{czq&`W< zLOC1!wJhDujn3EZ=k1p6$R54+Zj8EGri|i$df`R<*o7a%PC^bkL!B6{3qB$rygyYK zA25~U+G0ae-%rjyqJCnI5~#hVW2rX&8@57_%=VBDXZ3#ETwKP&8#925QujUfsa3{wouS6j$mf>0-CQ9y83@TM}1V-427sd)PgMjIbsx9+D{1; z)PZ?xdKt*IA-L&HvV0&L&Au@xgWv>dveb637*6DSk6SKcN@{ed1( zzzQBmvv7U5gIJ@k7gf2E*!GdUX>sgV83QSKGRmtVZCH@)bI>#HXu|YytJ#@Hof5RER4m@K*yDK*Y&Tydc;6l)`!>#{nFg+iB(5tW zgMiQ?V;q)>sf{SvIXQCn>N`>gULSZp^^VoHbR#74v)<+-TrOq!wJUi6`^Cy@(GFm( zLM}PgC2)Z~HD7QUNqCtwOOVPdW)m0=lrkGOxfEy~!K<`k4K2pPW~B}D!NL2wT@FX( zFluWUy$ppfjSi!Cdz-DxETci6sG~dUwE2!&CYAT^FBN&Q+8 z@=l@qsx0U)qt2_ot3}bp(SYI(#D~D7!O2tQ$5QhnbDt*F5 zg^Pj6fvk7!T?2ePatFO8;{_Q6J(Yc9ub^SWmJo99P{%AROIvHzb)`yEweixF z5uiO;h|<3Oo$R7nTeO!aG!7eBnZI~KJ-?s?N3cHS!M-SaXo;{V%zBuO;JJhi1~Wgw zLF9zN?<99CTCujp2BlUry-PMEIcH*i)QR^Sds($o4zwC+&h-3)JVuS$BJ@PjU$?ew zTd{8Hnz40iwfb1sj!F6MV9a=UD>Y91wA!K`G#I*fm1`@@EEroozhRUTu3r|OP@IJq z>d$YgURYnNglm`8OeoG}+t8prmGc%g=i2l1=_@Cp1E7Gx@HuTwxvsoTSEyU5Gth|X zp^MPGI{-gPuRaAa5I7N#vrKgh(Fc-ch{yDjKqCui43sZUmI+=geEDtQe?fDyj1m_s zgIy4}#_B!VmVxEU5B)(?>&wZHMwPA6@8;)g{(jc7cYDh?@7pK$o`e$q)Lm~gb_e?{ zW7i_3JgvF^Rg;~>c9Q>vW@jP%jxWP+rBgV|g&85<=VelfcjGt*w7c;*MH_votPe)6 z{L5*z6{C$u4KecMP8u_+bVLH*son-}doeVmY3KzeDGf=;asnTKEeVo1Av7`8MIvs} zyaFO0)0BeBMC4&`I3ll|X>X31a$^6$;g;1IN0r5^ZsZ@&*|cd+|B>07H_ukv`i~Ix zm)eF;V=DFX3loUE-QkaMn zqaLnxeWX{+Ohxw#POs0VnHKhm5S|@==7%4kp30E99XWpaLHBJhy|Ism(fa*2taeh5 zg^(e!Lq&q+4xyG){ccg5Qph{MS*1)6#^5xXK`7tCpLsZrwT#DeiiKmR=~vxhuR?dqLrsakI! z<;qRo!~$qu9EYy zk-TtlFA-P2S&dz?d-gmpO}=nyPh@|w7P35gmO7N<4W zlh0m6KohL)CB)Bgy24Y2XG@ldRz=n5Mbr9wsQf8A#%F9jpukT`dJU0ewcCzvJ$P^{ zpUIBW0Y=BP_0fY{V^Dg`A%sJyZ5c|1&exvMo)9cRRy@kmHwJ@ImcAh(QEME5mh3b+ zTh1oN4dX*OJ7DvOZN`Vjhk}IzTWOnieHwkyA%e97yXmL2O~xU#jZQ<`{q;DJ?=&(FZ@|b46QRA@kxH=ho6g6;ol>$GZvKiE@XCQU(M#b-TaBw{8Rqv zh1YoHTAC>6UX$&%0$8w9grEEL!FGoFp&-y>5rzn&1B!=|#}^NTV_nPxzL4?EQw}fK zX>XD$w!}SeN*7gXvB=;4-jJQ`#DCd2^~Q<%9CJClL5>)Y+ZnUV3-DRzFF^S*!~8m( zU>O8)gD2xsdU{Sy{b%vhh{c@r^eD6QpVQe`k)7R}{07f#he148vd}rB9g>iK0`(>Y zFd4fRF~HFE&wc)ZTN9{2gu<^JjDH5PGKd=cf~N-9E8aFT9A96w1|9T=V%*lDe-0nC zKA~`d8^|<3zy<|%6j`OnJN1dEA8IX$I3PN%57QF$5MimpVx@kKIaKLoo&JIi4=*8Y zrlV7k9N!3i_^<#*-EVZ=4^WibE=%c?KjeSse^<*Z3xu-N3EM%hEAanm+5Xw+(bp__ z5tagfVxX5X-ho=1?hJMXzaEgO3ZL3bCd8G-b!rDnf_LI*L#S);q?xg|!FbJNk@s_s z5gVCiQY*G4g~+o@mGDGRtFa-Ihp(Z_hYJ z+DL59)Z_fTRC{Tq{HMd)w;$dy(2I{9hvkdLv9+m{rS?=kumATI_|4H{yDnSAy!>~I zF57jCw3rH%+d1s54A}c@qL|cS`rQup+6p=3wFfG_go73KYFIM;tz*!dWQCko81ytV zMEGtpg@cfo<-~%etI5$FZEPdzgd*lED$xjTG%7(uT;d%F_|EM=zp3ijE*Z4|jk_NG ztp?t>5q#r_@y2rU8>$`6pqryoe=@`&>dWwN0#RJmL{*fkntuBz-n}Zrw_+n4LOL|A z5<;Cx^(A2I$nzPx$bv}X2*p$q4BOuP!}k4$B5obw+!2ugp=rf!+@e}F;H4UJ5HnzHL*5;OqkgI7|DQ!T5cX}DNdgdZP~P3i zvU+#gFqGCl{>knvaL6xJqx>Mohj}DL4RE|bobZENU*@0OBMh+X{`Ax(HjcorEOkNO zCf{K$^+%u3{-{$DDYEcF(XbFgPne$YM^T+k1Te@UlSw#P5|1X2&uVvOda4aUZ-MIJ zf5dGei5`QW-{;B_d#K(K+=37UvBf?(QIYRkcz9`ttObC}@y6L6FGtZusBp<|6KD|PH7W$ zEzBtkPjDJrqaP0?D#U<`)KrqugR0~rdQ59}6QxuIkpezwY>J01BJ%?pR6XWG6epx# zF+bAtKd?A_`(8F_49Ovc$p?>SR&&C1~cqjX}H z>w`5X27F(^N&p9#E62j*g?P7}LNpLVlt> z2-97IHj!BTJ>x0K*dwDhT!dB-v$W|g@3)D6`nKaDRH0yA^la>aI$6+T5Makh2pd@T@WzVbthTB&yL-hsXjk z|3O5BaY|k#Y!2g2x9S|rFQG2P?i%Y<-8fAwEiIN#A`0*P70V~=2X=BC@GE%6OdnYE z^>QkZ0AF9Dud&|O038MT>|kGRV%TD?E7VDv!1Nxj^`$a7T9W=5&>Yo#{z||+yK zVp0`~UzjmTfgq-Us~J^CbaE=fAN3P9RzLO?yC(yu>A2y%^%%1K!X;n= zsBgxxgT$R)BVwo|HX$C(c~bcJXgt!una;%Amwo5ZsUCjA&HLhxHh6tbQ|%#xrI z<%kIp0_txK6p`2r+5PIna*QT08gwm^nL z2#1kCL8@cfXURSxS$9{5QrE&+bi*rJTB_an4DKE+e46)1?qzCWD!VU`vi9F>DU5ZH zGblcuqxx{f(2B%}hK44ju_@&;JvPxl&;e>QG0`;o%Ia<44-&diTqCmmqGkcr%U-{r z)d-7)w(}|qgL1DC?v5=%bq!e(B{uXtcS~LWdl=9s_SeZp?}_N%iTzx zA5gRd2z{_R01OHAXokTb1f`6!Zyz`AT|BX=3c*;!rO@X_us1SO()NvX zWVG;+E-_rO3x8SJ*;6mwb>fOEPVf^~*p7>GK`e~l^H;gC&+DZaF4cuy5_uQ^yEihc z#=KTTt?ywp)sF#{WSAQ^e{IvWZ|2Tizc=jwCoj+@w7G@7)?(hGkNbY7?Wx2U! zeA5JN+^l?>8wtCDxu6(wWh^SmQ2#XvUQu9Gc-6>W7xO^^ucDxk;dnrr$>Vk!J_RBN zV^h#3pm)KZr#@DXaDs!k-C#1ueL5uKpUGKG)okt~8&MCyyhZ1Y2vSyrWyxPs1p()WkBmwLK|`r#K0u;L;*~3*Qr#v>z7ond zsL%(J2k4(T4?f0$9eB)$y70o~k>HpMc4U+~SGUS7vCp2!YW19d&x&^DbYS#Udh&uZ zTbrKK-0rNdl~;ON^ZDQIF}`2fY%finF?mY$c^&Oz!lRlT4bEIW$F4ba519?}$KP1i zv1-q{F?oT|;WfvmZ1CMzGQIm+zH9koYnj4kEL`7ySxZh{fzMwrugc2s?OnmHzW1?V z_oYUvj*Y%FH@{-smaknDU3Dlw&6DpKws`K!NedkXn%zd+_0%?d&^EixwqY#-NNlzO zpqycJ0&v*M}SHz{b@*rGHxu<$r^2{fMB0+2eQ%@oma0wHP(ls1Bb8p5bNq}tM( zT4x6HJe4VWn#0N3SKM>Hr#0(|&tk^+TUR?vGj?1sCKy@1FkCkR!B_w#b=jjkG<%Mo z>uhi|jS7!x?>Mh|%H$cTrS|4Zx#b?_$Zz#rxw0$E?biKe^?qMLUQWwp?duoLKy$2p zY&qX`ZTGa2ulqJkIkx6-D3CX1-JVq)%ZL)qX4eWF3n#6dyLgx*-;|S>DioHJ2*3!HwIN+idkF}Isqc(XtSx^yyYyu+&&yimfRu(E( z$nPrz74r{}nyNvd;E!8h7K-B^Tr*idC13M2gyzRdmt;?pV-YoI;ejpqyi1<>oL*B^ z6|y(^n#qw1gfsD&IJa54GVzpyGV*gD5@m5E6Khr^aI924G|2%2eOZ`5WcrARQWDfG z8P85*`15DGWU$!`%ukyK;btQnEw~vUghj6GMc>q@k^(O(nxAuOXR1Cn=Imy>KxU_^*?-IloeYh1K+~_vS0^mHkLFWZsxY; zX8DQcG-E+9$ohh9LHW@jYpUd_RjAcL9t`%9!geg+8Y((1X>Mly_>FwHdAHFPToB~8 zARS-H`YXpN!bb;ij4s3WuSJC75@2E^26GZhoE|DirUW!27(_4{ii*qQxfRSzf;1hX zayV0>ggY1>KszDtL=KIYO9-hz(N$PqYb8=-&C?3sE;~b>_<7+j=bTF4DFPp=jTIr%h0gH z=sNVrWvK=J9M>im>okp%Ar)}+uBk!<(8*|!7LX_ z&fmGo~r}61PA%@EH5U;X5`vy#Vdpdh~TR zEQKA`UHvyF)WZpBrIQOq8oaNRzeH0fdngNu{e(B-^vG@SoRue>iNmG5gv%x1!`o^5amp zv>B-(#*7Q3jgVoM2f%B6mkJM0AWt@3JwsMW*C1#Y*(+rJ!>$XmlF9qT!izwypBcUpps^TNByn>`fa#c1C3Lz8SuN`np=?Uz-t7Kd?dEk@u(X zb^5fSI6&X)bVggPKM0S**>~{g2eQVkcW1lLo1lE$6!iqWlWP68l8ga=AYjY3m1Zcv z@dqH=zsqvxFPFD{2# zG#+T2`Hiy|XxL>_a;p1&Qv=%S;4+q3`rQ{xr{<}Ua)tL4|sbFN`! z-W_{7kzaE+AK|jruwgc@QFKCELzTuyPn$jX47)dl*k$W z2-JLqRQXT)xhR(Ce<&>&!yN8UzX*5pTa(IKb{r# z>5~!)!9U8vrI=>2PxAt4_m_X$Nsq7iVdzm|Wz*e+WEa9RS#{9NSqOS)fJQ6&1%@su zQ{|R7O#qkS{v1biKwqwJ*fJBX_N>=2X8hQevBSxd2+pV+hM3GK5m=S1_PAo{KN5Z^>Zu&UHD|Y?)2t*i($>Dj6WeTwd*&}C#q-R--Z?$?Ku@C4n3kI5Ns&7yzP%?~Z%e)gKn--=feo6Sr>C!6#CH20-Te3{uxH2>VE5B> zJ(yo1SH*H4e5;{EGJf!ejs0+&vClnxxo`j1K`;PCgD@|H9w+BdXOvDkb z{iI_Gjgle>z$Yb+Aw~@L7En~Tk3bhEeF}VlZ03mq0c>SpK#E&S{Mn!%*{8VljyB<{ z1q&BenybS8zl*C&kgD)q3VsP4+H?sbc5|dP(hli*WXbGDAGGW#HI8#LMJ&ZmMQ%A+ zPel!dn7+v9N5`uoHV-q9D*9lc7H)Nj+iFDjXez_*7KUHJ&I*;owxU+I(WeveM;SW+ z-z}=Oq5Bq6y9#p*FG1W!oLE7L9sKCPfsgo4FWS3){a*en_IQqi@9+l?-&Ip^-!tuU z#{%Y95OSw3Xl-iI^$DX}7o@tEd&Z6`no~4ttj9NHgxBjGF-3l}pysZ_Knv_}hOc#y53D_*4i^`nCaKRWp1y%$}y z_adWrR09ZjI=-1%)KGPD7xD_C(NPVd^10WS2AP~+di`9$CK^hIH7xPFT>d2u!%8da zby?Qyjo&pCWqz{*{5=h$+9TmzC+n(P!!z#OI%Ue%J7k)=}QuDqCfu+Z>Yrx3yGE~{Gb+2d?*|0e>2`tivNNSLD>A8 zr2Bw^ki2eh18mU%Oi~SeP9avTl|T;ZPQYT2s;WUTjE6tX4FX2sVgWT=+X(%e^y~OP zQybSc=-H57<;{X#O$e>TaUmP9HYDP34fuPvjHep8S%w(P+3D4 z1wMxUVcr)1#1(MT78!yOGc<)QFXXLxRHN@=M(+X1o`ggn=)DzlFQik z8?QFTC=Paj|NZV&GD{ydawTTn;xuonO;uB2E?3m}nkx!%#IWJ6vKnu4o(w<6e@qXg zKFmgzJ+ft3qd!GX_4tbB$|>@wz}U*#EEg)Br+ac+OTCV2w|wc&4fd=#bE?X{m8!i+ z&CrWI-YlnQoCJ7!ag3Nvo3O3QX=(bka- zEL+LVT{JSkJm^%lU`A$IQMH;YqnQf6aX-uav*wlyU*RVn|NbLtn!2_w44q2T9RWuc zDkM9UuTIM^Pq#D0kykMDtDu@QeY40f0@lJOA_*dtH|P#Tc6?1H0pkGDr1+lYFoX-L zq)4q{pPqNY1?TmDlRgiOD_)T8A9v~3zkaDYk3M;&ynV$)`N@eZR!lVJPF%sCo0pwh zoLl^}u1n-6FX`&K#F%?Y7sWC~{vvT2WMh6NFPHF1ic2Km78JY~h5=x@D0_*RfI!a# zyp}uVE)m9%P6;jvCYb~VMwocwl+J#TbjrO1-!`Bg&@-QLP3TB?p26oUBR{{;n|uQ7 zL08wq=H<*wgDFA8PUNmA6IZo$nb(thabGjcYpK!rwa<1cF#}tRS(a}G$szeGE}66a&q2+5^DHE42XK%z&>)YRcj zE)TV+;5s2yPxvI%wEfKY>Q0sY;{K+KN*7(d?eO7k3#Y}VHLTxndxHPs;2nJ6Tl2Gv z*vOt62iDHn!QbHDzC`2yyIp5`~5lh{i30I&Eaj`+YZ;( za;ah3-t~clEbovk?|7AexbHRoPM&wl+%>D(hJTZ}zj67mUU_u;1cNW1)IDiAeNZ0- z#@6!51f)XLVV@qJBl{x)oj?-K?yc841g38ti) zVvQlkM)dn}v?N*qM6CF(SGu^!NhKKZ$kF7ZiYx&TIdTf#;ND0(bU+ig3T3;uJ<>_( zdC=8a7gi`QlmNLb40kB&>M*g$@KZ&L(|$sJDmoA0k_UQm8t8>fB3-8LZhpdX91aub zBQFrh_Ab*}5TEAQwdP5}yCSy+t>ZTTADk({Z`3f91yMZ#O2ZRuID3o@H`<3xMv&rv zPoy4xh0+p5!Xt2K;&_hVs(NDT!qL`rc6NqB8zbJ~r!bet>#0x1yTf=lTN_*>iM;xT z+T_~2Uds}W&-%9778%JH#ZJt#UR9BjXuOoNJ1C>!cK8U4XN0YD{7kY$zINv?53X8u z@RxnRI&k4Z_|1ud1b@fxAV(E_{1VVSfpS`LB_= z@n1h@uq0jS3@^gjX6WQR)Wo~PD`9QQ5$%ZDhWB(DR*1C)BI@BtAQ0(a7*V=oet(P- zIf;0p4Fq#XL|n$Qya$DNVtTUTF|6&C+vLNyz4FR!#=_fPVGF0MSu;gGOrN&4_|{i$ zQ`*){F%tWLpNO%ThJRMUw=Z(zxRApUNz8TyStowmqOc{eHTtto$ zt%M;yiJL+OMSCbmlg3Q>#REJ777Kan`u(x^5p*=@Ck$~A9V(@vtw}%VfPO$6c2mGs z+96#g?SfzDsPsMQU(t+0hzj3B+)GfR2<-8oPXRtJKjt;-AP_=8Oq&OjfhVne1{>LNgncP&}zk zwUXNyG*p7if!Zp`hkyiuDxw716XntFcSq;#Ku(Yi+ZPS5J-$imm8y2f>j{PcbPlwF^s$!Q|E%FP%n zd-$HTVWlZ)PXVV5mqz>s3I=+FyTx*(K$x}Djli>;le7zW6QKIF>4l|;uz?tfL z_g!+1;xn=Q&O5G8&tEJ5k|HKX{@U~T&iV7@n&#pBs@ht%dw8?_OPW}0KrJQzDzVHO zC!6u<$t*hKy%}usFy2*teKqeIX2z)hX`TbP3mL4^wl2(+;OAnnh{1}CsDN9zS;InO zasvhoc~E@}zrNV0iwTc3)X~Ue&ZTn888xLDbrlaEJpz2c zw$+mZM67S5N6)@Lh4DrIc6`qE=Jd48%&@a^Wz(GxOz2qdbj?X~<9ACv?ybsQIpwli zobR(`=h*d;Ig>`$Ypm3`E8xre_$!k~dDNo1N;RW#&K~B3R8>@F$U9j6_x!rsQ|G_;FTXjGUE_47Wxk~RIkKyKPD9qB zCEmIU*?Y+)Wsw!Ob=5&ve*V~RuikfOR&AC$tMkB>AI%#-F37oDmh;0$GK>6m9+%?L zi+Y%v!`_}yT~aqQnpx{iPtQERC6rS#YR0FxKX^ZW>dAWgJKth;{Lid3XL?;#imX@X z;CV9I=3SCgAwM*#CUyPsZTt^c=TzE@a`v$?H+^(@YL@!r*ZF;73#)tbn>JQnSLUs# z^X67pL^7RuwV5+VjR<)^aNdpw!Q<>mOI?wkni;51zYP6NmJXpN%OdbW6^q$V(kcdVDfd#T{DG>?WW3|E{6pkbe4)lQWIG$E%fE1{K0@dHBPxPox-S~uk0Fqjw zp}mUf4tt&rWtpkaitO{%4O}G{Yz>rB5%jm<)y@^wSLEHgUA5~es9#Cw>4`)~YkXoB zgjQ8oVG2#XR8}Piry20mbc{9jf$=+&x>ok$STNVh0I}QE6uV|4l@t05LQSxXB*Btc zguzh&^dDJ7cshFE0!*yd1DD`w2A|k^xJET1@5bp;QdFBlw`M*|e>)x$&qPnTQ?c81 zIghw`u|+*7o~STWN6{BqO;L55-KN=%uUa?U-)=sXqPmI(A4+`$XTr~M%ka++Vzzx& z#xya3Qr(=@*q=PllYqjUUn-pC(44Jx(|JC)9^Y?)qB2g)ScQ>q;bs!E3^Mx%LD2Ho z1$cvnMb;=7FGgUYOROM2lU7)rSmWlJkqv;0NOkIq^HPZ_4SpHvfslOclsGz)be`^{ zWpjH}9CLz@F<^(q+@&CQ1+a6(kO_iL?UZK4(W;;q#?fJom}92^A+|`mPP$vjcXVjv z%i$S`Y$@Ijs?}a`?wa`e(Eai4L%+*}cf=2&OQO>y;}Rt#a_eXdLK<{E%Q1vbmH?WX z4isCr=n4bq!srUh{%~t;Z7UDgwuZy4uZvF^8?a)0TkK-y+UI<2o^2eNN~49w-qTJ` zN{Y{=KWz?*sfHk7vsKRCJn*qh6l^(tZwZyRO_j{wsOY$+1fj8%D z>Ypl@u0b{Zv=d|8p*?L4?WtEnhgPb<0yoea%27ZN9m*L1xitBj5`NtRvueWWcB-K4 zNn-3*gmgl(aJ+#bj?QM&$QCD0!e)cnI!R>bM=ANf$AoeoV#xbCtNA0ycZQgZd_O@N z`Py_P)kd6wbh<;J7!>{Qf9!z&u>&l7S zVoaK|30+nCuf9HEddygk2O38u82#r64h3P$#^btWoRdCO0*D` zNXH@djV7N)nvFtY0}6A%Mv1gfp4=XxrSROBIPdh|pqk;nRsfi(M2|D#^h_*cnSLXBBbVV=mj3hP%RnqzZ@b5!UX2a`0EBx65H;(KJYB6JC#Ncu)B6k}H zrr%)1=E=c|;_KuW2r-o$CnWzTxKkL&ZY(8FsIU!j{m6l|l?h!VOl5S$+JGvVz09DT8AAY5WWs>y($Fp9IP=-V z3fM$c@(-V;;j(ag$W|9hPY?C=gp{{uq^HkF#}PIbflsg-K0(5;m|8NiS%~)>NC*Uf z5q!y~ZBxE=|K`p2Z+@qt3`VUX*3Du(zy%uZ?vlt>U^Y;fr))1^n1d zFDX|bszDN^o?wsj$N56{O7Cy(`Hi=m-K520{f8?{*}Jh)zD-5pR@`^omzj$D&K3Sc zy6=R`uPn?;l0z`b`v8W)ab{>nR-fIy``O*fQx6)_gNx^k9zAEYjCK4myI9j=yO(t@ z+bur&B{ep>qPt==eeltV9cp4DZVhrLqDWe|jl5PEr1W$idP2-weKGOB6vMd0-hU3C zmX@#$DQAqZ^JcvTlTo6ONVTUdye#utRnP%aa$!BQV zk-vS`@O2M-meeI5!g@Kj)B79bJ!ymwX`?x7N%o-|YT@*4w&|`aB2a$dCQv8RX5M*JH>jSKH_n zx!3GErbc^l2q?7?#THImwbu%{?Bpm6*;EBCp=Ly#_=ywXCBhlNvX3b3;T?@5kH%z) z_cm9dVx)0_pwOV_Ql#g=i*=s%BUpT9JtR0F5X}P`+-QTvh_APcjSM4Ml?=D-{e{mf-TiSPo~5$>7-?o(y&iyE((tarXuF zqjcxe`oei@_^;Qjg7$wQ7FN#-Hm*q1YHL^SV(YGPw2Vu=hFvsZyxrrh^*+oRXzt`k zGhNre>j@U7zOgI4m>ALE{T}sD`eZgGBZK|c22Q%$ZHjLt|J!!9>eut4Z0%~7Yt34= zcHVC;WII^es+jF6$M}}iD|LR<_$zA-s2Sgb{PcA4hjJAqkNu7RdjT2JR}@nFVE+53 zWy)eLsvx$&bq6?d^vkbjhq`AN& zX{NrgGqxyaZlRQx5IPTlL{OwK2EY*DF-b0}WvEasau@@MU z9{tlHC;jP=hInDb_mJmZmJ*N9pYWpd)LkN=i3%`b{-U$8(8)U8 z{(zfxItweE{6%28MR1UZV@HLYJtCumg6#+@*p$JJuO}VJf>a6@qi`ROC%q|2L}09G zmcjrOxzI(~5%q9%uXf8?SN}ruTRq&ruzvdVdi5|qpCG4OE&pG__hvhS!}#IoT^hpz zTSyDB>GhlrtY`R9EXgB>8)9A1;W`lCLS6+rDzyb7S76pe_KSuXr%N8`9_5xtxfo(p zRKM1xNLZ3XM^4k0j26unsx>O`b{cq-s;)ku!db6B%z%tc&X7 zx@>eF<8_ZmuBG!Bzn48Y4>&A3ZITuh=Rs^pL^Z|x!5cfHoN7A{)J;Ifa z>XWFmYMw_t6D5p*J(udSob3^Ck1pd4y2T%hXBGGOKkbuv#b4}!E1`0N}&iT@tMhmA6ci|3&Vu25*5xJcJbgXDq*+-y85D`6Vf}8r5>qQMV6Y0 zs>Ayh@+%1y3+WmJm>65gJ4d<}Sa{W*<{71YOL~C+mEu+`HIUA3DV@>G(}y?nztZ(s zYV&Y*tJc%EuBwUUu}$d!S%5Jhoo&ivO?>Szv<(_<7{A?i58VgsZD8k!SA@J6ocZa|}DDn;o#H4)b30r_J7BfmH8J z{H>VJLHDoH{p&Ll_fPoLb8Sn)C!mUf2{Ld4BJ-bqqyw1JZ(OzNMojav^E=;hM*mZf zcq~tLck=Cgd#785GSMTK_P7T=IP1d4g#c z{(F!;mAnBXWDyO=**AGZEiURyuCB%eig zKiA#e7whhZMjFX4IfZ~nOo>5DHKwnhjz8J_8i++~u}FV+jQ-QRr`MN^_1yW~?iiPN zH^cCQN_(egY)L(zzG=ZwqaxtBH~5(0@fRNDDWiIWH} z+mgJ4kj-?i5=D4-NIKKm%=5^^9&J)tR0fwQmx17dB(LDM7q5kU5)=`;7j<$HWZjhohDdhAR`Za=0*;jn zC_!PMhdz}$0{AsmDPTx#s%np%b_Q}VR~DEX@k;Q5dKoZY;O@y2b^&2pP|lgOMPT@$ zkdQh9+1SF=h4M=Qg(-fc*_mfusmsOOd1Cwx?Wm4>Ebf*h1!pC>loY1-K9DWN_PiC{X%Sp#izdxPB6-IjkXYWq_8i{fTuglTi=8$oIYysnQ!BMa>n4w^`Ps+F zb=bP)UZBqo-MdA;IW`mY{WDzcJ4tof+3u2$QE23}*a-3GOvkpQ{3$CAq9r54<6}=I zAA?Ip1_U|y;Y}GY<9^XstqtcIf#>ImnK=K%{zdLUin|f*H_w@womK-{Wh|d_Bz|DL z8IM`DfBlvj=^ouZb_2buy^CF5Z)){KLW#-k-A{Fo-JT;rnyZtsgMn&tIW z^b3ab3!nw46*= z2s%oUX_EMnoFKBpV-d5REQ%visW#U{BClgog5eddk+ht=)<*>u2Y(X`q88%d25lk| zQ$%G;oH1B6mj*VnnqlA3w_%o-leZ=gA|5S@fc#Ll zLA^;xF`_OmU_N5HhMYt$WpZ^g{wz`0<~7A@Xw0n z!W8z$9~N2oE(X|W+~-O;ViY+bY?eg60D7I^QZw^zEJ=g%E{WP9aZOoNx;V>zTR#Iv z7y4g6bKkaN*{$Azep*#*-dmr&mH>blB}16miHvKW+1JaR5s`K%*ChJiVpcw~$O; zvSkZ@8#LNWVgo15)%ezT1j#mJ#CN4Gy;YB5roK+PRoaJ)Ge9>{hz6fBYU3qwKQWnwovnw~GBY?H*VhAF;-JttL7hZIF9(7bEf8x?b!PHyLblO&VCjVeM8a`wvw4=Q;%^+ z7y==)&G{es17yjIF=&#y(_oD27jPDtc0Tr>h6)se)>j10J)l@QGo~P5meoDbk>Efu zr|rayO?(JlY%Z!}%SpieXE3J;FaqfM-sdK>7D`kU$RAt02LpFd4kM@zg@zF)E(Hay zU_T)HEUtU~pSQ<>!H`}-ta}_{%b@GS#Gw+2qsNWOLdwRJ(pXxi+B|MsX*Me@D_4G% zm&YpEQeU=lp4V%fm+fOqSw3(9J#tiz@*e*8VS1I<`-55~-^atoVg6^kou$hQSue_L z)lPm8HG7Ru|MC~v&K|0NFh|IjZmC<3=n+JT3m{?2vF#ts+cw~c{LrkGMQk3c`}ywO zKgV^y$$Mx?zJ$NW`=8sm@i}H=h0I1vyh^CTk>ul8o%pSaz5PRe@kR`~GGxW++^@6H zHqiWJOThQje{kDZnX2$A8Yuu*O4mApvH53sX06g`u1`zXVw<{}{*Phe>B3 zxt^Q{TVQ1f&_pEE1yQb3OiY7_Qkv6}5vDd+?m&PHm|sLGj5YFs;7f(tiVA7u?7|vh zZ1MI9X#(6Ncs~Yfu~AyeVEr|^ufhB-Lbnhe+w^dY8D~gsAPjBzeXZHW^w|=U8@P-x zdxiXee2gquzJSkune+xe8uJ1lO}p63##J=q$h-L%+QnYMF5ouK@yOH8Z?$I<))P9w zKkCJp6~xp)PGFIkOR!HdApmlYOeR6icdn|Zah-nhBtbt3wfXkj^hs-HZaZfsl=pt& zc?QpoCnb*0IDY|T2%h|OJ$ne3fqLheu%Q0hL~h<7O@P*$$i@V6$SMvwaO%wbO08g_KR!5vTvHM zd%|P}KQ;@mL>c`v#_;q_?ckHj}Y+q4keC+1bVsD)q}=>fceKAVSDq64ihY zXSZe>hv^vh(rI(pKR16wOpfR=Tep?{Aq|=H63rNAwDOYo0@j3DHgGTL3(8vlJ71{I z$6`Jo6{HXO)JueR9}xaNh3tEVq@|G9OCNOb^Fra~uL`5xo(B)mt&L&#`JRO*Wl z8nw%qsv=>t%GF;cd|jecyB6&)qr7wVT|-pc6B`lopm+Fxy>0`IBh#0u!EaIpg!W=- zC7wu!s}|&xF4@T-UNeci5>&{HqM&&eUimHXp zj1zmw^^@3yi)-g>Ww~kLiO$^J^I`R}1KwMg&EK8tjMTnAe^ZZHM1AwTtA<+x9%vu|E&e<`y*uIwkyICT4KBVRTdu09|SRN`hxn$vomI_ z+OlNHmQ^z{vahO}=WOoyRaqq%LT1)4@vqpmW%1%IyH@y@)IYvpPxipmEB z`8nD4AO6tJMjW(_@B5d7@*||i=@ovLA-P)U!|yMY|1AHxaNxVfUiks}0b?)3s_4Io zYoY&&T9`0$a2?c?Ld7MNno+=;#4-mj)0t8zzYqu*6AM#1?b$hcb@C;~7umL|mFiYo zkvzW8@5coO@!6CuPg!h|@b;tLx|mT+>wyr7B%SNUKPzX-3^&q*{*Q~#$za@zq9CcD zgWh=D|m7jQqCUg27;S0uAYd5TzV*Y zIm$6)uqYH;L0sAdpB`1G5pI8k2z(j{mZ7>(BoFUG*WgW|Ri&{c(5Rq9jRF@6z>b=7 z;78Xit3R>$>Uj-MhW(XwnR$E5C%H$x_Ub72r1E|FnRS)^>TxLM@|~Qqb1zpMy>9Qs znrGP34W)MfUJd2qYy*$1UNgexNYnIve*4c$E=d1m)rahZ50}@>T>bEa%O-2lTh<)f zzHmn4`B&9}V85~?nwJ+XsdV_Gb(dD$Io}^#{QQbrjXJM=Mnwz1yErh*tJt#A!v4~W z)3R)emt}tCykGMd7O(|u!Z+q`SU)Q%u3Lb5(8LEqP*peikKpwa4nMg5@O0(lSc}4n zDAle2Su>C*O0ndEFhWxl$SC=Z2BQkCszgB3UpQ>aO6LVF&ST?O`_z@T^tV#gH3xpq zX7Ck-ie8bE`c&%Z4CSd;`8zH-<^RcZ+RE683Y#%zkWJZW@Iz!r%$V&i7W^f#w5s~_RRLQU%~9V z+h@Xb<#;ObBPTzK^VXiPGJZ7DZkxN=_LVjp&h?wbRepo(YjZcRnqH3(JU@z)@uU7A zihhX=AKI^5>RECH`+$tznf2iEjL)Gd&^?pT*rZ(wCZC2uyoF@V*e^<<-(w0Wv) z4lL=i+8lL_W9T6ESSA!$fT?M?P4}zl6#KvyIt&-8!D&DpNXk)pj)_? zEUf?6rQzA znrSof?x{2CP=tj%3D=rWNwP(Gp(OAvX3x*hvoB|&a}&>vsb2HmgbyH4GidM+ypY)x!Z4s}F8W&(8F|8_H%myVLC6 zM{lCDvv6qO?Y3#FR!wWe5v3bih-DW%Fepxpw1Tdd4W-!JsH#_%)zp;nJP~&kdE`jC zqegcLsg@xo*NiRXyVFs_06uM`Ulx%kkAh5%MP>$OofnC7b;$a_0eX?hp)054 z91)Np__Hb?}&{oxC?KS0owfk?HlCV>tiWG2 zWwGKQGNNY(i(_TEa^zdgU+K{5pHBT>_ZZ#5;^LrO{owD^{+P%kW-j6?wQU`=alO)*091?zQ(LoX&r zr!b!dhnb($s8f^HEDphN(wXI#OwOY09>(k5}{-5u;cajO>_U!w$eg9`yK!zxZCJ%D#K>&kqtmFy42feX4{^k3bC{uuLS{W2Q|!vV$S` zMD)J#3WXn&)4C~yJSktOHI)o>#&11O1Jm1pULu8dIR>AcKPg4k?eG3#|4p*Sfb&c{wd7+ZC^L1K_f1 z$~LzA61m7RbQK#aU45AUboEC5p1gUtUJENq{Fj&T+qO;Nw|EuBCBNvdOM7|BHqTm| z9-qi9rK?%_;nk~l$1h_}!uy)?I6Q0Ub4{&K90(JSbOR96XkddsyPC0y^2I>=q~vAD z7X#~4VUIt7!6Ht90VYyN$plcSB9Dy*3bDKjVH+UZ2{8BQIl~TCbb6xa#|J@DA9AN* z7&XRPs4jX98Dis?qeH$196n+1BeX8h;h8j+$f7-h4sk9#GdLHg z&P55ToI(_!NqV*&9>a1Plb~ZZxZs2WRbr%0LLE>u^gfU~A!BxF5b^WK^ixzXmzGe? z#7G~h0~tp?8bkpF+-v+ba3tRI8#X@>Z>wm7iru1RKh2-}`gQ#82Ts3}WwY1izPtJF z?vdBVuMfA@1?|_dlmn-;u?oDl@{$1G_ZwnJWTmeum0C+UKG0TiKY#O1@>*Q?b=L4S zFT984-W`v$hUK-Pv5fp0*V%)0s5pB|Ua$Eqlx;*B837=sT-BMxd03PcSMPv#~awJ;QP)kD@+z~kkpd!YN*12@O{%>%#gen2mE zIO<;C&TsmVzj62O-FGwphZxFV$EPPfB!K}_??5yg`Q?eFW+_+@PJ;&nR<>ZNiuYm! zq>F@=x~7&&GQ}{e3V|$~nT5mqgJQq0RP-(}`{T1h`!pu9p(S!VCARD7!-GJOV6sVU zTMkaY9GR$}-qd&MsfSn^bFwsn{JE9);_vD8>u=q`*0If3t^25VGYHH!(`UMz{}=zS zr#d>8Z)UTvW{coZOjAgg3Omx+UmXw9%wHjV3WmYnu!7UX>#q>&+MXE;%GPY^M0|&R64lVEKcnUJMb@}Eovu?*WPfK$^ z(mJsY07tIFf`@p*2Ji$;hb3x}7|Dl4BJhxk z?ul+Hb|z6t!IY!L};_xhX_YGglo~4 zXd&Vt#gfLzIx>S%%5F;M>?I5lMPv#H5)wB@Fqk4knYoYlHV|>8Pll8R-c~%k*MBbz z7T*m|sGH|zvdSFxlCn_Hj+BnBu4P>=raSC`P;U(Y|pW zhX(TXoZ>u9Q7z8wsq+_1&UD-4w9=AvkRxSeOg&-#)NH3kRkXa~994_aRWV4G%WAYr zFW*|LD2+8l-wr}CXH~T;&n{OsHde~ap69Bna^_hzrKV9)>e;2By4Pyh?Oy-0&+_R2 z*F3n{=ZjBamEb6I7v;`n`4t81963c-GUP2=+%~su%a)XM*?kT=#@y^;C&Vu1%#_^# zwAgF~{(19qi!;y?MMb!GMj(6MJb!`BrZKR>XHp~?a@%7ZJWX(-i^vyawjfbXhq*&5 zVd&qOv(#M$`7Zt|R_MwvkOzql187>v%T3vQHt)_s$Q;TV_T$!L140xOUqL(RRS6q4uVwY@wAt&u@t4DqhJYqdbpElr!21_nZY@tMZg!fK@z+VgncRLrCX`lwA9w#8zp?@GABT!@^sdxgZOyEd24L=DZ z%kVf5HB8N9_3_8wFoKP4+xSuS&Qs=ec>cQawm$Wsp$M^EM%GEQ>zn?> zs1c;J>1%0gtz|)n^_gd^j$oN}ZQ9p1DfK4xd2FT_EyU$ymt+GPER%>{%16s5jdWzL zB69;t0Ie`079pm}=tv@rDTXknookSPghw@DS3e?w@I{O-GJX#pMA?ZLom9vkn9VRu z2n)A7Y}A#D2<#_pxt7+lVFPU$YfV^f3*)_o33IJ1-DQ|x@3JJ>WG}g7#1{L%*M2;# zS9%HeHCn9%mjTpDWce1j;3PFtuooojkb;+B`}ya^e=^KmkgPPa&QTg8kqwZ-c%d9I zxd=w-hep3O^xZ)r3jxh()MQxeRh93JA9p@me*x|}qR^ht)@joxjK6?gNTrYHsTVLR zQ^!~k)%R*u$b?lTHNv`aypV|?Ll1MCl3axE$)FhpOAmuUgiJ)iQp^?Reu(lAvBxuC z6A4NqC;PCEWPR~*H;G6KGFHHNk+2>4J(b6fENc%Tn{T?UMpi?4p-^CTXwJf4bO&>+<>-NhjZs!`)=u~I z8E)2>Gp;#a-#jjd{Y}UvCaT_~WI+|lao4z_rl&T`N6#%R%uX@T6_K)gP(e0fCH zXp|KLikCzI2tLrmo&iU7gWdoPdNvrh(4G_lp<6WJ*uz(3(aFq7YCU!<0YpY_uB`xO4hl$JfMUBcabLERKSqo1pi=BRfo5(+1$d|O72avCh<+e&9THjsfdr6# z45OJaAPF07Vj33F5e~Ak7C`PmS0MA51rsrBLKPL6UU=+ceZoieaBNon5}m;YrVNgm zZo!I%>A8ocX4Ty{CM&H+#2jOqK51%ATHHJ%U5?;XY7+xzV$_E$24Ap7$+;RnXYFEP zp_YB#N+MMfl`nF!#dZ>K5xyJ7Z=s46@{s^U|}pj z??Xg!5ua*a#lDM`quc(!`79u3{_j4^rDk=8mtW#x{@L&4&v=$_p^X;wUjF8|@y&?`y7b@r{g{J6+?9BoFEt$e)8CJh7!P8J@j&^q zQlqdAZ+D~!jyxC#FjYnx;nr!OtZ9-nK*@K= z`-?QM8X4m#t`B>ZG3=`!dGsruVs-6VTW0EIEvoZre&A`R+ES)3yZrKH%w<=P?D)YW z7v*@Acld?;1Ef$x4mAGL^)Op*aMl*P2A#F|S6HV<{{HBXuJ+i*S5IHm5W4j{-~MW- z;i?~9J*MHgt1thd^|6nxVfH5;$CV%O3xB_X2~;+JdxNu(u67Q(Y9ZkMU%p$&Pyc@T zZvUTRZAH8``~s&*+aOVz@H%prkXQ~xgaO7hwh2*z`H<{ogsdQpm4H$N!&pOzKY%sE z`(R6Jzzjo%HtaX~7jP746Z9o>8&(PGW@MKDA%H@hndk6M=v(2^@nk#zxvAS@)a@=owu!xMn9g zN(OeWF*e(K_rCxB-cLMu>OEPw92auGK*$bIHc9v$hyAM|sTmfWx#)A$Ff^o)x6v>z zBB=DAIuW1gG6uM0nk|QTG$@nMXnP0?6MB;w^P)+ z+7}LZd4{tq61znv_y-ffE{^j-SL~G$nflZtc7Ob0Fnh1^!*H5Lz5(Dn>tVeb$vkla zFcW7=TY%-d5YJB^1wNoeK8}Rf?da`<7mnIWG?)P3U~o;enb~4yb2YNM%t%96@scuV zIG>0@g^)gQ)kGRfEZ9#-{Du;*A{h)vlAxhJGHQzr8W}h!cSl@8V}S;#vLW&~bC9lO ziI4UATgUjlC;8blrm|B9*)sM~YnZ@Rz(Rwul@AUMD!UHf5aGL`>g`FnI}RkR|5e=B z;8{|&0`IN@?<2Vz(>Kh=l*u$c)Rkzp2u&Au2o zPf&W8xnKpJk9rn4^5HAN*G5tF@R>*r$QD*7IN|9N0eB6D4D$Sf5YZTAFq%ck+yj#- zVG;<%GuSKw`RNeiCC;+ zQ^*e--^(wtd*0ux*j2CZC34h$YvH}kd$o?S-JAQ|^V;}{yLkMj^N|iK?09u-y}~MA zjNiO!>xtc4xk~65wmy9Ts1?xs@f~73JTX2 zcyBn2F>>b`GC`210vnh8X@}PW@Gf%GMCn(7vm_=4Y2w?69UfnWE2|J5fA~gv?WgPn z*`gm=Be&7kj(0Bm-}_t?>k-xAWdfp~STVXWpOXCw`mom(M7Aixs1sDG2-BSpf+fCA zbl4Eh3&6P6fy4+Y94vJR^#xcgF^n2BD@~(7j#b+4l6y|NHcc)Y55ReR&(2`8lP|@p ze4D1mS46MZFJI+it}_0^HXz{H9WJ|e-nO(Hx8iQale6b%FXBSj>*{+Z6LV>ze zR9&oia%#tyQco_bDps>a=-3||QHIHK0#E8cRc=Xhh@X3HR z!~3zz-MM2>)+g=v8}V6?H{g3!3@nssTEgTeIt>^k5J6!u2`CZJ3J_cy3#34yi{Y0A zBWyjajWm9ebypasaJ4W?0m7Kr7NsfD2LVFu^2h@`-nr&lx63zzhAUINGV3+r zv)kb4ngvGMNIc2sswtb6)R14W+6Phn7^F@t6yW}VMZhz_ZxJ1S1{zh5s@2ikN59$6 zdMa(eeA)Ch#i-4h$w-m>86>}qQIDvB5i7+cwjfM)oL*|ts)9*SG{UljFAH2YqDBl$^z{y_Y!X!U%Ay~6S36`?PBC8W9 ziR|)4{K%rRY=N98Y}~@;pCQUtCN<9RlslcCvE_>vm5nw1ty}mX%v&n#ezr?nWXXkB zbqu7D)5|2m9D{`DtgwmL4p#m9?M3f6ae&if}F4ZPl~3BSvms?Pt_foz%I) zQu{{eu0$W{hP8fYaGw*!eS?wJ?}w>ew8x>}uW;lM=Ca^_8Q%JcZ9^HRO5G7AbaS6Q9b^Vntc zQgmB!I-nwM!bu=CeZmdAff|Mc4QTpn?GGwu9u{BL$h zS3HuNo?aqf`cS&1Fr5C%E9qgth#rzJEs32Ue*^G=8-K|#?vowYw0r7;Nf&lcpd-Y; z(fJ3H*$_omD3R;LkQ(B>ALm*5`!EX&_*l$jP_ks4PkB;Q{TRrD_^&@s_d@fvS9(fU z5J%_+9!8QH-7AaICPcQC>oVYW)Id$$2cU;e=zUHr$QVGub=4*o+dsl_>Z24i8HQVO`IM=>VWVq=weqkq)z!%UAW6G-D zjd5_R(5z-)0^_TmfrrHf| zJ;j+9c5b)$nSmH_Xz*ZJ&Vss5+Zz463iZJfktGu3^_`Tof ze|qxC%P&3&&AGmTIG&U2g&>mzZu?!Nr{-)9+5E>`|C+v$w& zd8}@(;|l&l>>WDy62s;~{GEi8tOkoQWGzIQle|Trw4nYn^;I^a$eIWZXtyDJt|L0E z{CZh1p|C=s92zk7)3XvA&q&sM)Dw23sc#xnJKG&Vo71J~GRvj(?R>OJ-t0th4cjPR;>l;4tjP$?xx_A0XwCtc z#^VR@B_gFDi;~R&{ml=gu95#&qKO&mHgWH2y09iEj9*QfUaYYUDUv~B6%9(Jaw;Lr zq#kUYetd3ZhKAiNNVYU^3xt#bXp%6cH`g#^s@mLBP+H(n?7W6K9!ET5!Bu~}6!lTJ zee~kituN9KfAGe`)7amZ@rRdgzVU@ld2s)BS=zq;;eKA8UzXyL4U!7{rnNRAKIw7( z{?^k(B;=3NZ~5>VJbi4AQ4mU#A31#ES*TJ~q(OTW^{cPO%kM{*FKVSW0B%G`Y&7JjaydtLNUD!FZ@CDzZMZOmbXLho0 z=}tReW_Rk(>xED?4gJs^%ANj8SB;8b%3*nCFj$e^pv@0uC$Ru~&Bl za=>tSP){#P*I_Y8JtmhQkI+f{^1Z;v=70=w(2|m3VYlcG$exK3qy7h8Ix}S-Wtw8qrAml*N0_*@f74k`C>PS6TCZ@dY7-DA=Ia$W6sTi|&gw zHJ~hU9FXXe1~$9mSB2bFpKfE;M;7!h;LrE6ipu5`S$=CT>ynRSGvtmJj??%?ewIA# zC&$fX$4z5DIwC#lzBs42SDkm^qc&Skl<($$Q2{EIL#vBr`EXWod&Y&2vQ%EVzw!gs zG4#uf#qD;*{S9^n9jEj3**O=xALp*d^fvn=%o;4^&o5yWQx=~ttM|p7e81fJBC;8< zX)3#lZJKz$ z#!h>CeC{{+>4|!w$%X9oB<7NF;6<2$R)@q&(x%;zI57=o7Qjynd9>UR2H;eRv3n>sVXqZUUfpj*Oh%N z>TvJ@l1-6~>OuObXq)zeOO70Pg}-?__U`2{xoOGgy7Xhwbl~FckTAOJ0tc&Yi-dr!3il2MK37+fzEd!$P;&h zuS$I8%z>MtzaV*pChLSt4Wre7u*Vlj-ys?a0ly%-MuuyZQ& z1bza#ZMwq&216{_U@Xfx!uB%i&^W>)wB!*>zH#Ecd;vk*aWxyH6-4+wVVt;t-D@7< z5n4fiCf~f_kMTjSu0mc=%xSP3llvu7lu5o95|hZW5*T)cx|0PVGoq2kTMW9znl@O~ zGE@)$$yR3;Un~H~h!GlsA51;Cusulv+UoZT{u+u@v<&jaSj(-mcjIEf6B1YWV{d#? zVdNV?OdrL=&X%r4o1y~`{|O!ke6T(W!xU*OiO3*vm^cscT^K=`_!Y@1P2mxNq2jNa z1&IB?{7WdVBKmXW3K2$%0VG7y_HcQL*iSa#@sy`A-D3S91K;c(dyFTa3p71 zWtDAeU8JHMDx%AoZ?mX!s?C>~=P#)!33&a$Y1Fwre*c)-rk06QrmbJHxMNDNRGHN? zt9yo5PPe7#7AWeeUrR#_Xoe@puBGWyJ0{Or-aTV%Q&BNur(Kz6y?$b_YeLJo&MC8c zXB|IvYFlf#5+O*w%*v|fwvK7HFCKSxb7NzB>5_G)o$1cWttfA5oHo9#xdoB2*+m6; z*;(oK3~x!TUYxRiQ|FY9maz@>#Q{%7Wq9U{mCMkd>!crPhfM5ADaH6pPN5vvB_fW+ z&|RYh&DH$z33$4s{b$Sd@?{SG7cFcHYms45#RxjUSG^rPcnzS4P6X5tlvm z_8IeMtaZ1tF}B<(xwhY4>Rvlz{uyWS@AL1gzc(iL{haineVh0Tr~joLs1r6Xad`QL%P|39p{8MPE!{x6ADH~S7`sW1G!{iCtnvWz%=k&ntD zGVYj~aY9fU#$S-?Y?|AlIl~-e+NP7Ct%5wvu+&+=Pcbal^u{AXu!Aj}s1^*-$Jj%( z3x1e%lDD*hkHE5O!mNfq*aZPkB>NDCd2b)ddn5nInKa_3$%R}^GaL!1!oDT&q$Jzn zxkz#?DwYaCV>6mbzY#NDko-2}$DzauWMT0`CW5sIEM7=;FzA(w_T$$=P(#4Prif&+dNRX#M^KH3w+V(q!g2&dDwOfg z?LYYA54Ly0Av{PJvtNB@>1`{U{M`A+yYBkq-Ri@0&eGP-cJ4YSXZGY2**dl>MP}<} zJNS$nR24}lPDPmqQS@CUsa2u0)U~o}VT()b;SrR(`Udtq$`mCI4w`G!S4KG3i-E+gg(sR= ziO~)r*!9Txfrk#kfC}CkGMT!Ygk>%T2T3xhYlOki$BOk(W28{>kYNjoyj;pkHHnO1 zyt8iCf*THZPrY;Vy?#rH*XvV@zIlN#kSR~`+nOfsTr+v&zAJxd;R8MM=Waa@Nsgar zQ+-}hi_PTbKUM)?_Y;9P|_VNbs@}p75<_7*|Oq|lh^E= zBnK?dTq%b#=T`8m_-{}I3c8!=*NJC*M$B2chzFQT==fn9dI@;Jf^c(GuBegmpK`#3F!oR%F)U)}AIa-5L?meu2df|u5(d9D zLjc>4oJn#KeAE+6KRa0_iDwL-r|>+QbrRrlFB7IYANcknMPfRFTcjivj3G25HF+9? zB*bb5o7z%$r1mroeaoG)?7W?8n5(YhF7_@e$bj{}D0NV_Q4-09y)cRjj8^=GNm zJ$dR5wIB}c<`U1a0q;*qRCmRlag zZ<392Bo?Si_`inZQV??>Mj)AR9auYho5rl7q&XLcYEw8@@K8o{ip9vIV^F~TLU521 z=B0K1FNB|qBRbq@VtOf-2-#<;p?6Vu0?e`^sH}B99~AHxMwpqSx7dG?Dc5|bl0hb? z;@QbWOXG={*WT1#z-(srHPqX5IHwWDi|90~We^f2vcTiBVy2ibfL{m>^<@Tx<US_@;gKnY=>ZfZ0g_rhJ{(Jm4XX4sKuUHtl19{b?&7El)1ZIfPQ8LZ;U z!K;smhPq5N8k&n5%4oFs9C%Wg3=5|xZ{DJ_I;YCY?AeMuweze+^YV~~LQ`$B6MhHT zYWK8Fnp06Evzg+h%$mvMqRQ^}@tLWzg~<+whJYABmR#XKFss74^6O`M>=|XAi=I0@ zrM6@D8{YVwjqsczGwFg`1{RENX5~BD&Kh4TjQz6kxrFt@ zG-ALaax6Hj`U7vaR^pv7ckTpliI(lnD#3qQNA3HC%jtWPf5fyWeNGpTK5)q;YT@Tb zzCymoJ3FYi=wqQdxq!WEmQIw`;9Z*Yrih`1gofx?a}T~rpGt05Rq)jp6J8SIq9NDf zO$UrTDde^amIes9L|94E7D&V;WdRXMaQI93MDyXb@F|5Ns)^NBjju+Y0{*<%_&Cln=mzA}$yidebDR=j5wp8+&6CZ4Rea$yom!_n& zE*W2)wKy%;QaZi*U`9bf#=#utiMK!f3wh`(1^J$mOVcy_1@#$ze+Ib><;L8eTsBZ4 zw^TICAN&l>Mc?8v^LP=pgR96_gs%@x07wPY{n2^6dslE~zhY?C8<7eLL!n!|`a zF+s*Gv;-rbhU7G8kR&r3@lwY?gC{hCVR@YliggrSL)KBD4^Tu?vH~z{OcY}OQMm9$ zbcrG4n_2{FOI)4_8xO|2k z=~tBuzER^CVr%~WZOIhp0%Ry+r!s+VA=YO^P&b)hw$JUM!N6=X2-~_sGlP%lPduj8 zMTp=SF9fN-u#XL9{XMI1ocv1j^6Xnq@@(!ILZUErcf711lJ=z2tseAd_ijk5{h3HP zR!_g`Of%Jq5}7boZXY|PEyADSZ;c%{C|<)raUO3~EPr@2JD=cZ!rTQzNk2vhfX?x* zNBD9bAOB91wbO)2`wZ7GSF%t$n1B{X+w{#LhKCLY1>f z@mU#J*7&U6c9mcYLx`i52j#(DEeibsj1o{qD1*`|e07>tWjDo_lWM2{Fj2r4u|58F zk?)6aDMpU01hxY$YFI;>sv z-ZuBB;K&#pGTsxViM#WY4xrB=rlUnWqCJiFb5oxiFPk)m01&4zmn_xJ=nX6T=FgAK z5BJWfRp-S{(6VZ~%e9>NAL^c}i~k`)BhDh~`6iUV)MZXoy&> z@s!Y}Jc5W&y<&uq9_;kxJkR4{zXf^o#YBF8i6Fm^-9=3H$^m}1qt3z4M$ssH5M^JJ zC|k%OXh?$%6(}U4S_~;0iRC^)oF9LU{nq1=Wl>~;9DfV$A_rr4v5QeOm7B!NFX}u@ z%3lP~P$L4FigjoZ$<+Ku)lTt^u=LXd(M@@tsPpKuAtI5wUncPO`L za=@&gdPU-SsLkY3YAY1XlG+)_AyQOf?J}GCXwLscZh@rcrMj`C_9RsBa6^(LCl4mt z&`3Tol3Re9kDpB~N@XNHpY%`6X)rQ(6u^dxNG+m7Fy&L^nS*#Bz&IsjBs7ghV%CFf zMQ9#+bW;x{tsUVG1nrZ*a?{{R51un8CtWM9NY9uU-Ff`|+nQQ7*Ub6VxjpJsp}PO! z+!MR6{=v64oZTbT?Mcf{+Ngxw>XXN3p9SM|0(j9bYy-*jur6Mt=TD zw@te!PJX+SZtLAqJ#S3?cA@&OtSFn>xsgu5A$ms?^I(kisgS`Xw=rA@>(;33x&*@` zGF;S~(xk(~hXTlqrq1DyQxpx`Ws?;}w(&lJEu1v;pcj1mBbk8XyK(*`I>w_mYl=I? zYU5FHAmi%%9H%9WSwtCD7!qq z6W;~CR|)Oeplug(MNx^}i021%7S>c?IYk^A*i|TTbruR@(H+Xh+I zE@YCwTc8rruHO)N#V%s!bvV?hW$i;BTtt=3zL|M`cqUt90v^Q)yvxKgCizbj*31$y zFo@Ye2k$@45M%%(%TvP5g?)`sR7XT09G6d9gTOF(VBeoNF05{TxIYen_cv&=GAFLu z-gZTx?uZis+z1>0ye}R_#Mv64wq?jk%{L^^a7UzJ>I6XpA?AlFy-xdyln(6$cu8l!*LpeDyDLWF$RxbuJIDRnO-P5c_n|s{Z@of8e zyBx9m^M?lV{dW4k|D|wJG>8AYVoMn-V`bVa+AFZ-Nfd=x0024qTm}MG4e&D|%Eb_o zf-Va1{gkxgWdS=IVncy7GlSg1hKjX3v5#B0h4!%_4$OI8ns$eS7IaE*=9&N$3 zw`?)DhxVD<${TIT2a-3$cm%2h;#;gsTW8v_%SfCQO+^F zdfDVH5z`o@Vm-#17byTuv@7_cKEBqF2vWr?BfN`=rQEMb2Trc2 zP)B~^l3G!5@_~_`{{60k%&e3&*8cGo;D%XRN>*lpYyZbr%%tPUr8@k-E*)s!y{Ob_ zPx?J{$XU8*ce_35xBbABoD3^+gB+2LUO8}p$%-{2X9|Y|i~I2Bk{1|{|1b9J zeXH~V2`F-V#64#0D9m{DB$JJw&U-tCd@v|J3=zBI>o7bT?eD!rIZ99sqDvh66waSq{ zyZp^adzE~7X;pt!YgJXrn(mvf7FV~S9FBr$IUWD59&_39_{z8AXF9y&ZMOGUTS&hk zpP(VbxiBIUkH9DoB_7Yf`U|HergTjAnGr)}9ntBO(aH5>!$obon*&`ajyqAupTG zm`vEz$hMF;SL+d6QCxN3zk%#48}9Q&W?8Eo7cD$`NBkG!OidDK?BaFTflTPyb(YQu ze{aDS%g82kF4)4G#Ytr6qqx_gJ^+kKNU)J~k`J5_;@gNOAJ3c^^&m@v#1dvJF`bPE zBtq=NJW!bFqz6gVH!(9{C@l;HiB%2WIpiFJ1)xMbA-Z}sIp!Vn7cH6(5Wr*YlO|3i z8MwB-z81o7%>N#n`~G|T_P!@)@%^ZOOxn^z33`A2z4%wZ_wz&Hw)^gtgZJLw7S?w} z`Rn{?{!1Y<&;N*J4$VhFueJZjQ~M_pyrA~#+pn(09vRItyi$6ZQ<{&J>`^j2f~!d)$~si<|&An?M4yjLNYYHsE~z3X$#aGF;t`X1n8;B zlI#lv6Xr{hIE%cl!(ruvJ70ugxV4LiQmpKiS=%5czPQuM%f#B6ZL`=bRwk^JFjSJQ z5>8iITF3pt%D0QX;GRQMyqLGJ)o6o`Z;%T6 zc7rH^h&5 z$apS@bV3j$y$fUmhH0WmSdu=o<{zs}dnOw)D$`yq({q8-An9HhMMXr^u_z5t^K1>R zl=BHSXVc_$zG8at!t6vVTW~LK$u#aoJ{%ek2Eb602mQMqW5Rq?0b@8#?Z(89@eGbb z8so%+dXmN}BDYgB zO+*e_`UJ%{`2UJmgGn#5`MRX-1By%n6({&QXaY5ir$Sc-TuhgaYlzv{oWP7z1M!yw z0l6Y@>Wlz?Q2EaWwPDS1{Gs5Q8Db~>%vclT4+*={e7(Xfmp)YSMVq#C(<;YMuJWJ! zK`IwlQrRe%&579>DDyg-P36)~`a!vDHtZvNrM>zI`U&vpk=>2Rh>W2PVHvQtfrXM3 zc@iDTy=ffBI7ALkKu*wI5;#D^x7UyK0d*ymLx8(|?b%kJ3WLtOysIQqS!h$}DEo6_ zKXcWU!12h8 z+YTLK-=J#aF7b1$x~5rmtCEWNYgjvxsE?jit>ax(L%djMgE*pEY$>)BizPjddZE_r z;h{sgp3WhxkP1|#Rns(!uBT|ae4aRm&r<6u3sXuWl64ptk?`|$3tour;{7BW>u9(d zwQWg=_a-K>oSEXjxsCTX>ijQyy<#wl3j$w*P?f`lCCq0DBE1QPxKLU}jP}X# zCdm^+^132q01}z;r5VC;reZELG{vyzh%6EGB~l-v&knE1FtHNo2kabpA;T8eFuEL_ zs89&A0Hh_D>S<~@YW*TYL{t*of=HLEW|&+~hZM$j1 zn3HznWPY%&OC=0kMFD?X;7_p-GtB@b2Ncd9Fz&;b8U9uE&1#Uf_O=fJ=eV$4h2htL zt>XO*i5sb~lH(M#qAWcp{Zen#ec%~_UzT|UF%H@CLm;XLM7qg?5e5m)i3TAP`nJ!4 zM7S&v3<0UCtBapz-zguX`uX>cvqi^(JK{`lX6@vZnf!oX8B@-_GjD@?#ti<}S8Hn7 zj^k2-tetl>WIg>fGR6Lsub-%m>&l1Mc-n*+`OiOZljUi3>=8CLziXT}@xI?77vM*) z%+1bzo_)JM$i}u?YuUB@G%HwN6LmJE2;u8NzSU6`AFej58?LaT+?Cg!S7%SEa^F~f zCO>m*pKZ9ptSBclxvJVvoAdLIu4?{_3BwhQy`7d^Lp14d{d|awD*afS4p$9Ib*9JV zGn#W$OMbjsKR`2H@UGc{RUdD>s#E%C$bBS4S`cFG8Hb z8Hn4sT{1QT{xcHcLNyW62cw{e#Y<@@-$WxhZjH#pgHmAX5Y*N--M?Cm|MTB+hss= zVZ;m(suUIrNG9aR!xxHlx(T*v)yz~))^Gx13n=d99z&@MxAkRHPs{Gv40)vOLeZx* zsV!_A7JG4Eb5Axw#k0e0*ueioApK9o1bW%8v5KQdC`d5=pem8SiN76rrX+jKwvzty zO17=ZE_o)x|AhT9z;Rz5w|(05`pM}Y?+GV2(K0=&uP+OW$@NVqpWvnC^v>0z)0N+T7ep@@NeoNGC*&AxK&2~HvMC7hbiBN`JnMt#u+`E zaC$5IkWLrp$#cb^f^bCwwGXz1IjTRCg&VeYWH%f*(2(7+t%1KZ6N&7laN8SMDQAp(!uFhCFeiQ>SRafZpjEjYtJ;C1_lWbT-;vA;{1EiP&n{c2 z0!W2{fI=~h%M`DvOGodK={zd?Um=$8AB~y$cjf|AhIySxHV{}hF?$m?UIuzSxlhH^ z4Z=kI58Waq?^-*)IK^@We0b|)^6@BizwLoZx)h@GTNzUmOa?S;QSD{Q|e zmiJ#bURQy$#?TKEG14?0e8LbBpphamU0_Z*N{}v5eh;Z3Ov46$3Bv||iB?%S-hSs9 z%FAcmdFL6iiZkwH-f;^Sj8k5wU(0<*or0GwolHZ1=rlG2dv{5}@ed-O1@D}--f2#f0$U;};B1!yuQ>nN#NB8$1u2b-8U zCcvKw1u~&98r#OjxZ)Ep{-h|grpVi9^dg`2#v8LrLAUHnc_}NP6#BGCP7oVI{o0 z!JbXZ`&@$mYmiVq_p;aEjLXFs`WOp*_hE*Y=K@2AwtWuDSbr_=EIFIgvGT42wz3}7(xU}S)wjjWrQ8DKRc zAk(NgL~h5Z?96n7i)?4@~{#yykkd@BJDjk@4dbR z385H+j(smx+=YB$MaA%XC2mz>`Lm!l`lwIb1^2=o_YU8w-@H{1?m`LKF!h-;B%WeK z1{yDIkR+go8{m&ctN{!(hWQqfIK~;c*Z^`(5NjEh96`oGMkz?@LfJ0Cn)o(~fpNee zzzL%@3AlDh9VfIbiWdM`7C-kJbC=ZleHoRD$*z0i_}s$q)TND^j-Sl5lDaipzIolr z6|L8gUEz-uvbyDMebZx4GWpx%=KH;S*Ys8vYD_+HYTPr3u)gE7e z=F)a0_P6PMn#AUh<7>VC{!_kp`2&HmGv=NFQToAcGb;Vcc(4D=mgAfJz?5R_*e>nW z&VwDV7*=nXHiUT&hR1~Yj%-{=79`AhWWHk_p8(~H6UokJ82aGiNEr4CnEDRucPVy_ z_j|xQj}_*QiQx-_n(`)phjzJYJUZ+5FzkxuFt$HoD3vmD!Vi$i``8|z$`eYoS z!`nR`EUnCgu~`hRZrU_%7JG)Z&?h4Ju|xED5>A<+>j?b^`n0f51zf+30X*i za1?tPLJr9}`^2d@8~-z7Vf^9|MNImj-ln&~MifE}SQH~aBF-_g=pwu=A*Lo+%-KEx=9 zm^d2d&>`ptrbZ3OFrRHe z0z+71DVpwjw%O#U!Z&8y>hUKLyuJP)2b%KuIOmonc6aI@v;eHA7Rgm;BjUoMPU8NjLMe!kF=<5tnWRtPhF}VD}qME--(rc<=`H&sky(9h9Tu0?**dR%Th&=# z;X6~*Y^&{r{5I7oXM1`|XXO6<%99&s_GI}LuuH10Q08iDwra}`SzB7Ql3XUGeWDHZ zKIviUyXXyg^Mp$qWkfA8664@wAQ{Gh#Cu)CmR3Y-Vz5j$pW>7txLK2-^xk0K#_tq1~gof6*S3I${sPypVNdb zlt1zhF>?aD)g$%j_vt;55PVX*v;=+1%*;aqFy!N5o?;e6>_3umlu&;{#{fGBMhb}1 z6SRgJi$cRekhpvy$O~T-T`Hh0c<&YuyO3q@Kk=>no%qXa_Zd9U#P{6$?lkqBtSrsq zbXu<9eb#i3jM37QZe_c!u%sxd+4-5c7bz|bm@6!AUP`znbUsssga zEB_Pd6)sdR{bsEHjNKhA@7}BQk6#gc62)pL{z^F`T^EI4p`{n3W@lJlA(s&-6*N_S zMQz&99p9OqDtA!1L-*wbCrn&FTkYwF>OgW6<<#6Oa&=V!+tvYW=u*LaB&_+EA&Agb z3^E8Np#i2)#l)ieG(A{KH!-rsAnXYNF9=0wB64nXCrQj??ZCTQ>PA{KTK6ja!@oY)_Zfjm>T_~WI+|lao4z`#% z+oDA?rop)rh}TD!FOSF?ee7Ih1QL)jDCr}ZJHk&MFRhTy$EakY285+X*nPkxkFUu= zat0EXnjpFmoMmv5fIbQ1J1KDjT}N39gegcEk1%c-+ZccZMI^QaK_s??;RtYbu?CBg zajrNT(aP5hXxFWF41LX5s*{Hfc1A7iq>ePT-dKs@s^CW%&sr&(qe-NTXjH<&JtRv)sEJt(1CDH~jfgBoFQqg)vlMWG(H|L#1SfM^wSC?x+>i2<)ia(afRB!T z-HTi{cfl@M0oJY)rPHO$C5RbFO9LTS#$#i^!$U(CmPtM+rH`;cCc&YaVM+xk6k%GK z1s3#bSPAf%g7EWSv<5 z;n}W{NpQ)u+!OuwMcv&OeLLFJlJeWG+Zt2z#wayucPME}4$J00UR9uHTl4ZythQga zbUXXmxzB|yR$Y);UGe&Q} z5f3s&S|FVzNp4~Nfd^lP%||pr2z`|@bsS{_iuj7^n#gDf{Jxl3NQnd zm{WJ~6{(tX#W&CX$+YQXA6j|cdWXyFXLsVJM$Ho!M-%0WHc#;Q{ENq$4&&;mo)Rlb z3b;#(h)qOvBs$lzB4Bx!!PsELwjL|&m7h8sB1?Qc|0v%&VFQ0H_#4Ya?(j(>o8&5K zE&OmI#JCP=Ibjo1^+w1|G({EB|ISbmtR=xvQxRq+SbAyB(~E)@Ispe*32npDF3!w! zdLSe~EI4{I=Y1e&Fn9CaSLv8Ebo3R!t56L&i@gK67>; zKb#x?6|;~C!G|ah+~FH;JlsbSAup+q$jcMDqfo$om^>>HsCM?+y`#b-L z1Pf;Ae1TOQK78X@X>=pxnNp-5O3$L5=fb``MOus(NQMWD9~#L^us_fy=6xdhELCJ+ zt-#ACd)$^xQYHvBOCkqp88B8rDj-Ed*HRefiV?>$jocz$R=#z1cx7#9*7z!$e}O+T zW#RtnlfwS|)srXP-oamc=~bJ2e8(k^T7UE`bDh{1t+r<@&pV}IYQ@^%y3(|81(=WW zS9*izuUx6L$_~Djq;?@w0(|m5Tn&0CWBLR09&f2Bt~&n&e@0I6#N)p`_)Or!<0321l0>6O3nv(C;UD3d6R5ODbQKkq3-k zHd~Xbq-64)NDh%p*JsNdcgXqIVS zGq_F!}$bh*aaDY31DIqKsfXgw_3^EHdacV zqzF^4@HUl@GKU@_u60F5c0+5VOqWp`?9h)g?`c1K`oYVVW%#E(1h~p0{P82-@_Q_- zup*yXoFyNzli9hfxWD5ge)AJo{Amu$HqZ)uR|>9Xda=BoRtAl|N2HVzT`$q=alrP* zOOZ_9l&xQRdfi=AXxp@hnES33ecpVeL0mrf3H~|?HA^qa*|m|768VZ=k^**%Vx9M) zh!nnSWbXj(^ZySYP1;d@s3u6EosdIM5LT7bq)qr9Mhm2$>X8~A2JuBFyi*BpRKiP@ z*iLX!C$^7XelG77n?K^oD(Q$sI_lz=3MZD~(2{>4Tv_oG#Xi%qCF3mkx0tc`9B!^- zIJ=U4U85Xd$p)&LsaNU2Y3C#(ad=JBIP zk)uaFFvN}oD#>3Vcu*CK2$&EkacOHnY#?WrI8-mTxUm(_Hj`DfQ{oC(Ow1(GUfHCI zIU>$(Pb%j?(z>xfD$aRPz}7`RF9mM07y=dxyb5fxXa?v9QMSt>ft$>UEtryk44}bX zkWge=5Gmz|A9A_mIvNb)f0kVi*k7Te*E(yRat)1*@%J%fp`3nDL@Wcp1C?W3Ekqqr zoTa_c8UMQ6?JNvC<2#&A`AnyFAFV$Wf5;gwr0q)jL46bn^%*ehV@xt4imAQ{Y3vlL zk6ee;zlW$k$PNEzhc*ordjZvnKks047=Kis+fg5|T8R3f61e*REAC4GBPpu>r;a%@ zxwH4O8}`0QCfSQaHs>a6vdLzWEUX}OW;)4`PWRB$v)L>nD~BkEc%gzQ-UleMir@_> zhzfY1;ED2iqNoU-%WlH=d)3{ONfi0`{2zPtyn20ASH0u+UR774FUe=&a{M0LvuElk z)d#nwI1rpQQss<7D{9F7LMC3QJl8uC zSUeev=gzFL%mYx1hIXY!Y(S}pt0{wOfcN+}j~x|*M~@vldg7L&$Ji}>S1bwcSaL-l z3p{rVoXQ|R4yw*i-0F@8m#YiSDzIDq(~ljMgBR@CbHVg2Qvvcxc#fHM+AA>&ttHTn zZXi^85<`HdhPP_MYgZTAPXX9DpW?)bFji1rt8$G&wb)Q1xEAkhLy+dxNPG_U&~WgR zuRQXp*tvMKrJTgZTg3hKSKbb=;eMJtIquydxpB-TfjYMRnGpHC+2wSCYIK@oi*Ys5ba&(T}bNl7GSD(uy=$; zL!;`QemGWIZ-urLdKNFa;=iWlt-A7l6=jlK`Jvy`GoTR>Qkmr6=W}QS@k6~TLVU5q z=rsSb=m%bqHIg%oKPX1z&b!X8bA*Z7HlAW>~Vl?h5{=v%C3@S?lyqe_{IZ2iXa> z?vY2PfAGk#3S7jw?n3=oLe!qU9{-!Ck3ICr^>2LyLC!`m^1ym6UYt4%YCYy2Hx(p& z-by8*3P5jUVrBKo)yt5##S#_>(AHudG72szGvoqAEcN;Vw7d}$5~y-+?%7C=B#(b+ zGV1JoHX404e_PpMgG5kZLEEnA7L%IM~^C?y464Dyl4oz0?d#RURwgmSv z#_QQbOM|P<+HzuK%ULUfOW8wPZk+1I!kOrpz-MCPxYm)NjqacNxgs`B|KZCTz~6J4{eJX+Iw=F7@^5}Ot*W;|4} zaO2LsqGLnH)O|}!7xmSjeg_Hmji+x~HE+S?RV#-_m)9(fg)UotWW~Z|u}EpnrX;&( z`n=MG^DCbuvvDuGv~(j4OFmp(J{_o8TUJ%GC)8eDRk0SIr9b(B?@raMj-WPYRK^x8 zT)lE-jR>&|wydni0+UNO)|6MRt=W8fRWumgulrH=FMMs z#%SGAC>RX~Rj*yYv-!%{4e`oTmRFTLeM4E*=H(Toq1bf$G^Y1+R0q-Jz3h7x!Lm|( z;fDd2qhsOkU+kTf0lsnTo;_Qq9>uyuQ;!kJH-HR1I`tTF9#x!;Ao&K6%Fg~jS!XK{ zcpmi^$oDSBHCL?R7cYr)mJ!r{4)ea(EXF*++1ICm9EGV!$_w9GpxdxO%Yn_}$hNH~jx6sy zZ5luyJ+_eO*!$veRzlROu~lb_i5CbmO@lRA^gK~bd@-5%hY|l zH&4HP+2F2xc>akCPCC13>-706!4KZpgpZl|v3u^CQ*NrdDt`46( zc+cR;;nm^x4RLvY?Se~o#unXN8(A8^c~Q(gtgB42e|H$4RCAJp@uOESDrqlUwNAz# zUJN zdmSQr_x>fXLF6NKckQ8Njoz#>eQ}Ft*=egC9udra&mmgGCEV1*{zw{h>pXG zuarYk9RZ>bR~ZT9aea-h)~Ao;4Gl!Qx|gGVZFMiI*%s8T7W4mR*DUAp>hbn^BmVnO z5NNkdr`@tg^xDLhy4q8{dgVaXZ*JU7b$#KA4XVe>p@Hb3YjNW`ER{tSpue?&z@TpD zGL~6eS69EKuC5N87kWk-pU0o)M`%{B*SCt}Zp4t{(g43;k_8!-fMpS&fEX*m#|27Z zhsqft_9|A5sY3JciueMy5Z{zp%$DE-Str40EoZfu*1iJq=_-bx4;g`VY&|}Ovk`&C zW|*r}@McpzOJE*FBMkmFww>)@JK3p#&8OpQW;A7R7i+>yoEFx~+VF*q-K+y2dN_;i z!ABmt*`rzUUM>GD`yBf`@~mHE zUt)J-b;x_!m)Td?ee8brRrUb;8v8o?278b_#2#kfWZz=nX5V3tu~Z!}_A~YbUQ+%!`vv{{=ojoo@Rey&#-6N zpV?p7U)kT--`PLdG4>o*^E|<(*fe{d!yseQh~Sb3d5DL3ghzRdm+&|*lnU&fd7T7EKL!B_HCd^JZ3ny=&Q`3Am`Z{nNz7Jdq^ zmD_@n$W{(b%f{zLvF z{$u_V{y6_B{~3RR{}=x`{{{ag{}ul={|$eV|CaxbKgEB~|G@vqpXPt!&+upYpZQ<- zU-{qo-}yiIG5#Ds&QI_uKFyyO0j%zWwWG0cN>GGESVTlr#6$@u$drmQQ7$S(rKl3s zqDIUU^Th(OP%ILQ#S*bp;FAzyxu_Ko>(i^iS=TG*eEuM&0>o=dVp)5PiG4Dm8?rr0H#M6+lSt)fk|i`}9_bc(aY9?>Pb#a_`P zdc{7`Cz7IH42VIoUz{z@5eLM}#kt}<@d|OixIk!PNTfts=weumh*6Od7m6`qU;?x$ zERhu#30vfZBl6;)m=K4=q&O@t7OxbSh*ybA#S!spahbSWTp_L$uMt;?*NWGPtHm|q z_2OFb263Huqj-~evv`YmtGHggO}t&aL);*46z>!_iFb*2i<`xJ#Cyg2#QVhu#0SNP z#D~R4#7D&~;#To7@o{mR_=LD!92K7wpAvV7Pm4RnUE(w1v*L5&^WqEQi{eY-ZgG#e zSA1D~McgOu7he?*h_8vSi*JYr#Y5s@@lEk9@on)P@rd}Y_?~!FJSM&`ejt7*ek6V@ zej*+hKNUX{Pl*2#KNr6czZAa`zZSm{Pm14)--)Nh@5LX)AH~z+PvRNztoXC|i}~?_EoW3WJYiN_fUM-)~Wm`Jq z$fR~q54IS(W`|mJLwEF0pFVCK)J0P|9ct0clx~EYQYk&_$ZpHlWuKn5TC{Q9*1}2MaWdvea4?hBEt#A&Q;}9{!Za)`ExWah zA=@)%T6XA0HZ+hWZm@0Kx-b*&pRjrXP@-Kr>en)=IVE(ocPrG_N{z{+j#FD~D;w{# z9L>?)TZVddD{JV%{g!Q{WxJt`l&BWaC%Q7`Sa7#t4e9YF$I()w-U$aRYrNBpHe1%% zxMq*#P{UMyTsNJ{UfXi?l%uD8j-*P3oAU-*Di?0C#_6z_s#*rTbV&=4jh1pc&0#Cl zp_wCSpTUei(PS8*ZY|rJF@wFMmZ^u56B#EpiYutY(dH)FOJG0WZAJFzlS3AolqxdT zr4Kt%BKio-#fWOQ9LE}09p&iZK7H8MbE6@*5yHvQ{P3`$%dSigy*p}Uqe=8@%0U%M zdh879t2uy2q0aHFWjjRq}thkW5JlhG zXfWN5Q!C;k#w02OF@u0mhlL@0R^D_(OWp~&-G*!DAYttp1E)#CxgjMq3GZPv(}@-? z7EKxuJ*uxv;(5^v1boA|M^iSNi=b0l?aVujr4^gxxqPwkr{7q|Nb7d6DWA?*lDfAP0-4j(En1eEwv?KvaNCkz-5$^62$JTa9q29&zEb=i!Y#=D`J{g!+GaBC)KWX7nc$Mjee zE^8_gDN$q#I$F1*iY5_~B;{e+^yHlg-3;xgM|+%6-5$u%)1=2Ura*g?DQn;g4JsX{ z258k&wyx!LRp>O`LWK4tGl%tPi*6W7u0_8#5=AHKn$u(I9;GBN@6b~u9nFp=Ep!P{ zF-SoQVTEJavDSoN!lW$ky!1sJbvIr|gDhigjmFPfxrQ1ZMzL+Vwien2A=)hTeI!3n0Ju2Qz=EUpWDC}N4sYyC=K)0=C&CZ4J zDEK9tM8#n?9~o7=b@*j+@BfFM4OGg?pT(ibl$|H zioxE!v1S#ana~-}JjC|pGrH4(>*gX&89QreFc$sV5bRA9yHaC&VUbE&M)iZXWs)U@ z0x5bRtL%A^*;e*C;u+waXMssCd<*IJ7-UbG38mOd{ga#E9IR!iyo_P6!* zp98IgO*i3W$_KJdHmrVfG&Ag=Onx<=j=^W*;cz)q>mPGO1F(oXi>-$MYsdFvS;kx&rkN z3BKzJJIyilFxAep|24bqOg5{NN_GH+9EN$)j8JkC-eR20aLzJeR1fQ=uH-hEY4QOc zBxu8v_6!r$$>mB1vT5`+dIF|N_Z)e#Z->p!W=!9GE5j6rS+pP{Bd|G!>!M~%SFjE3 zY*)?H4@n9qlS1tVfQ3?Hu9wQeOBFn0sdum|4$*FXL{nC;$Ws-Za!@&_5S5#9B~7_3 z>`NcH`69SRK*_@hP=s{bq#kw=Ng}*ovrSao*BPalZRvbU0fjhxXBH2O=dzt(zpcSu z5UPTCIjAcH-f~ZgYCKvLtb!x3!9XscU=?=8tkKcuaaJC0^O&~ zW_6Kj?A1t2=cvmlY`Jciko!jMNYUecAapwk#|Fne$F=d6mQb=450NgT9`c|hPKfpo zc3Wl!Jss_U(NK+C-U`f|(!1c$EugkkWpR101X4F zUd4=%!oouh1ZpSAWQyi|F47NtLrPud0y}h;x=U%HNS9YAyu24i2Twxqfm~<>L=m9J zbo%WKFet#VGC8FLb_#Ab(q^W6Ef|k*2Mn=gPe%OH0iLp5+9dQP0(DRql0egAak$8w zW9L=pQP<)QK>g-e300H8J5DU0(MmF+iPC}zp<>yrQUYFIT?}r%bcTcn4wF{*XR@)D zQ5#_fS+JPvtO>cq-KTJMwWL)ahP%;i=yFA~=0GMU2~>`tL3TzxMtxlfe$Ld>Vf;`< zVs4R0c$S0x)(DyjT@dk3sPl<>pGf#bgHJU2#8#i!<`dg}Vuw%coLjfLxs3X`jKo|< z!(2w=T*lV9jBRrn+vhTNlzNX$*2AuwRjQv=O3W%X%qlg`Ds7!r+BU1SeO75lNfNkK zGYCc17tx6#x}k_}ETXqox#z-yKr#ta>FFSWt$MXhsBJ?{pQVvm?j$@64-Wm%jB=0J z3gODZ5Lcg-HT$e_&Ac$9oFO3&F_3I7n=Q>Q*e}>IlAhV&wHry#iF03KbUQ1WEskCP z*HLRZDNUzY2yqbgAeKhu3$dvSD4^36h9U$7H4dPO#{uN@L&^{=euiSf|4}^YMM)G% zrnGVBor)yk%}JqEyi^=T`f;N~z(4$gVI$0e7cJ)gIb5U&QALCQDhyP?AHt_0_y!kF z&xC%v5%K=pfX_2M)M9SXR~*yD2FzhE=`j<-{T~nC3K84?jp*!O$6*3dZhE39YzpqSt$g++1YK^1$4|n%Xs(2^6Xee*nLyDNr2$I% z5F}V`m7)~2PiV->#Eaoc8O4;PT^-4yUO(gT!t zh*F?Ji9O&RrF&kj^k8SRu8qgl7C8Y5&r8~MJv{_x3ttLVO6#Rvl&m0di1dRaDXmon z^yCHY3g{zYQ^bPr&d>uFv6A^Ahf+KhGbgqJvZeK!8AkG=JOVu%@o^Coh@Bt?`l)i} z$Tl2l^jp<49BSGeg*RpBK^sT+APbYyj4+|OobH71Yas4+4N)T4X=d|IP=!-Kyk01j z94{eAMhM3;s9U|DT18-J^g_Hni$Ms?jnRq&6UFgEm4Pf`Hvf8rlZuLTI*R4RC!-v3 z@nSAffs8^cV@bu$!l=^M32S--P$u z5bU#tEGOKaaVU~Q+Dm11lj9*|Lq;;`@PIjHqWaM$B!b8cJ8ry7dJGt+jhGfW(x~bm zXoej_@|0pJ$WED?c=3~{F;#07^@reXbt8>F8MYD;LLijRLlz?EM4Aw`*_qr}OvO&k zT6#o}xr#_I-y@Y5bEAwzITB42&~;}h*wWLLg^aGxsVTNTKC?*6d{$*4-|BNJh@<+; zyLeUGp)3>*AYJ#5iS4$0GEPt67Z%Kc=RSYQzld=BeQ`>m_=O5TpW-uo32BHY4Qrws z5tL>2m{FyyLwRE;)SEwi*w72 z^tQH_K)q}S$>TBIoCWQ6mK6ib`nn*FvBpb0!Giu$O57epVyic!C()%yM?!;BW27Zv zMNwJ)?vk`bP3#=9tr_G}91c4Vr(2L_~?z@1BgLHrn_ zh^{Z96Ge1G5#3lsZ!My?717&^=p9A$PQpIkee3HeU!BLGh8wko+5wPZ#Z`Ez5<>7P z{h4vS4sqy6q$T4_($G=VClaw1;RC662!5F_6kK?10pO9?0%+_mf=hC@&nUWOD}^*FWB6zz3<5+>(1Zo$2jOIyOeH z901e&$Y=ly| z7yYWplcWlnZD+pA^U?fVwukC{HUMnJo%R-%q5DpKpY?Wp#u zzQN$CfAl~$q6`l8lOj|&{wUN<6X4A4rr=~t@@4GI!WD*9qGMaYeX8y5Z)LJXLcG)TB_g zYTwh-h194t0v&?mIek(mbggDwz-8LjLCpZdfP+#G7$!7;#zvkuMKLOQxj6DtYD|KB zUx-46eybcFuo$+9-JR_b)o;3;2(@YUP(BTdH&TG>OaGN-`jR@oHIJbAP%q38Mm%UR zJBi0kN6FZ@?G8<+QSRYk0n0c!S)wjPs*?b;b7NI^XK|?^;fJBB z0~2+#iVcZy+d+8R`b5}8C<)jUn*D=-6hn@PM`*CuD^TV=OQ9?F)X?~+W?)W6ZUSRH zE)$(aDTglfR}Nz-1)BV2u`wD#&p(eh5Eu$^{GX3?eyPNShAlq0 fgi=85r6cH%`u*`kchh&c0#EEc^;-A$jHmt^2)>LU literal 0 HcmV?d00001 diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 3d19efeae9..0113973ab6 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -1147,6 +1147,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Measure_TextLayoutSymbolWithAndWidthIncludingTrailingWhitespace() + { + const string symbolsFont = "resm:Avalonia.Skia.UnitTests.Fonts?assembly=Avalonia.Skia.UnitTests#Symbols"; + using (Start()) + { + var textLayout = new TextLayout("\ue971", new Typeface(symbolsFont), 12.0, Brushes.White); + + Assert.Equal(new Size(12.0, 12.0), new Size(textLayout.Width, textLayout.Height)); + Assert.Equal(12.0, textLayout.WidthIncludingTrailingWhitespace); + } + } + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface From 11b5d421c426b56485b1063558931b41b5583663 Mon Sep 17 00:00:00 2001 From: pjt33 Date: Wed, 26 Feb 2025 12:23:35 +0100 Subject: [PATCH 02/23] Fix SlicedStream.Seek(offset, SeekOrigin.End) (#18313) * Fix SlicedStream.Seek(offset, SeekOrigin.End) This fixes https://github.com/AvaloniaUI/Avalonia/issues/13604 The offset _from is applied in set_Position, so applying it also in Seek mispositions the stream. ZipArchive exposes the problem by seeking to the end to read the table of contents. * Add tests for SlicedStream --------- Co-authored-by: Julien Lebosquain --- .../Platform/Internal/SlicedStream.cs | 2 +- .../Platform/SlicedStreamTests.cs | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/Avalonia.Base.UnitTests/Platform/SlicedStreamTests.cs diff --git a/src/Avalonia.Base/Platform/Internal/SlicedStream.cs b/src/Avalonia.Base/Platform/Internal/SlicedStream.cs index 124c248aa8..86a8dd5da9 100644 --- a/src/Avalonia.Base/Platform/Internal/SlicedStream.cs +++ b/src/Avalonia.Base/Platform/Internal/SlicedStream.cs @@ -29,7 +29,7 @@ internal class SlicedStream : Stream if (origin == SeekOrigin.Begin) Position = offset; if (origin == SeekOrigin.End) - Position = _from + Length + offset; + Position = Length + offset; if (origin == SeekOrigin.Current) Position = Position + offset; return Position; diff --git a/tests/Avalonia.Base.UnitTests/Platform/SlicedStreamTests.cs b/tests/Avalonia.Base.UnitTests/Platform/SlicedStreamTests.cs new file mode 100644 index 0000000000..dbcabf997c --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Platform/SlicedStreamTests.cs @@ -0,0 +1,29 @@ +using System.IO; +using Avalonia.Platform.Internal; +using Xunit; + +namespace Avalonia.Base.UnitTests; + +public class SlicedStreamTests +{ + [Theory] + [InlineData(2, SeekOrigin.Begin, 22, 2, 9)] + [InlineData(2, SeekOrigin.Current, 22, 17, 24)] + [InlineData(-2, SeekOrigin.End, 22, 40, 47)] + public void Seek_Works( + long offset, + SeekOrigin origin, + long startingUnderlyingPosition, + long expectedPosition, + long expectedUnderlyingPosition) + { + var memoryStream = new MemoryStream(new byte[1024]); + var slicedStream = new SlicedStream(memoryStream, 7, 42); + memoryStream.Position = startingUnderlyingPosition; + + slicedStream.Seek(offset, origin); + + Assert.Equal(expectedPosition, slicedStream.Position); + Assert.Equal(expectedUnderlyingPosition, memoryStream.Position); + } +} From 85ffed056a2f74fe344b46e444d9743d1e403775 Mon Sep 17 00:00:00 2001 From: Alexandre Mutel Date: Wed, 26 Feb 2025 13:40:20 +0100 Subject: [PATCH 03/23] Remove `List>.ToArray()` allocations in `LightweightObservableBase` (#18316) * Remove `List>.ToArray()` allocations in `LightweightObservableBase` * Cleanup code * Remove unused code * Remove space --- .../Reactive/LightweightObservableBase.cs | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs index cf20f20172..04759e314d 100644 --- a/src/Avalonia.Base/Reactive/LightweightObservableBase.cs +++ b/src/Avalonia.Base/Reactive/LightweightObservableBase.cs @@ -1,7 +1,7 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Threading; -using Avalonia.Threading; namespace Avalonia.Reactive { @@ -118,32 +118,59 @@ namespace Avalonia.Reactive if (Volatile.Read(ref _observers) != null) { IObserver[]? observers = null; - IObserver? singleObserver = null; + int count = 0; + + // Optimize for the common case of 1/2/3 observers. + IObserver? observer0 = null; + IObserver? observer1 = null; + IObserver? observer2 = null; lock (this) { if (_observers == null) { return; } - if (_observers.Count == 1) - { - singleObserver = _observers[0]; - } - else + + count = _observers.Count; + switch (count) { - observers = _observers.ToArray(); + case 3: + observer0 = _observers[0]; + observer1 = _observers[1]; + observer2 = _observers[2]; + break; + case 2: + observer0 = _observers[0]; + observer1 = _observers[1]; + break; + case 1: + observer0 = _observers[0]; + break; + case 0: + return; + default: + { + observers = ArrayPool>.Shared.Rent(count); + _observers.CopyTo(observers); + break; + } } } - if (singleObserver != null) + + if (observer0 != null) { - singleObserver.OnNext(value); + observer0.OnNext(value); + observer1?.OnNext(value); + observer2?.OnNext(value); } - else + else if (observers != null) { - foreach (var observer in observers!) + for(int i = 0; i < count; i++) { - observer.OnNext(value); + observers[i].OnNext(value); } + + ArrayPool>.Shared.Return(observers); } } } From abd7a88608c21d5efd3cb4054c1794c8d5e578c2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 26 Feb 2025 18:38:12 +0500 Subject: [PATCH 04/23] [DRAFT] Send dispose command for CompositionTarget as an OOB batch (#18119) * Send dispose command for CompositionTarget as an OOB batch * Make Close_Should_Remove_PointerOver to provide some render interface stub * why ins't reactive stuff using our headless testing? * fix? --------- Co-authored-by: Dan Walmsley --- .../Media/MediaContext.Compositor.cs | 20 ++++++++++++----- .../Rendering/Composition/Compositor.cs | 22 +++++++++++++++++++ .../Input/PointerOverTests.cs | 4 +++- .../AvaloniaActivationForViewFetcherTest.cs | 4 ++-- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Media/MediaContext.Compositor.cs b/src/Avalonia.Base/Media/MediaContext.Compositor.cs index ffe73c795c..32ee1ab932 100644 --- a/src/Avalonia.Base/Media/MediaContext.Compositor.cs +++ b/src/Avalonia.Base/Media/MediaContext.Compositor.cs @@ -98,6 +98,13 @@ partial class MediaContext if (AvaloniaLocator.Current.GetService() == null) return; + using var _ = NonPumpingLockHelper.Use(); + SyncWaitCompositorBatch(compositor, CommitCompositor(compositor), waitFullRender, catchExceptions); + } + + private void SyncWaitCompositorBatch(Compositor compositor, CompositionBatch batch, + bool waitFullRender, bool catchExceptions) + { using var _ = NonPumpingLockHelper.Use(); if (compositor is { @@ -105,12 +112,10 @@ partial class MediaContext Loop.RunsInBackground: true }) { - var batch = CommitCompositor(compositor); (waitFullRender ? batch.Rendered : batch.Processed).Wait(); } else { - CommitCompositor(compositor); compositor.Server.Render(catchExceptions); } } @@ -132,10 +137,15 @@ partial class MediaContext /// public void SyncDisposeCompositionTarget(CompositionTarget compositionTarget) { - compositionTarget.Dispose(); + using var _ = NonPumpingLockHelper.Use(); + + // TODO: We are sending a dispose command outside of the normal commit cycle and we might + // want to ask the compositor to skip any actual rendering and return the control ASAP + // Not sure if we should do that for background thread rendering since it might affect the animation + // smoothness of other windows - // TODO: introduce a way to skip any actual rendering for other targets and only do a dispose? - SyncCommit(compositionTarget.Compositor, false, true); + var oobBatch = compositionTarget.Compositor.OobDispose(compositionTarget); + SyncWaitCompositorBatch(compositionTarget.Compositor, oobBatch, false, true); } /// diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 4190991b25..257e41f2d6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -211,6 +211,28 @@ namespace Avalonia.Rendering.Composition return commit; } } + + /// + /// This method submits a composition with a single dispose command outside the normal + /// commit cycle. This is currently used for disposing CompositionTargets since we need to do that ASAP + /// and without affecting the not yet completed composition batch + /// + internal CompositionBatch OobDispose(CompositionObject obj) + { + using var _ = NonPumpingLockHelper.Use(); + obj.Dispose(); + var batch = new CompositionBatch(); + using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) + { + writer.WriteObject(ServerCompositor.RenderThreadDisposeStartMarker); + writer.Write(1); + writer.WriteObject(obj.Server); + } + + batch.CommittedAt = Server.Clock.Elapsed; + _server.EnqueueBatch(batch); + return batch; + } internal void RegisterForSerialization(ICompositorSerializable compositionObject) { diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs index 62c99a553b..45277ca75f 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using Avalonia.Controls; +using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Rendering; @@ -22,7 +23,8 @@ namespace Avalonia.Base.UnitTests.Input { using var app = UnitTestApplication.Start(new TestServices( inputManager: new InputManager(), - focusManager: new FocusManager())); + focusManager: new FocusManager(), + renderInterface: new HeadlessPlatformRenderInterface())); var renderer = new Mock(); var device = CreatePointerDeviceMock().Object; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index a4e669cb34..c25816040a 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -153,7 +153,7 @@ namespace Avalonia.ReactiveUI.UnitTests [Fact] public void Activation_For_View_Fetcher_Should_Support_Windows() { - using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (UnitTestApplication.Start(TestServices.StyledWindow)) { var window = new TestWindowWithWhenActivated(); Assert.False(window.Active); @@ -171,7 +171,7 @@ namespace Avalonia.ReactiveUI.UnitTests [Fact] public void Activatable_Window_View_Model_Is_Activated_And_Deactivated() { - using (UnitTestApplication.Start(TestServices.MockWindowingPlatform)) + using (UnitTestApplication.Start(TestServices.StyledWindow)) { var viewModel = new ActivatableViewModel(); var window = new ActivatableWindow { ViewModel = viewModel }; From 418e15d294663f8e7c1313ba9d0e84c5c7835e8e Mon Sep 17 00:00:00 2001 From: Jonko <69772986+jonko0493@users.noreply.github.com> Date: Wed, 26 Feb 2025 06:20:46 -0800 Subject: [PATCH 05/23] Search all SelectingItemsControl items with TextSearch on key input, not just realized ones (#17506) * Modify SelectingItemsControl to not just use unrealized items * Fixup null deref * Get unrealized items searched in comboboxes * Fixup one small comparison bug * Reset file that shouldn't have been changed * Try again * Revert frfr * Revert frfrfr * Fixup per PR feedback * Remove documentation from internal method * Remove unused usings --- .../Presenters/ItemsPresenter.cs | 40 ++++++++++++++++++- .../Primitives/SelectingItemsControl.cs | 28 +++---------- src/Avalonia.Controls/VirtualizingPanel.cs | 6 +++ .../ComboBoxTests.cs | 21 +++++++--- 4 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 63e512492e..58d0e797fe 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Layout; namespace Avalonia.Controls.Presenters { @@ -198,6 +196,44 @@ namespace Avalonia.Controls.Presenters return v.GetRealizedContainers(); return Panel?.Children; } + + internal static bool ControlMatchesTextSearch(Control control, string textSearchTerm) + { + if (control is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty)) + { + var searchText = ao.GetValue(TextSearch.TextProperty); + + if (searchText?.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase) == true) + { + return true; + } + } + return control is IContentControl cc && + cc.Content?.ToString()?.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + } + + internal int GetIndexFromTextSearch(string textSearch) + { + if (Panel is VirtualizingPanel v) + return v.GetIndexFromTextSearch(textSearch); + return GetIndexFromTextSearch(ItemsControl?.Items, textSearch); + } + + internal static int GetIndexFromTextSearch(IReadOnlyList? items, string textSearchTerm) + { + if (items is null) + return -1; + + for (var i = 0; i < items.Count; i++) + { + if (items[i] is Control c && ControlMatchesTextSearch(c, textSearchTerm) + || items[i]?.ToString()?.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase) == true) + { + return i; + } + } + return -1; + } internal int IndexFromContainer(Control container) { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 636b4bd576..f830131916 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Avalonia.Controls.Selection; using Avalonia.Data; -using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Metadata; @@ -115,7 +114,7 @@ namespace Avalonia.Controls.Primitives /// public static readonly StyledProperty IsTextSearchEnabledProperty = AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); - + /// /// Event that should be raised by containers when their selection state changes to notify /// the parent that their selection state has changed. @@ -610,29 +609,12 @@ namespace Avalonia.Controls.Primitives _textSearchTerm += e.Text; - bool Match(Control container) + var newIndex = Presenter?.GetIndexFromTextSearch(_textSearchTerm); + if (newIndex >= 0) { - if (container is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty)) - { - var searchText = ao.GetValue(TextSearch.TextProperty); - - if (searchText?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true) - { - return true; - } - } - - return container is IContentControl control && - control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + SelectedIndex = (int)newIndex; } - - var container = GetRealizedContainers().FirstOrDefault(Match); - - if (container != null) - { - SelectedIndex = IndexFromContainer(container); - } - + StartTextSearchTimer(); e.Handled = true; diff --git a/src/Avalonia.Controls/VirtualizingPanel.cs b/src/Avalonia.Controls/VirtualizingPanel.cs index 1cd676ed4f..c2ae8445df 100644 --- a/src/Avalonia.Controls/VirtualizingPanel.cs +++ b/src/Avalonia.Controls/VirtualizingPanel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; @@ -192,6 +193,11 @@ namespace Avalonia.Controls Children.RemoveRange(index, count); } + internal int GetIndexFromTextSearch(string textSearchTerm) + { + return ItemsPresenter.GetIndexFromTextSearch(Items, textSearchTerm); + } + private protected override void InvalidateMeasureOnChildrenChanged() { // Don't invalidate measure when children are added or removed: the panel is responsible diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 413ce22e71..00a31fafe7 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -198,9 +198,14 @@ namespace Avalonia.Controls.UnitTests new Popup { Name = "PART_Popup", - Child = new ItemsPresenter + Child = new ScrollViewer { - Name = "PART_ItemsPresenter", + Name = "PART_ScrollViewer", + Content = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel()), + }.RegisterInNameScope(scope) }.RegisterInNameScope(scope) }.RegisterInNameScope(scope) } @@ -243,23 +248,30 @@ namespace Avalonia.Controls.UnitTests [InlineData(-1, 2, "c", "A item", "B item", "C item")] [InlineData(0, 1, "b", "A item", "B item", "C item")] [InlineData(2, 2, "x", "A item", "B item", "C item")] + [InlineData(0, 34, "y", "0 item", "1 item", "2 item", "3 item", "4 item", "5 item", "6 item", "7 item", "8 item", "9 item", "A item", "B item", "C item", "D item", "E item", "F item", "G item", "H item", "I item", "J item", "K item", "L item", "M item", "N item", "O item", "P item", "Q item", "R item", "S item", "T item", "U item", "V item", "W item", "X item", "Y item", "Z item")] public void TextSearch_Should_Have_Expected_SelectedIndex( int initialSelectedIndex, int expectedSelectedIndex, string searchTerm, params string[] items) { - using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + using (UnitTestApplication.Start(TestServices.StyledWindow)) { var target = new ComboBox { Template = GetTemplate(), - ItemsSource = items.Select(x => new ComboBoxItem { Content = x }) + ItemsSource = items.Select(x => new ComboBoxItem { Content = x }), }; + TestRoot root = new(target) + { + ClientSize = new(500,500), + }; + target.ApplyTemplate(); target.Presenter.ApplyTemplate(); target.SelectedIndex = initialSelectedIndex; + root.LayoutManager.ExecuteInitialLayoutPass(); var args = new TextInputEventArgs { @@ -293,7 +305,6 @@ namespace Avalonia.Controls.UnitTests Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); - } } From b815659435341b06ea36a373724c9f27306ce7e9 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 28 Feb 2025 10:27:06 +0100 Subject: [PATCH 06/23] Fix Window.MeasureOverride measuring with the old ClientSize (#18338) --- src/Avalonia.Controls/Window.cs | 15 ++++++++++----- tests/Avalonia.Controls.UnitTests/WindowTests.cs | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 9b0428d446..c771cb2af0 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -1057,8 +1057,13 @@ namespace Avalonia.Controls { var sizeToContent = SizeToContent; var clientSize = ClientSize; - var constraint = clientSize; var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity; + var useAutoWidth = sizeToContent.HasAllFlags(SizeToContent.Width); + var useAutoHeight = sizeToContent.HasAllFlags(SizeToContent.Height); + + var constraint = new Size( + useAutoWidth || double.IsInfinity(availableSize.Width) ? clientSize.Width : availableSize.Width, + useAutoHeight || double.IsInfinity(availableSize.Height) ? clientSize.Height : availableSize.Height); if (MaxWidth > 0 && MaxWidth < maxAutoSize.Width) { @@ -1069,19 +1074,19 @@ namespace Avalonia.Controls maxAutoSize = maxAutoSize.WithHeight(MaxHeight); } - if (sizeToContent.HasAllFlags(SizeToContent.Width)) + if (useAutoWidth) { constraint = constraint.WithWidth(maxAutoSize.Width); } - if (sizeToContent.HasAllFlags(SizeToContent.Height)) + if (useAutoHeight) { constraint = constraint.WithHeight(maxAutoSize.Height); } var result = base.MeasureOverride(constraint); - if (!sizeToContent.HasAllFlags(SizeToContent.Width)) + if (!useAutoWidth) { if (!double.IsInfinity(availableSize.Width)) { @@ -1093,7 +1098,7 @@ namespace Avalonia.Controls } } - if (!sizeToContent.HasAllFlags(SizeToContent.Height)) + if (!useAutoHeight) { if (!double.IsInfinity(availableSize.Height)) { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index ce2915557a..cd43282dc7 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; +using Avalonia.Threading; using Avalonia.UnitTests; using Moq; using Xunit; @@ -686,10 +687,23 @@ namespace Avalonia.Controls.UnitTests Content = child }; + // Verify that the child is initially measured with our Width/Height. Show(target); Assert.Equal(1, child.MeasureSizes.Count); Assert.Equal(new Size(100, 50), child.MeasureSizes[0]); + + // Now change the bounds: verify that we are using the new Width/Height, and not the old ClientSize. + child.MeasureSizes.Clear(); + child.InvalidateMeasure(); + + target.Width = 120; + target.Height = 70; + + Dispatcher.UIThread.RunJobs(); + + Assert.Equal(1, child.MeasureSizes.Count); + Assert.Equal(new Size(120, 70), child.MeasureSizes[0]); } } From 61de7e36b1107e21276b8c59660ca0aae88d990f Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Mon, 3 Mar 2025 03:10:32 +0100 Subject: [PATCH 07/23] feat(Window): Allow to persist content of Clipboard after App close (#16778) * feat(Window): Allow to persist content of Clipboard after App close * fix: missing iOS Platform * fix(Api Validation): Missing suppression * Revert "fix(Api Validation): Missing suppression" This reverts commit 04b4e0c634ca70759969e098b581d7e1bbae802a. * fix: Address review * fix: ValidateApiDiff * test: Fix build issue --------- Co-authored-by: Julien Lebosquain --- api/Avalonia.nupkg.xml | 6 +++ .../Platform/ClipboardImpl.cs | 4 ++ .../Input/Platform/IClipboard.cs | 36 ++++++++++++++- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 3 ++ src/Avalonia.Native/ClipboardImpl.cs | 4 ++ src/Avalonia.X11/X11Clipboard.cs | 6 ++- src/Browser/Avalonia.Browser/ClipboardImpl.cs | 4 ++ .../HeadlessPlatformStubs.cs | 10 ++-- src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs | 4 ++ src/Windows/Avalonia.Win32/ClipboardImpl.cs | 46 +++++++++++++++++-- .../Interop/UnmanagedMethods.cs | 2 + src/iOS/Avalonia.iOS/ClipboardImpl.cs | 4 ++ .../MaskedTextBoxTests.cs | 8 ++-- .../TextBoxTests.cs | 5 +- 14 files changed, 123 insertions(+), 19 deletions(-) diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 6b0f1c59cc..08c27e16f2 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -61,6 +61,12 @@ baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Input.Platform.IClipboard.FlushAsync + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + CP0006 M:Avalonia.Controls.Notifications.IManagedNotificationManager.Close(System.Object) diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 028134ffad..163e0c53ec 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -55,5 +55,9 @@ namespace Avalonia.Android.Platform public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); + + /// + public Task FlushAsync() => + Task.CompletedTask; } } diff --git a/src/Avalonia.Base/Input/Platform/IClipboard.cs b/src/Avalonia.Base/Input/Platform/IClipboard.cs index 3de352fc4f..f042537387 100644 --- a/src/Avalonia.Base/Input/Platform/IClipboard.cs +++ b/src/Avalonia.Base/Input/Platform/IClipboard.cs @@ -6,16 +6,48 @@ namespace Avalonia.Input.Platform [NotClientImplementable] public interface IClipboard { + /// + /// Returns a string containing the text data on the Clipboard. + /// + /// A string containing text data in the specified data format, or an empty string if no corresponding text data is available. Task GetTextAsync(); + /// + /// Stores text data on the Clipboard. The text data to store is specified as a string. + /// + /// A string that contains the UnicodeText data to store on the Clipboard. + /// is null. Task SetTextAsync(string? text); + /// + /// Clears any data from the system Clipboard. + /// Task ClearAsync(); + /// + /// Places a specified non-persistent data object on the system Clipboard. + /// + /// A data object (an object that implements ) to place on the system Clipboard. + /// is null. Task SetDataObjectAsync(IDataObject data); - + + /// + /// Permanently adds the data that is on the Clipboard so that it is available after the data's original application closes. + /// + /// + /// This method works only on Windows platform, on other platforms it does nothing. + Task FlushAsync(); + + /// + /// Get list of available Clipboard format. + /// Task GetFormatsAsync(); - + + /// + /// Retrieves data in a specified format from the Clipboard. + /// + /// A string that specifies the format of the data to retrieve. For a set of predefined data formats, see the class. + /// Task GetDataAsync(string format); } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index e3117ab8b2..dc7ab400cf 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -225,6 +225,9 @@ namespace Avalonia.DesignerSupport.Remote public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); public Task GetDataAsync(string format) => Task.FromResult((object)null); + + public Task FlushAsync() => + Task.CompletedTask; } class CursorFactoryStub : ICursorFactory diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 34b47ce236..96370895a8 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -182,6 +182,10 @@ namespace Avalonia.Native using (var n = Native.GetBytes(format)) return n.Bytes; } + + /// + public Task FlushAsync() => + Task.CompletedTask; } class ClipboardDataObject : IDataObject, IDisposable diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index da079458d9..bfa72dfeec 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -310,7 +310,7 @@ namespace Avalonia.X11 public Task SetDataObjectAsync(IDataObject data) { _storedDataObject = data; - XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero); + XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero); return StoreAtomsInClipboardManager(data); } @@ -350,5 +350,9 @@ namespace Avalonia.X11 return await SendDataRequest(formatAtom); } + + /// + public Task FlushAsync() => + Task.CompletedTask; } } diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs index 5df09e555d..b02a0f804f 100644 --- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs +++ b/src/Browser/Avalonia.Browser/ClipboardImpl.cs @@ -25,5 +25,9 @@ namespace Avalonia.Browser public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); public Task GetDataAsync(string format) => Task.FromResult(null); + + /// + public Task FlushAsync() => + Task.CompletedTask; } } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 6bfe8562dc..47ae308115 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -6,18 +6,12 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Platform.Storage; -using Avalonia.Platform.Storage.FileIO; -using Avalonia.Utilities; namespace Avalonia.Headless { @@ -82,6 +76,10 @@ namespace Avalonia.Headless return (object?)_data; }); } + + /// + public Task FlushAsync() => + Task.CompletedTask; } internal class HeadlessCursorFactoryStub : ICursorFactory diff --git a/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs b/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs index 549582b034..d08eb6f36e 100644 --- a/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs +++ b/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs @@ -73,4 +73,8 @@ internal class NuiClipboardImpl : IClipboard public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); + + /// + public Task FlushAsync() => + Task.CompletedTask; } diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 1a760aeab8..5bea02b2ff 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -1,10 +1,10 @@ using System; using System.Linq; -using Avalonia.Reactive; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.Win32.Interop; using MicroCom.Runtime; @@ -15,6 +15,13 @@ namespace Avalonia.Win32 { private const int OleRetryCount = 10; private const int OleRetryDelay = 100; + /// + /// The amount of time in milliseconds to sleep before flushing the clipboard after a set. + /// + /// + /// This is mitigation for clipboard listener issues. + /// + private const int OleFlushDelay = 10; private static async Task OpenClipboard() { @@ -32,7 +39,7 @@ namespace Avalonia.Win32 public async Task GetTextAsync() { - using(await OpenClipboard()) + using (await OpenClipboard()) { IntPtr hText = UnmanagedMethods.GetClipboardData(UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT); if (hText == IntPtr.Zero) @@ -54,7 +61,7 @@ namespace Avalonia.Win32 public async Task SetTextAsync(string? text) { - using(await OpenClipboard()) + using (await OpenClipboard()) { UnmanagedMethods.EmptyClipboard(); @@ -68,7 +75,7 @@ namespace Avalonia.Win32 public async Task ClearAsync() { - using(await OpenClipboard()) + using (await OpenClipboard()) { UnmanagedMethods.EmptyClipboard(); } @@ -90,7 +97,7 @@ namespace Avalonia.Win32 if (--i == 0) Marshal.ThrowExceptionForHR(hr); - + await Task.Delay(OleRetryDelay); } } @@ -142,5 +149,34 @@ namespace Avalonia.Win32 await Task.Delay(OleRetryDelay); } } + + /// + /// Permanently renders the contents of the last IDataObject that was set onto the clipboard. + /// + public async Task FlushAsync() + { + await Task.Delay(OleFlushDelay); + + // Retry OLE operations several times as mitigation for clipboard locking issues in TS sessions. + + int i = OleRetryCount; + + while (true) + { + var hr = UnmanagedMethods.OleFlushClipboard(); + + if (hr == 0) + { + break; + } + + if (--i == 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + await Task.Delay(OleRetryDelay); + } + } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5ebc165dd5..cfb818e682 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1573,6 +1573,8 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", PreserveSig = true)] public static extern int OleGetClipboard(out IntPtr dataObject); + [DllImport("ole32.dll", PreserveSig = true)] + public static extern int OleFlushClipboard(); [DllImport("ole32.dll", PreserveSig = true)] public static extern int OleSetClipboard(IntPtr dataObject); diff --git a/src/iOS/Avalonia.iOS/ClipboardImpl.cs b/src/iOS/Avalonia.iOS/ClipboardImpl.cs index 8c6cadee0e..6061e006ee 100644 --- a/src/iOS/Avalonia.iOS/ClipboardImpl.cs +++ b/src/iOS/Avalonia.iOS/ClipboardImpl.cs @@ -57,5 +57,9 @@ namespace Avalonia.iOS return Task.FromResult(null); } + + /// + public Task FlushAsync() => + Task.CompletedTask; } } diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 70c6969021..71ff289c22 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.Reactive.Linq; using System.Threading.Tasks; @@ -13,8 +12,6 @@ using Avalonia.Input.Platform; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Rendering.Composition; using Avalonia.UnitTests; using Moq; using Xunit; @@ -906,7 +903,7 @@ namespace Avalonia.Controls.UnitTests topLevel.ApplyTemplate(); topLevel.LayoutManager.ExecuteInitialLayoutPass(); - var texts = new List(); + var texts = new System.Collections.Generic.List(); target.PropertyChanged += (_, e) => { @@ -1028,6 +1025,9 @@ namespace Avalonia.Controls.UnitTests public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); public Task GetDataAsync(string format) => Task.FromResult((object)null); + + public Task FlushAsync() => + Task.CompletedTask; } private class TestTopLevel : TopLevel diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 66108952ad..36a9c8cc68 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1995,7 +1995,10 @@ namespace Avalonia.Controls.UnitTests public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); - public Task GetDataAsync(string format) => Task.FromResult((object?)null); + public Task GetDataAsync(string format) => Task.FromResult((object)null); + + public Task FlushAsync() => + Task.CompletedTask; } private class TestTopLevel : TopLevel From b66471b3567f30be077d79f3a4460b413ac2abb2 Mon Sep 17 00:00:00 2001 From: affederaffe Date: Mon, 3 Mar 2025 09:33:04 +0100 Subject: [PATCH 08/23] Bump DBus stack (#18343) --- .../Avalonia.FreeDesktop.csproj | 4 +- .../DBusIme/DBusTextInputMethodBase.cs | 2 +- .../DBusIme/Fcitx/FcitxICWrapper.cs | 8 +-- .../DBusIme/Fcitx/FcitxX11TextInputMethod.cs | 8 +-- .../DBusIme/IBus/IBusX11TextInputMethod.cs | 12 ++-- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 63 ++++++++++--------- .../DBusPlatformSettings.cs | 8 +-- src/Avalonia.FreeDesktop/DBusSystemDialog.cs | 46 +++++++------- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 18 +++--- 9 files changed, 86 insertions(+), 83 deletions(-) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index 5dc19e593e..35bd5df46e 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 764bc9df15..d396d375bc 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -60,7 +60,7 @@ namespace Avalonia.FreeDesktop.DBusIme private async Task WatchAsync() { - var dbus = new OrgFreedesktopDBus(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); + var dbus = new OrgFreedesktopDBusProxy(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); try { _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs index 8cb5d5761c..295a36b060 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxICWrapper.cs @@ -7,15 +7,15 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { internal class FcitxICWrapper { - private readonly OrgFcitxFcitxInputContext1? _modern; - private readonly OrgFcitxFcitxInputContext? _old; + private readonly OrgFcitxFcitxInputContext1Proxy? _modern; + private readonly OrgFcitxFcitxInputContextProxy? _old; - public FcitxICWrapper(OrgFcitxFcitxInputContext old) + public FcitxICWrapper(OrgFcitxFcitxInputContextProxy old) { _old = old; } - public FcitxICWrapper(OrgFcitxFcitxInputContext1 modern) + public FcitxICWrapper(OrgFcitxFcitxInputContext1Proxy modern) { _modern = modern; } diff --git a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs index eaa5c8f13e..ac4da69a07 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/Fcitx/FcitxX11TextInputMethod.cs @@ -25,18 +25,18 @@ namespace Avalonia.FreeDesktop.DBusIme.Fcitx { if (name == "org.fcitx.Fcitx") { - var method = new OrgFcitxFcitxInputMethod(Connection, name, "/inputmethod"); + var method = new OrgFcitxFcitxInputMethodProxy(Connection, name, "/inputmethod"); var resp = await method.CreateICv3Async(GetAppName(), Process.GetCurrentProcess().Id); - var proxy = new OrgFcitxFcitxInputContext(Connection, name, $"/inputcontext_{resp.Icid}"); + var proxy = new OrgFcitxFcitxInputContextProxy(Connection, name, $"/inputcontext_{resp.Icid}"); _context = new FcitxICWrapper(proxy); } else { - var method = new OrgFcitxFcitxInputMethod1(Connection, name, "/inputmethod"); + var method = new OrgFcitxFcitxInputMethod1Proxy(Connection, name, "/inputmethod"); var resp = await method.CreateInputContextAsync(new[] { ("appName", GetAppName()) }); - var proxy = new OrgFcitxFcitxInputContext1(Connection, name, resp.Item1); + var proxy = new OrgFcitxFcitxInputContext1Proxy(Connection, name, resp.Item1); _context = new FcitxICWrapper(proxy); } diff --git a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs index d81d2d7394..bc97378ac1 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/IBus/IBusX11TextInputMethod.cs @@ -13,21 +13,21 @@ namespace Avalonia.FreeDesktop.DBusIme.IBus { internal class IBusX11TextInputMethod : DBusTextInputMethodBase { - private OrgFreedesktopIBusService? _service; - private OrgFreedesktopIBusInputContext? _context; + private OrgFreedesktopIBusServiceProxy? _service; + private OrgFreedesktopIBusInputContextProxy? _context; private string? _preeditText; private int _preeditCursor; private bool _preeditShown = true; - private int _insideReset = 0; + private int _insideReset; public IBusX11TextInputMethod(Connection connection) : base(connection, "org.freedesktop.portal.IBus") { } protected override async Task Connect(string name) { - var portal = new OrgFreedesktopIBusPortal(Connection, name, "/org/freedesktop/IBus"); + var portal = new OrgFreedesktopIBusPortalProxy(Connection, name, "/org/freedesktop/IBus"); var path = await portal.CreateInputContextAsync(GetAppName()); - _service = new OrgFreedesktopIBusService(Connection, name, path); - _context = new OrgFreedesktopIBusInputContext(Connection, name, path); + _service = new OrgFreedesktopIBusServiceProxy(Connection, name, path); + _context = new OrgFreedesktopIBusInputContextProxy(Connection, name, path); AddDisposable(await _context.WatchCommitTextAsync(OnCommitText)); AddDisposable(await _context.WatchForwardKeyEventAsync(OnForwardKey)); AddDisposable(await _context.WatchUpdatePreeditTextAsync(OnUpdatePreedit)); diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 9496204cc1..37b77c45a1 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -24,7 +24,7 @@ namespace Avalonia.FreeDesktop public static string GenerateDBusMenuObjPath => $"/net/avaloniaui/dbusmenu/{Guid.NewGuid():N}"; - private sealed class DBusMenuExporterImpl : ComCanonicalDbusmenu, ITopLevelNativeMenuExporter, IDisposable + private sealed class DBusMenuExporterImpl : ComCanonicalDbusmenuHandler, ITopLevelNativeMenuExporter, IDisposable { private readonly Dictionary _idsToItems = new(); private readonly Dictionary _itemsToIds = new(); @@ -32,7 +32,7 @@ namespace Avalonia.FreeDesktop private readonly PathHandler _pathHandler; private readonly uint _xid; private readonly bool _appMenu = true; - private ComCanonicalAppMenuRegistrar? _registrar; + private ComCanonicalAppMenuRegistrarProxy? _registrar; private NativeMenu? _menu; private bool _disposed; private uint _revision = 1; @@ -63,7 +63,7 @@ namespace Avalonia.FreeDesktop public override Connection Connection { get; } - protected override ValueTask<(uint Revision, (int, Dictionary, Variant[]) Layout)> OnGetLayoutAsync(int parentId, int recursionDepth, string[] propertyNames) + protected override ValueTask<(uint Revision, (int, Dictionary, VariantValue[]) Layout)> OnGetLayoutAsync(Message message, int parentId, int recursionDepth, string[] propertyNames) { var menu = GetMenu(parentId); var layout = GetLayout(menu.item, menu.menu, recursionDepth, propertyNames); @@ -73,31 +73,31 @@ namespace Avalonia.FreeDesktop OnIsNativeMenuExportedChanged?.Invoke(this, EventArgs.Empty); } - return new ValueTask<(uint, (int, Dictionary, Variant[]))>((_revision, layout)); + return new ValueTask<(uint, (int, Dictionary, VariantValue[]))>((_revision, layout)); } - protected override ValueTask<(int, Dictionary)[]> OnGetGroupPropertiesAsync(int[] ids, string[] propertyNames) + protected override ValueTask<(int, Dictionary)[]> OnGetGroupPropertiesAsync(Message message, int[] ids, string[] propertyNames) => new(ids.Select(id => (id, GetProperties(GetMenu(id), propertyNames))).ToArray()); - protected override ValueTask OnGetPropertyAsync(int id, string name) => - new(GetProperty(GetMenu(id), name) ?? new Variant(0)); + protected override ValueTask OnGetPropertyAsync(Message message, int id, string name) => + new(GetProperty(GetMenu(id), name) ?? VariantValue.Int32(0)); - protected override ValueTask OnEventAsync(int id, string eventId, VariantValue data, uint timestamp) + protected override ValueTask OnEventAsync(Message message, int id, string eventId, VariantValue data, uint timestamp) { HandleEvent(id, eventId); return new ValueTask(); } - protected override ValueTask OnEventGroupAsync((int, string, VariantValue, uint)[] events) + protected override ValueTask OnEventGroupAsync(Message message, (int, string, VariantValue, uint)[] events) { foreach (var e in events) HandleEvent(e.Item1, e.Item2); return new ValueTask([]); } - protected override ValueTask OnAboutToShowAsync(int id) => new(false); + protected override ValueTask OnAboutToShowAsync(Message message, int id) => new(false); - protected override ValueTask<(int[] UpdatesNeeded, int[] IdErrors)> OnAboutToShowGroupAsync(int[] ids) => + protected override ValueTask<(int[] UpdatesNeeded, int[] IdErrors)> OnAboutToShowGroupAsync(Message message, int[] ids) => new(([], [])); private async Task InitializeAsync() @@ -106,7 +106,7 @@ namespace Avalonia.FreeDesktop if (!_appMenu) return; - _registrar = new ComCanonicalAppMenuRegistrar(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); + _registrar = new ComCanonicalAppMenuRegistrarProxy(Connection, "com.canonical.AppMenu.Registrar", "/com/canonical/AppMenu/Registrar"); try { if (!_disposed) @@ -211,32 +211,32 @@ namespace Avalonia.FreeDesktop private static readonly string[] s_allProperties = ["type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"]; - private static Variant? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) + private static VariantValue? GetProperty((NativeMenuItemBase? item, NativeMenu? menu) i, string name) { var (it, menu) = i; if (it is NativeMenuItemSeparator) { if (name == "type") - return new Variant("separator"); + return VariantValue.String("separator"); } else if (it is NativeMenuItem item) { if (name == "type") return null; if (name == "label") - return new Variant(item.Header ?? ""); + return VariantValue.String(item.Header ?? ""); if (name == "enabled") { if (item.Menu is not null && item.Menu.Items.Count == 0) - return new Variant(false); + return VariantValue.Bool(false); if (!item.IsEnabled) - return new Variant(false); + return VariantValue.Bool(false); return null; } if (name == "visible") - return new Variant(item.IsVisible); + return VariantValue.Bool(item.IsVisible); if (name == "shortcut") { @@ -244,7 +244,7 @@ namespace Avalonia.FreeDesktop return null; if (item.Gesture.KeyModifiers == 0) return null; - var lst = new Array(); + var lst = new List(); var mod = item.Gesture; if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Control)) lst.Add("Control"); @@ -255,19 +255,19 @@ namespace Avalonia.FreeDesktop if (mod.KeyModifiers.HasAllFlags(KeyModifiers.Meta)) lst.Add("Super"); lst.Add(item.Gesture.Key.ToString()); - return Variant.FromArray(new Array>([lst])); + return VariantValue.ArrayOfVariant((VariantValue[]) [VariantValue.Array(lst)]); } if (name == "toggle-type") { if (item.ToggleType == NativeMenuItemToggleType.CheckBox) - return new Variant("checkmark"); + return VariantValue.String("checkmark"); if (item.ToggleType == NativeMenuItemToggleType.Radio) - return new Variant("radio"); + return VariantValue.String("radio"); } if (name == "toggle-state" && item.ToggleType != NativeMenuItemToggleType.None) - return new Variant(item.IsChecked ? 1 : 0); + return VariantValue.Int32(item.IsChecked ? 1 : 0); if (name == "icon-data") { @@ -280,7 +280,7 @@ namespace Avalonia.FreeDesktop var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item); using var ms = new MemoryStream(); icon.Save(ms); - return Variant.FromArray(new Array(ms.ToArray())); + return VariantValue.Array(ms.ToArray()); } } } @@ -288,7 +288,7 @@ namespace Avalonia.FreeDesktop if (name == "children-display") { if (menu is not null) - return new Variant("submenu"); + return VariantValue.String("submenu"); return null; } } @@ -296,11 +296,11 @@ namespace Avalonia.FreeDesktop return null; } - private static Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) + private static Dictionary GetProperties((NativeMenuItemBase? item, NativeMenu? menu) i, string[] names) { if (names.Length == 0) names = s_allProperties; - var properties = new Dictionary(); + var properties = new Dictionary(); foreach (var n in names) { var v = GetProperty(i, n); @@ -311,18 +311,21 @@ namespace Avalonia.FreeDesktop return properties; } - private (int, Dictionary, Variant[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) + private (int, Dictionary, VariantValue[]) GetLayout(NativeMenuItemBase? item, NativeMenu? menu, int depth, string[] propertyNames) { var id = item is null ? 0 : GetId(item); var props = GetProperties((item, menu), propertyNames); - var children = depth == 0 || menu is null ? [] : new Variant[menu.Items.Count]; + var children = depth == 0 || menu is null ? [] : new VariantValue[menu.Items.Count]; if (menu is not null) { for (var c = 0; c < children.Length; c++) { var ch = menu.Items[c]; var layout = GetLayout(ch, (ch as NativeMenuItem)?.Menu, depth == -1 ? -1 : depth - 1, propertyNames); - children[c] = Variant.FromStruct(Struct.Create(layout.Item1, new Dict(layout.Item2), new Array(layout.Item3))); + children[c] = VariantValue.Struct( + VariantValue.Int32(layout.Item1), + new Dict(layout.Item2), + VariantValue.ArrayOfVariant(layout.Item3)); } } diff --git a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs index f290d5c1ad..a8735eb6a3 100644 --- a/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs +++ b/src/Avalonia.FreeDesktop/DBusPlatformSettings.cs @@ -10,7 +10,7 @@ namespace Avalonia.FreeDesktop { internal class DBusPlatformSettings : DefaultPlatformSettings { - private readonly OrgFreedesktopPortalSettings? _settings; + private readonly OrgFreedesktopPortalSettingsProxy? _settings; private PlatformColorValues? _lastColorValues; private PlatformThemeVariant? _themeVariant; @@ -21,7 +21,7 @@ namespace Avalonia.FreeDesktop if (DBusHelper.DefaultConnection is not { } conn) return; - _settings = new OrgFreedesktopPortalSettings(conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + _settings = new OrgFreedesktopPortalSettingsProxy(conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); _ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); _ = TryGetInitialValuesAsync(); } @@ -46,8 +46,8 @@ namespace Avalonia.FreeDesktop if (version >= 2) value = await _settings!.ReadOneAsync("org.freedesktop.appearance", "color-scheme"); else - // Variants-in-Variants are automatically collapsed by Tmds.DBus.Protocol, so need to do so here as normally necessary - value = await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); + // Unpack nested Variant + value = (await _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme")).GetVariantValue(); return ToColorScheme(value.GetUInt32()); } catch (DBusException) diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index c4663878cc..7709b87242 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs @@ -21,8 +21,8 @@ namespace Avalonia.FreeDesktop return null; using var restoreContext = AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Input); - - var dbusFileChooser = new OrgFreedesktopPortalFileChooser(conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); + + var dbusFileChooser = new OrgFreedesktopPortalFileChooserProxy(conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); uint version; try { @@ -37,11 +37,11 @@ namespace Avalonia.FreeDesktop } private readonly Connection _connection; - private readonly OrgFreedesktopPortalFileChooser _fileChooser; + private readonly OrgFreedesktopPortalFileChooserProxy _fileChooser; private readonly IPlatformHandle _handle; private readonly uint _version; - private DBusSystemDialog(Connection connection, IPlatformHandle handle, OrgFreedesktopPortalFileChooser fileChooser, uint version) + private DBusSystemDialog(Connection connection, IPlatformHandle handle, OrgFreedesktopPortalFileChooserProxy fileChooser, uint version) { _connection = connection; _fileChooser = fileChooser; @@ -59,19 +59,19 @@ namespace Avalonia.FreeDesktop { var parentWindow = $"x11:{_handle.Handle:X}"; ObjectPath objectPath; - var chooserOptions = new Dictionary(); + var chooserOptions = new Dictionary(); if (TryParseFilters(options.FileTypeFilter, out var filters)) chooserOptions.Add("filters", filters); if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) - chooserOptions.Add("current_folder", Variant.FromArray(new Array(Encoding.UTF8.GetBytes(folderPath + "\0")))); + chooserOptions.Add("current_folder", VariantValue.Array(Encoding.UTF8.GetBytes(folderPath + "\0"))); - chooserOptions.Add("multiple", new Variant(options.AllowMultiple)); + chooserOptions.Add("multiple", VariantValue.Bool(options.AllowMultiple)); objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); + var request = new OrgFreedesktopPortalRequestProxy(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); using var disposable = await request.WatchResponseAsync((e, x) => { @@ -81,7 +81,7 @@ namespace Avalonia.FreeDesktop tsc.TrySetResult(x.Results["uris"].GetArray()); }); - var uris = await tsc.Task ?? Array.Empty(); + var uris = await tsc.Task ?? []; return uris.Select(static path => new BclStorageFile(new FileInfo(new Uri(path).LocalPath))).ToList(); } @@ -89,17 +89,17 @@ namespace Avalonia.FreeDesktop { var parentWindow = $"x11:{_handle.Handle:X}"; ObjectPath objectPath; - var chooserOptions = new Dictionary(); + var chooserOptions = new Dictionary(); if (TryParseFilters(options.FileTypeChoices, out var filters)) chooserOptions.Add("filters", filters); if (options.SuggestedFileName is { } currentName) - chooserOptions.Add("current_name", new Variant(currentName)); + chooserOptions.Add("current_name", VariantValue.String(currentName)); if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) - chooserOptions.Add("current_folder", Variant.FromArray(new Array(Encoding.UTF8.GetBytes(folderPath + "\0")))); + chooserOptions.Add("current_folder", VariantValue.Array(Encoding.UTF8.GetBytes(folderPath + "\0"))); objectPath = await _fileChooser.SaveFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); + var request = new OrgFreedesktopPortalRequestProxy(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); FilePickerFileType? selectedType = null; using var disposable = await request.WatchResponseAsync((e, x) => @@ -147,22 +147,22 @@ namespace Avalonia.FreeDesktop public override async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { if (_version < 3) - return Array.Empty(); + return []; var parentWindow = $"x11:{_handle.Handle:X}"; - var chooserOptions = new Dictionary + var chooserOptions = new Dictionary { - { "directory", new Variant(true) }, - { "multiple", new Variant(options.AllowMultiple) } + { "directory", VariantValue.Bool(true) }, + { "multiple", VariantValue.Bool(options.AllowMultiple) } }; if (options.SuggestedFileName is { } currentName) - chooserOptions.Add("current_name", new Variant(currentName)); - if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) - chooserOptions.Add("current_folder", Variant.FromArray(new Array(Encoding.UTF8.GetBytes(folderPath + "\0")))); + chooserOptions.Add("current_name", VariantValue.String(currentName)); + if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath) + chooserOptions.Add("current_folder", VariantValue.Array(Encoding.UTF8.GetBytes(folderPath + "\0"))); var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions); - var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath); + var request = new OrgFreedesktopPortalRequestProxy(_connection, "org.freedesktop.portal.Desktop", objectPath); var tsc = new TaskCompletionSource(); using var disposable = await request.WatchResponseAsync((e, x) => { @@ -180,7 +180,7 @@ namespace Avalonia.FreeDesktop .Select(static path => new BclStorageFolder(new DirectoryInfo(path))).ToList(); } - private static bool TryParseFilters(IReadOnlyList? fileTypes, out Variant result) + private static bool TryParseFilters(IReadOnlyList? fileTypes, out VariantValue result) { const uint GlobStyle = 0u; const uint MimeStyle = 1u; @@ -207,7 +207,7 @@ namespace Avalonia.FreeDesktop filters.Add(Struct.Create(fileType.Name, new Array>(extensions))); } - result = Variant.FromArray(filters); + result = filters.AsVariantValue(); return true; } } diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index 80942e93c8..b19da5ff36 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -16,12 +16,12 @@ namespace Avalonia.FreeDesktop public static readonly (int, int, byte[]) EmptyPixmap = (1, 1, [255, 0, 0, 0]); private readonly Connection? _connection; - private readonly OrgFreedesktopDBus? _dBus; + private readonly OrgFreedesktopDBusProxy? _dBus; private IDisposable? _serviceWatchDisposable; private readonly PathHandler _pathHandler = new("/StatusNotifierItem"); private readonly StatusNotifierItemDbusObj? _statusNotifierItemDbusObj; - private OrgKdeStatusNotifierWatcher? _statusNotifierWatcher; + private OrgKdeStatusNotifierWatcherProxy? _statusNotifierWatcher; private (int, int, byte[]) _icon; private string? _sysTrayServiceName; @@ -50,7 +50,7 @@ namespace Avalonia.FreeDesktop IsActive = true; - _dBus = new OrgFreedesktopDBus(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); + _dBus = new OrgFreedesktopDBusProxy(_connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); var dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath; MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(dbusMenuPath, _connection); @@ -86,7 +86,7 @@ namespace Avalonia.FreeDesktop if (!_serviceConnected && newOwner is not null) { _serviceConnected = true; - _statusNotifierWatcher = new OrgKdeStatusNotifierWatcher(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); + _statusNotifierWatcher = new OrgKdeStatusNotifierWatcherProxy(_connection, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher"); DestroyTrayIcon(); @@ -213,7 +213,7 @@ namespace Avalonia.FreeDesktop /// /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html /// - internal class StatusNotifierItemDbusObj : OrgKdeStatusNotifierItem + internal class StatusNotifierItemDbusObj : OrgKdeStatusNotifierItemHandler { public StatusNotifierItemDbusObj(Connection connection, ObjectPath dbusMenuPath) { @@ -225,17 +225,17 @@ namespace Avalonia.FreeDesktop public event Action? ActivationDelegate; - protected override ValueTask OnContextMenuAsync(int x, int y) => new(); + protected override ValueTask OnContextMenuAsync(Message message, int x, int y) => new(); - protected override ValueTask OnActivateAsync(int x, int y) + protected override ValueTask OnActivateAsync(Message message, int x, int y) { ActivationDelegate?.Invoke(); return new ValueTask(); } - protected override ValueTask OnSecondaryActivateAsync(int x, int y) => new(); + protected override ValueTask OnSecondaryActivateAsync(Message message, int x, int y) => new(); - protected override ValueTask OnScrollAsync(int delta, string orientation) => new(); + protected override ValueTask OnScrollAsync(Message message, int delta, string orientation) => new(); public void InvalidateAll() { From 603a2bdb43b7e8b2d1c8919f9d0669af7b33f117 Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Mon, 3 Mar 2025 17:41:05 -0500 Subject: [PATCH 09/23] add content template to flyout (#18361) --- src/Avalonia.Controls/Flyouts/Flyout.cs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs index 8ec5cfa50a..3f83b3be82 100644 --- a/src/Avalonia.Controls/Flyouts/Flyout.cs +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Metadata; using Avalonia.Styling; @@ -10,8 +11,14 @@ namespace Avalonia.Controls /// /// Defines the property /// - public static readonly StyledProperty ContentProperty = - AvaloniaProperty.Register(nameof(Content)); + public static readonly StyledProperty ContentProperty = + AvaloniaProperty.Register(nameof(Content)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ContentTemplateProperty = + AvaloniaProperty.Register(nameof(ContentTemplate)); private Classes? _classes; @@ -39,17 +46,27 @@ namespace Avalonia.Controls /// Gets or sets the content to display in this flyout /// [Content] - public object Content + public object? Content { get => GetValue(ContentProperty); set => SetValue(ContentProperty, value); } + /// + /// Gets or sets the data template used to display the content of the flyout. + /// + public IDataTemplate? ContentTemplate + { + get => GetValue(ContentTemplateProperty); + set => SetValue(ContentTemplateProperty, value); + } + protected override Control CreatePresenter() { return new FlyoutPresenter { - [!ContentControl.ContentProperty] = this[!ContentProperty] + [!ContentControl.ContentProperty] = this[!ContentProperty], + [!ContentControl.ContentTemplateProperty] = this[!ContentTemplateProperty] }; } From acd46534116e9778081eb68b105d1c33268c9652 Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Thu, 6 Mar 2025 20:34:10 +0900 Subject: [PATCH 10/23] Add diagnostic metrics/activities (#18314) * Add System.Diagnostics.DiagnosticSource nuget package for pre-.NET 8 targets * Move System.Memory reference to Base.props * Add CompositorRenderPass and CompositorUpdatePass metrics * Layout measure/arrange pass metrics * Add UI render pass and input pass meters * Add observable metrics * Add RaisingRoutedEvent activity * Add FindingResourceActivity activity * Add AttachingStyleActivity and EvaluatingStyleActivator activities * Add PerformingHitTest activity * Add MeasuingLayoutable/ArrangingLayoutable activities * Missed RaisingRoutedEvent definition * Missed tag definitions * Start FindingResourceActivity on static resources too * Fix compilation * Naming * Add Avalonia.Diagnostics.Diagnostic.IsEnabled runtime switch * Maybe make it more trimmable as well --- Avalonia.sln | 1 - build/Base.props | 6 ++ build/System.Memory.props | 6 -- src/Avalonia.Base/Avalonia.Base.csproj | 1 - .../Controls/ResourceNodeExtensions.cs | 8 +- .../Diagnostics/Diagnostic.Activities.cs | 25 +++++ .../Diagnostics/Diagnostic.Consts.cs | 51 ++++++++++ .../Diagnostics/Diagnostic.Metrics.cs | 94 +++++++++++++++++++ src/Avalonia.Base/Diagnostics/Diagnostic.cs | 22 +++++ src/Avalonia.Base/Interactivity/EventRoute.cs | 5 + .../Interactivity/Interactive.cs | 3 + .../Interactivity/RoutedEvent.cs | 5 + src/Avalonia.Base/Layout/LayoutManager.cs | 3 + src/Avalonia.Base/Layout/Layoutable.cs | 7 ++ .../Composition/CompositingRenderer.cs | 6 +- .../Server/ServerCompositionTarget.cs | 28 +++--- src/Avalonia.Base/Styling/ControlTheme.cs | 7 ++ src/Avalonia.Base/Styling/Style.cs | 6 ++ src/Avalonia.Base/Styling/StyleInstance.cs | 8 ++ .../Threading/DispatcherTimer.cs | 4 + .../Utilities/StopwatchHelper.cs | 4 + src/Avalonia.Base/Visual.cs | 5 + src/Avalonia.Controls/TopLevel.cs | 3 + .../StaticResourceExtension.cs | 5 + .../Avalonia.Markup/Avalonia.Markup.csproj | 1 - ...valonia.Controls.DataGrid.UnitTests.csproj | 1 - 26 files changed, 291 insertions(+), 24 deletions(-) delete mode 100644 build/System.Memory.props create mode 100644 src/Avalonia.Base/Diagnostics/Diagnostic.Activities.cs create mode 100644 src/Avalonia.Base/Diagnostics/Diagnostic.Consts.cs create mode 100644 src/Avalonia.Base/Diagnostics/Diagnostic.Metrics.cs create mode 100644 src/Avalonia.Base/Diagnostics/Diagnostic.cs diff --git a/Avalonia.sln b/Avalonia.sln index cd7e56e9d4..1b977839ea 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -115,7 +115,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\SkiaSharp.props = build\SkiaSharp.props build\SourceGenerators.props = build\SourceGenerators.props build\SourceLink.props = build\SourceLink.props - build\System.Memory.props = build\System.Memory.props build\TargetFrameworks.props = build\TargetFrameworks.props build\TrimmingEnable.props = build\TrimmingEnable.props build\UnitTests.NetFX.props = build\UnitTests.NetFX.props diff --git a/build/Base.props b/build/Base.props index 6b7ae5d677..0e667f105b 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,6 +1,12 @@  + + + + + + diff --git a/build/System.Memory.props b/build/System.Memory.props deleted file mode 100644 index 35a87a3a2f..0000000000 --- a/build/System.Memory.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index c782bff011..3e89984a38 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -12,7 +12,6 @@ - diff --git a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs index c307b709fe..f1a4a90864 100644 --- a/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Base/Controls/ResourceNodeExtensions.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Diagnostics; using Avalonia.Reactive; using Avalonia.Styling; @@ -75,12 +76,17 @@ namespace Avalonia.Controls control = control ?? throw new ArgumentNullException(nameof(control)); key = key ?? throw new ArgumentNullException(nameof(key)); - IResourceHost? current = control; + using var activity = Diagnostic.FindingResource()? + .AddTag(Diagnostic.Tags.Key, key) + .AddTag(Diagnostic.Tags.ThemeVariant, theme); + + var current = control; while (current != null) { if (current.TryGetResource(key, theme, out value)) { + activity?.AddTag(Diagnostic.Tags.Result, true); return true; } diff --git a/src/Avalonia.Base/Diagnostics/Diagnostic.Activities.cs b/src/Avalonia.Base/Diagnostics/Diagnostic.Activities.cs new file mode 100644 index 0000000000..b6b2cad5da --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/Diagnostic.Activities.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; + +// ReSharper disable ExplicitCallerInfoArgument + +namespace Avalonia.Diagnostics; + +internal static partial class Diagnostic +{ + private static ActivitySource? s_activitySource; + + public static void InitActivitySource() + { + s_activitySource = new("Avalonia.Diagnostic.Source"); + } + + private static Activity? StartActivity(string name) => s_activitySource?.StartActivity(name); + + public static Activity? AttachingStyle() => StartActivity("Avalonia.AttachingStyle"); + public static Activity? FindingResource() => StartActivity("Avalonia.FindingResource"); + public static Activity? EvaluatingStyle() => StartActivity("Avalonia.EvaluatingStyle"); + public static Activity? MeasuringLayoutable() => StartActivity("Avalonia.MeasuringLayoutable"); + public static Activity? ArrangingLayoutable() => StartActivity("Avalonia.ArrangingLayoutable"); + public static Activity? PerformingHitTest() => StartActivity("Avalonia.PerformingHitTest"); + public static Activity? RaisingRoutedEvent() => StartActivity("Avalonia.RaisingRoutedEvent"); +} diff --git a/src/Avalonia.Base/Diagnostics/Diagnostic.Consts.cs b/src/Avalonia.Base/Diagnostics/Diagnostic.Consts.cs new file mode 100644 index 0000000000..e89aef4bf0 --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/Diagnostic.Consts.cs @@ -0,0 +1,51 @@ +namespace Avalonia.Diagnostics; + +internal static partial class Diagnostic +{ + public static class Meters + { + public const string SecondsUnit = "s"; + public const string MillisecondsUnit = "ms"; + + public const string CompositorRenderPassName = "avalonia.comp.render.time"; + public const string CompositorRenderPassDescription = "Duration of the compositor render pass on render thread"; + public const string CompositorUpdatePassName = "avalonia.comp.update.time"; + public const string CompositorUpdatePassDescription = "Duration of the compositor update pass on render thread"; + + public const string LayoutMeasurePassName = "avalonia.ui.measure.time"; + public const string LayoutMeasurePassDescription = "Duration of layout measurement pass on UI thread"; + public const string LayoutArrangePassName = "avalonia.ui.arrange.time"; + public const string LayoutArrangePassDescription = "Duration of layout arrangement pass on UI thread"; + public const string LayoutRenderPassName = "avalonia.ui.render.time"; + public const string LayoutRenderPassDescription = "Duration of render recording pass on UI thread"; + public const string LayoutInputPassName = "avalonia.ui.input.time"; + public const string LayoutInputPassDescription = "Duration of input processing on UI thread"; + + public const string TotalEventHandleCountName = "avalonia.ui.event.handler.count"; + public const string TotalEventHandleCountDescription = "Number of event handlers currently registered in the application"; + public const string TotalEventHandleCountUnit = "{handler}"; + public const string TotalVisualCountName = "avalonia.ui.visual.count"; + public const string TotalVisualCountDescription = "Number of visual elements currently present in the visual tree"; + public const string TotalVisualCountUnit = "{visual}"; + public const string TotalDispatcherTimerCountName = "avalonia.ui.dispatcher.timer.count"; + public const string TotalDispatcherTimerCountDescription = "Number of active dispatcher timers in the application"; + public const string TotalDispatcherTimerCountUnit = "{timer}"; + } + + public static class Tags + { + public const string Style = nameof(Style); + public const string SelectorResult = nameof(SelectorResult); + + public const string Key = nameof(Key); + public const string ThemeVariant = nameof(ThemeVariant); + public const string Result = nameof(Result); + + public const string Activator = nameof(Activator); + public const string IsActive = nameof(IsActive); + public const string Selector = nameof(Selector); + public const string Control = nameof(Control); + + public const string RoutedEvent = nameof(RoutedEvent); + } +} diff --git a/src/Avalonia.Base/Diagnostics/Diagnostic.Metrics.cs b/src/Avalonia.Base/Diagnostics/Diagnostic.Metrics.cs new file mode 100644 index 0000000000..5881d39131 --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/Diagnostic.Metrics.cs @@ -0,0 +1,94 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Diagnostics; + +internal static partial class Diagnostic +{ + private static Histogram? s_compositorRender; + private static Histogram? s_compositorUpdate; + private static Histogram? s_layoutMeasure; + private static Histogram? s_layoutArrange; + private static Histogram? s_layoutRender; + private static Histogram? s_layoutInput; + + public static void InitMetrics() + { + // Metrics + var meter = new Meter("Avalonia.Diagnostic.Meter"); + s_compositorRender = meter.CreateHistogram( + Meters.CompositorRenderPassName, + Meters.MillisecondsUnit, + Meters.CompositorRenderPassDescription); + s_compositorUpdate = meter.CreateHistogram( + Meters.CompositorUpdatePassName, + Meters.MillisecondsUnit, + Meters.CompositorUpdatePassDescription); + s_layoutMeasure = meter.CreateHistogram( + Meters.LayoutMeasurePassName, + Meters.MillisecondsUnit, + Meters.LayoutMeasurePassDescription); + s_layoutArrange = meter.CreateHistogram( + Meters.LayoutArrangePassName, + Meters.MillisecondsUnit, + Meters.LayoutArrangePassDescription); + s_layoutRender = meter.CreateHistogram( + Meters.LayoutRenderPassName, + Meters.MillisecondsUnit, + Meters.LayoutRenderPassDescription); + s_layoutInput = meter.CreateHistogram( + Meters.LayoutInputPassName, + Meters.MillisecondsUnit, + Meters.LayoutInputPassDescription); + meter.CreateObservableUpDownCounter( + Meters.TotalEventHandleCountName, + () => Interactive.TotalHandlersCount, + Meters.TotalEventHandleCountUnit, + Meters.TotalEventHandleCountDescription); + meter.CreateObservableUpDownCounter( + Meters.TotalVisualCountName, + () => Visual.RootedVisualChildrenCount, + Meters.TotalVisualCountUnit, + Meters.TotalVisualCountDescription); + meter.CreateObservableUpDownCounter( + Meters.TotalDispatcherTimerCountName, + () => DispatcherTimer.ActiveTimersCount, + Meters.TotalDispatcherTimerCountUnit, + Meters.TotalDispatcherTimerCountDescription); + } + + public static HistogramReportDisposable BeginCompositorRenderPass() => Begin(s_compositorRender); + public static HistogramReportDisposable BeginCompositorUpdatePass() => Begin(s_compositorUpdate); + public static HistogramReportDisposable BeginLayoutMeasurePass() => Begin(s_layoutMeasure); + public static HistogramReportDisposable BeginLayoutArrangePass() => Begin(s_layoutArrange); + public static HistogramReportDisposable BeginLayoutInputPass() => Begin(s_layoutInput); + public static HistogramReportDisposable BeginLayoutRenderPass() => Begin(s_layoutRender); + + private static HistogramReportDisposable Begin(Histogram? histogram) => histogram is not null ? new(histogram) : default; + + internal readonly ref struct HistogramReportDisposable + { + private readonly Histogram _histogram; + private readonly long _timestamp; + + public HistogramReportDisposable(Histogram histogram) + { + _histogram = histogram; + if (histogram.Enabled) + { + _timestamp = Stopwatch.GetTimestamp(); + } + } + + public void Dispose() + { + if (_timestamp > 0) + { + _histogram.Record(StopwatchHelper.GetElapsedTimeMs(_timestamp)); + } + } + } +} diff --git a/src/Avalonia.Base/Diagnostics/Diagnostic.cs b/src/Avalonia.Base/Diagnostics/Diagnostic.cs new file mode 100644 index 0000000000..f49676d6b9 --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/Diagnostic.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Diagnostics; + +internal static partial class Diagnostic +{ + public static bool IsEnabled { get; } + + private static bool InitializeIsEnabled() => AppContext.TryGetSwitch("Avalonia.Diagnostics.Diagnostic.IsEnabled", out var isEnabled) && isEnabled; + + static Diagnostic() + { + IsEnabled = InitializeIsEnabled(); + if (!IsEnabled) + { + return; + } + + InitActivitySource(); + InitMetrics(); + } +} diff --git a/src/Avalonia.Base/Interactivity/EventRoute.cs b/src/Avalonia.Base/Interactivity/EventRoute.cs index d0d82b4884..967c095d98 100644 --- a/src/Avalonia.Base/Interactivity/EventRoute.cs +++ b/src/Avalonia.Base/Interactivity/EventRoute.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Collections.Pooled; +using Avalonia.Diagnostics; namespace Avalonia.Interactivity { @@ -80,6 +81,10 @@ namespace Avalonia.Interactivity e.Source = source; + using var _ = Diagnostic.RaisingRoutedEvent()? + .AddTag(Diagnostic.Tags.Control, e.Source) + .AddTag(Diagnostic.Tags.RoutedEvent, e.RoutedEvent); + if (_event.RoutingStrategies == RoutingStrategies.Direct) { e.Route = RoutingStrategies.Direct; diff --git a/src/Avalonia.Base/Interactivity/Interactive.cs b/src/Avalonia.Base/Interactivity/Interactive.cs index 0dfaae0fc1..33443af8e5 100644 --- a/src/Avalonia.Base/Interactivity/Interactive.cs +++ b/src/Avalonia.Base/Interactivity/Interactive.cs @@ -12,6 +12,7 @@ namespace Avalonia.Interactivity /// public class Interactive : Layoutable { + internal static int TotalHandlersCount { get; private set; } private Dictionary>? _eventHandlers; /// @@ -90,6 +91,7 @@ namespace Avalonia.Interactivity if (subscriptions[i].Handler == handler) { subscriptions.RemoveAt(i); + TotalHandlersCount--; } } } @@ -185,6 +187,7 @@ namespace Avalonia.Interactivity } subscriptions.Add(subscription); + TotalHandlersCount++; } private void AddToEventRoute(RoutedEvent routedEvent, EventRoute route) diff --git a/src/Avalonia.Base/Interactivity/RoutedEvent.cs b/src/Avalonia.Base/Interactivity/RoutedEvent.cs index 79a4dc60e4..bc3a2e5015 100644 --- a/src/Avalonia.Base/Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Base/Interactivity/RoutedEvent.cs @@ -103,6 +103,11 @@ namespace Avalonia.Interactivity { _routeFinished.OnNext(e); } + + public override string ToString() + { + return FormattableString.Invariant($"{OwnerType.Name}.{Name}"); + } } public class RoutedEvent : RoutedEvent diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index 79d43239e6..411b747107 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -2,6 +2,7 @@ using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Media; using Avalonia.Metadata; @@ -246,6 +247,7 @@ namespace Avalonia.Layout private void ExecuteMeasurePass() { + using var _ = Diagnostic.BeginLayoutMeasurePass(); while (_toMeasure.Count > 0) { var control = _toMeasure.Dequeue(); @@ -261,6 +263,7 @@ namespace Avalonia.Layout private void ExecuteArrangePass() { + using var _ = Diagnostic.BeginLayoutArrangePass(); while (_toArrange.Count > 0) { var control = _toArrange.Dequeue(); diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 53a3c84364..a71ceb76af 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.Reactive; using Avalonia.VisualTree; @@ -367,6 +368,9 @@ namespace Avalonia.Layout if (!IsMeasureValid || _previousMeasure != availableSize) { + using var activity = Diagnostic.MeasuringLayoutable()? + .AddTag(Diagnostic.Tags.Control, this); + var previousDesiredSize = DesiredSize; var desiredSize = default(Size); @@ -417,6 +421,9 @@ namespace Avalonia.Layout if (!IsArrangeValid || _previousArrange != rect) { + using var activity = Diagnostic.ArrangingLayoutable()? + .AddTag(Diagnostic.Tags.Control, this); + Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Arrange to {Rect} ", rect); IsArrangeValid = true; diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 9faa3d721f..98fb147c2c 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -6,6 +6,7 @@ using System.Numerics; using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Collections.Pooled; +using Avalonia.Diagnostics; using Avalonia.Media; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Threading; @@ -96,6 +97,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester /// public IEnumerable HitTest(Point p, Visual? root, Func? filter) { + using var _ = Diagnostic.PerformingHitTest(); + CompositionVisual? rootVisual = null; if (root != null) { @@ -198,7 +201,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester _updating = true; try { - UpdateCore(); + using (Diagnostic.BeginLayoutRenderPass()) + UpdateCore(); } finally { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index e942761fcf..90b973c3a8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; using Avalonia.Collections.Pooled; +using Avalonia.Diagnostics; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Media.Immutable; @@ -122,22 +123,24 @@ namespace Avalonia.Rendering.Composition.Server Revision++; _overlays.MarkUpdateCallStart(); + using (Diagnostic.BeginCompositorUpdatePass()) + { + var transform = Matrix.CreateScale(Scaling, Scaling); + // Update happens in a separate phase to extend dirty rect if needed + Root.Update(this, transform); - var transform = Matrix.CreateScale(Scaling, Scaling); - // Update happens in a separate phase to extend dirty rect if needed - Root.Update(this, transform); + while (_adornerUpdateQueue.Count > 0) + { + var adorner = _adornerUpdateQueue.Dequeue(); + adorner.Update(this, transform); + } - while (_adornerUpdateQueue.Count > 0) - { - var adorner = _adornerUpdateQueue.Dequeue(); - adorner.Update(this, transform); - } + _updateRequested = false; + Readback.CompleteWrite(Revision); - _updateRequested = false; - Readback.CompleteWrite(Revision); + _overlays.MarkUpdateCallEnd(); + } - _overlays.MarkUpdateCallEnd(); - if (!_redrawRequested) return; @@ -151,6 +154,7 @@ namespace Avalonia.Rendering.Composition.Server using (var renderTargetContext = _renderTarget.CreateDrawingContextWithProperties( this.PixelSize, out var properties)) + using (var renderTiming = Diagnostic.BeginCompositorRenderPass()) { if(needLayer && (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted)) { diff --git a/src/Avalonia.Base/Styling/ControlTheme.cs b/src/Avalonia.Base/Styling/ControlTheme.cs index 6cd09e1808..fbd869a9a7 100644 --- a/src/Avalonia.Base/Styling/ControlTheme.cs +++ b/src/Avalonia.Base/Styling/ControlTheme.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using Avalonia.Diagnostics; using Avalonia.PropertyStore; namespace Avalonia.Styling @@ -46,12 +47,18 @@ namespace Avalonia.Styling if (TargetType is null) throw new InvalidOperationException("ControlTheme has no TargetType."); + using var activity = Diagnostic.AttachingStyle()? + .AddTag(Diagnostic.Tags.Style, this); + if (HasSettersOrAnimations && TargetType.IsAssignableFrom(StyledElement.GetStyleKey(target))) { Attach(target, null, type, true); + activity?.AddTag(Diagnostic.Tags.SelectorResult, SelectorMatchResult.AlwaysThisType); + return SelectorMatchResult.AlwaysThisType; } + activity?.AddTag(Diagnostic.Tags.SelectorResult, SelectorMatchResult.NeverThisType); return SelectorMatchResult.NeverThisType; } } diff --git a/src/Avalonia.Base/Styling/Style.cs b/src/Avalonia.Base/Styling/Style.cs index 44ffc22e91..57ce8d7927 100644 --- a/src/Avalonia.Base/Styling/Style.cs +++ b/src/Avalonia.Base/Styling/Style.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Diagnostics; using Avalonia.PropertyStore; namespace Avalonia.Styling @@ -67,11 +68,16 @@ namespace Avalonia.Styling if (HasSettersOrAnimations) { + using var activity = Diagnostic.AttachingStyle()? + .AddTag(Diagnostic.Tags.Style, this); + var match = Selector?.Match(target, Parent, true) ?? (target == host ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance); + activity?.AddTag(Diagnostic.Tags.SelectorResult, match.Result); + if (match.IsMatch) { Attach(target, match.Activator, type, Selector is not OrSelector); diff --git a/src/Avalonia.Base/Styling/StyleInstance.cs b/src/Avalonia.Base/Styling/StyleInstance.cs index 61cb31c6d0..bfccfa8e83 100644 --- a/src/Avalonia.Base/Styling/StyleInstance.cs +++ b/src/Avalonia.Base/Styling/StyleInstance.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Avalonia.Animation; using Avalonia.Data; +using Avalonia.Diagnostics; using Avalonia.PropertyStore; using Avalonia.Reactive; using Avalonia.Styling.Activators; @@ -100,8 +101,15 @@ namespace Avalonia.Styling _animationTrigger?.OnNext(_activator.GetIsActive()); } + using var activity = _activator is null ? null : Diagnostic.EvaluatingStyle()? + .AddTag(Diagnostic.Tags.Activator, _activator) + .AddTag(Diagnostic.Tags.Selector, (Source as Style)?.Selector) + .AddTag(Diagnostic.Tags.Style, Source as StyleBase); + _isActive = _activator?.GetIsActive() ?? true; hasChanged = _isActive != previous; + + activity?.AddTag(Diagnostic.Tags.IsActive, _isActive); return _isActive; } diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index fd008186fa..ea88109bdf 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -9,6 +9,8 @@ namespace Avalonia.Threading; /// public partial class DispatcherTimer { + internal static int ActiveTimersCount { get; private set; } + /// /// Creates a timer that uses theUI thread's Dispatcher2 to /// process the timer event at background priority. @@ -147,6 +149,7 @@ public partial class DispatcherTimer if (!_isEnabled) { _isEnabled = true; + ActiveTimersCount++; Restart(); } @@ -165,6 +168,7 @@ public partial class DispatcherTimer if (_isEnabled) { _isEnabled = false; + ActiveTimersCount--; updateOSTimer = true; // If the operation is in the queue, abort it. diff --git a/src/Avalonia.Base/Utilities/StopwatchHelper.cs b/src/Avalonia.Base/Utilities/StopwatchHelper.cs index 4719226ea4..622bdc27a0 100644 --- a/src/Avalonia.Base/Utilities/StopwatchHelper.cs +++ b/src/Avalonia.Base/Utilities/StopwatchHelper.cs @@ -10,10 +10,14 @@ namespace Avalonia.Utilities; internal static class StopwatchHelper { private static readonly double s_timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; + private static readonly double s_timestampToMs = s_timestampToTicks / TimeSpan.TicksPerMillisecond; public static TimeSpan GetElapsedTime(long startingTimestamp) => GetElapsedTime(startingTimestamp, Stopwatch.GetTimestamp()); public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => new((long)((endingTimestamp - startingTimestamp) * s_timestampToTicks)); + + public static double GetElapsedTimeMs(long startingTimestamp) + => (Stopwatch.GetTimestamp() - startingTimestamp) * s_timestampToMs; } diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index cf9a050185..52e5251add 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Specialized; using Avalonia.Collections; using Avalonia.Data; +using Avalonia.Diagnostics; using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.Media; @@ -31,6 +32,8 @@ namespace Avalonia [UsableDuringInitialization] public partial class Visual : StyledElement, IAvaloniaListItemValidator { + internal static int RootedVisualChildrenCount { get; private set; } + /// /// Defines the property. /// @@ -493,6 +496,7 @@ namespace Avalonia Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Attached to visual tree"); _visualRoot = e.Root; + RootedVisualChildrenCount++; if (_visualParent is null) { throw new InvalidOperationException("Visual was attached to the root without being added to the visual parent first."); @@ -541,6 +545,7 @@ namespace Avalonia Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree"); _visualRoot = null; + RootedVisualChildrenCount--; if (RenderTransform is IMutableTransform mutableTransform) { diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 3892925c15..fa03cc7be8 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -23,6 +23,7 @@ using Avalonia.Utilities; using Avalonia.Input.Platform; using System.Linq; using System.Threading.Tasks; +using Avalonia.Diagnostics; using Avalonia.Rendering.Composition; using Avalonia.Threading; @@ -839,6 +840,8 @@ namespace Avalonia.Controls { Dispatcher.UIThread.Send(static state => { + using var _ = Diagnostic.BeginLayoutInputPass(); + var (topLevel, e) = (ValueTuple)state!; if (e is RawPointerEventArgs pointerArgs) { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index adc8ff7e58..8dc14463f6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Controls; +using Avalonia.Diagnostics; using Avalonia.Markup.Data; using Avalonia.Markup.Xaml.Converters; using Avalonia.Markup.Xaml.XamlIl.Runtime; @@ -64,6 +65,10 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions // which might be able to give us the resource. if (stack is not null) { + using var activity = Diagnostic.FindingResource()? + .AddTag(Diagnostic.Tags.Key, resourceKey) + .AddTag(Diagnostic.Tags.ThemeVariant, themeVariant); + // avoid allocations iterating the parents when possible if (stack is IAvaloniaXamlIlEagerParentStackProvider eagerStack) { diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 11c85f8ab6..46e2a6a7b0 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -10,7 +10,6 @@ - diff --git a/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj index e532f50e8d..69d4155e78 100644 --- a/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj +++ b/tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj @@ -11,7 +11,6 @@ - From 7d3f490b046f113c191316ae1180af2620109caf Mon Sep 17 00:00:00 2001 From: Betta_Fish <96322503+zxbmmmmmmmmm@users.noreply.github.com> Date: Sat, 8 Mar 2025 22:41:32 +0800 Subject: [PATCH 11/23] [Grid] Add RowSpacing and ColumnSpacing (#18077) * Add spacing properties * Add spacing while computing final offsets * Add spacing while calculating desired size * Add unit tests * fix missing spacing arrangement * draw grid line * Add property changed handler * add new unit test * Resolve star spacing * fix AffectsMeasure * overflow unit test * Clean up --------- Co-authored-by: Poker --- src/Avalonia.Controls/Grid.cs | 110 ++++++++++++++---- .../Avalonia.Controls.UnitTests/GridTests.cs | 85 ++++++++++++++ 2 files changed, 172 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 2e745cd364..0a55a44ad2 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -25,6 +25,8 @@ namespace Avalonia.Controls static Grid() { ShowGridLinesProperty.Changed.AddClassHandler(OnShowGridLinesPropertyChanged); + ColumnSpacingProperty.Changed.AddClassHandler(OnSpacingPropertyChanged); + RowSpacingProperty.Changed.AddClassHandler(OnSpacingPropertyChanged); IsSharedSizeScopeProperty.Changed.AddClassHandler(DefinitionBase.OnIsSharedSizeScopePropertyChanged); ColumnProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); @@ -32,6 +34,7 @@ namespace Avalonia.Controls RowProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); RowSpanProperty.Changed.AddClassHandler(OnCellAttachedPropertyChanged); + AffectsMeasure(ColumnSpacingProperty, RowSpacingProperty); AffectsParentMeasure(ColumnProperty, ColumnSpanProperty, RowProperty, RowSpanProperty); } @@ -161,6 +164,24 @@ namespace Avalonia.Controls set => SetValue(ShowGridLinesProperty, value); } + /// + /// Gets or sets the size of the spacing to place between grid rows. + /// + public double RowSpacing + { + get => GetValue(RowSpacingProperty); + set => SetValue(RowSpacingProperty, value); + } + + /// + /// Gets or sets the size of the spacing to place between grid columns. + /// + public double ColumnSpacing + { + get => GetValue(ColumnSpacingProperty); + set => SetValue(ColumnSpacingProperty, value); + } + /// /// Returns a ColumnDefinitions of column definitions. /// @@ -299,7 +320,7 @@ namespace Avalonia.Controls // the cells belonging to them. // // However, there are cases when topology of a grid causes cyclical - // size dependences. For example: + // size dependencies. For example: // // // column width="Auto" column width="*" @@ -425,17 +446,19 @@ namespace Avalonia.Controls // MeasureCellsGroup(extData.CellGroup1, constraint, false, false); - + double combinedRowSpacing = RowSpacing * (RowDefinitions.Count - 1); + double combinedColumnSpacing = ColumnSpacing * (ColumnDefinitions.Count - 1); + Size innerAvailableSize = new Size(constraint.Width - combinedRowSpacing, constraint.Height - combinedColumnSpacing); { // after Group1 is measured, only Group3 may have cells belonging to Auto rows. bool canResolveStarsV = !HasGroup3CellsInAutoRows; if (canResolveStarsV) { - if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } - MeasureCellsGroup(extData.CellGroup2, constraint, false, false); - if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } - MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); } + MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, false, false); + if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); } + MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false); } else { @@ -444,9 +467,9 @@ namespace Avalonia.Controls bool canResolveStarsU = extData.CellGroup2 > PrivateCells.Length; if (canResolveStarsU) { - if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } - MeasureCellsGroup(extData.CellGroup3, constraint, false, false); - if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } + if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); } + MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false); + if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); } } else { @@ -462,7 +485,7 @@ namespace Avalonia.Controls double[] group2MinSizes = CacheMinSizes(extData.CellGroup2, false); double[] group3MinSizes = CacheMinSizes(extData.CellGroup3, true); - MeasureCellsGroup(extData.CellGroup2, constraint, false, true); + MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, false, true); do { @@ -472,14 +495,14 @@ namespace Avalonia.Controls ApplyCachedMinSizes(group3MinSizes, true); } - if (HasStarCellsU) { ResolveStar(DefinitionsU, constraint.Width); } - MeasureCellsGroup(extData.CellGroup3, constraint, false, false); + if (HasStarCellsU) { ResolveStar(DefinitionsU, innerAvailableSize.Width); } + MeasureCellsGroup(extData.CellGroup3, innerAvailableSize, false, false); // Reset cached Group2Widths ApplyCachedMinSizes(group2MinSizes, false); - if (HasStarCellsV) { ResolveStar(DefinitionsV, constraint.Height); } - MeasureCellsGroup(extData.CellGroup2, constraint, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged); + if (HasStarCellsV) { ResolveStar(DefinitionsV, innerAvailableSize.Height); } + MeasureCellsGroup(extData.CellGroup2, innerAvailableSize, cnt == c_layoutLoopMaxCount, false, out hasDesiredSizeUChanged); } while (hasDesiredSizeUChanged && ++cnt <= c_layoutLoopMaxCount); } @@ -489,8 +512,8 @@ namespace Avalonia.Controls MeasureCellsGroup(extData.CellGroup4, constraint, false, false); gridDesiredSize = new Size( - CalculateDesiredSize(DefinitionsU), - CalculateDesiredSize(DefinitionsV)); + CalculateDesiredSize(DefinitionsU) + ColumnSpacing * (DefinitionsU.Count - 1), + CalculateDesiredSize(DefinitionsV) + RowSpacing * (DefinitionsU.Count - 1)); } } finally @@ -524,9 +547,12 @@ namespace Avalonia.Controls else { Debug.Assert(DefinitionsU.Count > 0 && DefinitionsV.Count > 0); - - SetFinalSize(DefinitionsU, arrangeSize.Width, true); - SetFinalSize(DefinitionsV, arrangeSize.Height, false); + double columnSpacing = ColumnSpacing; + double rowSpacing = RowSpacing; + double combinedRowSpacing = rowSpacing * (RowDefinitions.Count - 1); + double combinedColumnSpacing = columnSpacing * (ColumnDefinitions.Count - 1); + SetFinalSize(DefinitionsU, arrangeSize.Width - combinedColumnSpacing, true); + SetFinalSize(DefinitionsV, arrangeSize.Height - combinedRowSpacing, false); var children = Children; @@ -540,14 +566,13 @@ namespace Avalonia.Controls int rowSpan = PrivateCells[currentCell].RowSpan; Rect cellRect = new Rect( - columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset, - rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset, + columnIndex == 0 ? 0.0 : DefinitionsU[columnIndex].FinalOffset + (columnSpacing * columnIndex), + rowIndex == 0 ? 0.0 : DefinitionsV[rowIndex].FinalOffset + (rowSpacing * rowIndex), GetFinalSizeForRange(DefinitionsU, columnIndex, columnSpan), GetFinalSizeForRange(DefinitionsV, rowIndex, rowSpan)); cell.Arrange(cellRect); - } // update render bound on grid lines renderer visual @@ -2088,7 +2113,7 @@ namespace Avalonia.Controls // double dpi = columns ? dpiScale.DpiScaleX : dpiScale.DpiScaleY; var dpi = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; double[] roundingErrors = RoundingErrors; - double roundedTakenSize = 0.0; + double roundedTakenSize = 0; // round each of the allocated sizes, keeping track of the deltas for (int i = 0; i < definitions.Count; ++i) @@ -2363,6 +2388,17 @@ namespace Avalonia.Controls grid.SetFlags((bool)e.NewValue!, Flags.ShowGridLinesPropertyValue); } + private static void OnSpacingPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) + { + Grid grid = (Grid)d; + + if (grid._extData != null + && grid.ListenToNotifications) + { + grid.CellsStructureDirty = true; + } + } + private static void OnCellAttachedPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { if (d is Visual child) @@ -2674,6 +2710,18 @@ namespace Avalonia.Controls public static readonly StyledProperty ShowGridLinesProperty = AvaloniaProperty.Register(nameof(ShowGridLines)); + /// + /// Defines the property. + /// + public static readonly StyledProperty RowSpacingProperty = + AvaloniaProperty.Register(nameof(RowSpacing)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ColumnSpacingProperty = + AvaloniaProperty.Register(nameof(ColumnSpacingProperty)); + /// /// Column property. This is an attached property. /// Grid defines Column property, so that it can be set @@ -3269,6 +3317,14 @@ namespace Avalonia.Controls drawingContext, grid.ColumnDefinitions[i].FinalOffset, 0.0, grid.ColumnDefinitions[i].FinalOffset, _lastArrangeSize.Height); + + if (grid.ColumnSpacing != 0) + { + DrawGridLine( + drawingContext, + grid.ColumnDefinitions[i].FinalOffset - grid.ColumnSpacing, 0.0, + grid.ColumnDefinitions[i].FinalOffset - grid.ColumnSpacing, _lastArrangeSize.Height); + } } for (int i = 1; i < grid.RowDefinitions.Count; ++i) @@ -3277,6 +3333,14 @@ namespace Avalonia.Controls drawingContext, 0.0, grid.RowDefinitions[i].FinalOffset, _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset); + + if (grid.RowSpacing != 0) + { + DrawGridLine( + drawingContext, + 0.0, grid.RowDefinitions[i].FinalOffset - grid.RowSpacing, + _lastArrangeSize.Width, grid.RowDefinitions[i].FinalOffset - grid.RowSpacing); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index 339a4a7cdd..3eebdde255 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -1656,6 +1656,91 @@ namespace Avalonia.Controls.UnitTests Assert.False(grid.IsArrangeValid); } + [Fact] + public void Should_Grid_Controls_With_Spacing() + { + var target = new Grid + { + RowSpacing = 10, + ColumnSpacing = 10, + RowDefinitions = RowDefinitions.Parse("100,100"), + ColumnDefinitions = ColumnDefinitions.Parse("100,100"), + Children = + { + new Border(), + new Border { [Grid.ColumnProperty] = 1 }, + new Border { [Grid.RowProperty] = 1 }, + new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 } + } + }; + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Rect(0, 0, 210, 210), target.Bounds); + Assert.Equal(new Rect(0, 0, 100, 100), target.Children[0].Bounds); + Assert.Equal(new Rect(110, 0, 100, 100), target.Children[1].Bounds); + Assert.Equal(new Rect(0, 110, 100, 100), target.Children[2].Bounds); + Assert.Equal(new Rect(110, 110, 100, 100), target.Children[3].Bounds); + } + + [Fact] + public void Should_Grid_Controls_With_Spacing_Complicated() + { + var target = new Grid + { + Width = 200, + Height = 200, + RowSpacing = 10, + ColumnSpacing = 10, + RowDefinitions = RowDefinitions.Parse("50,*,2*,Auto"), + ColumnDefinitions = ColumnDefinitions.Parse("50,*,2*,Auto"), + Children = + { + new Border(), + new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 }, + new Border { [Grid.RowProperty] = 2, [Grid.ColumnProperty] = 2 }, + new Border { [Grid.RowProperty] = 3, [Grid.ColumnProperty] = 3, Width = 30, Height = 30 }, + }, + }; + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Rect(0, 0, 200, 200), target.Bounds); + Assert.Equal(new Rect(0, 0, 50, 50), target.Children[0].Bounds); + Assert.Equal(new Rect(60, 60, 30, 30), target.Children[1].Bounds); + Assert.Equal(new Rect(100, 100, 60, 60), target.Children[2].Bounds); + Assert.Equal(new Rect(170, 170, 30, 30), target.Children[3].Bounds); + } + + [Fact] + public void Should_Grid_Controls_With_Spacing_Overflow() + { + var target = new Grid + { + Width = 100, + Height = 100, + ColumnSpacing = 20, + RowSpacing = 20, + ColumnDefinitions = ColumnDefinitions.Parse("30,*,*,Auto"), + RowDefinitions = RowDefinitions.Parse("30,*,*,Auto"), + Children = + { + new Border(), + new Border { [Grid.RowProperty] = 1, [Grid.ColumnProperty] = 1 }, + new Border { [Grid.RowProperty] = 2, [Grid.ColumnProperty] = 2 }, + new Border { [Grid.RowProperty] = 3, [Grid.ColumnProperty] = 3, Width = 30, Height = 30 }, + }, + }; + target.Measure(Size.Infinity); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Rect(0, 0, 100, 100), target.Bounds); + Assert.Equal(new Rect(0, 0, 30, 30), target.Children[0].Bounds); + Assert.Equal(new Rect(50, 50, 0, 0), target.Children[1].Bounds); + Assert.Equal(new Rect(70, 70, 0, 0), target.Children[2].Bounds); + Assert.Equal(new Rect(90, 90, 30, 30), target.Children[3].Bounds); + } + private class TestControl : Control { public Size MeasureSize { get; set; } From 8a7945e492c1bbac60313a4af4b38e56a3eda7af Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Sat, 8 Mar 2025 09:43:48 -0500 Subject: [PATCH 12/23] Bring control into view only if control isn't properly visible in viewport (#18359) * bring control into view only if control isn't currently in viewport * fix margin add comments * add more bring to view tests --- .../Presenters/ScrollContentPresenter.cs | 67 ++++-- .../Presenters/ScrollContentPresenterTests.cs | 193 ++++++++++++++++++ 2 files changed, 239 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 88e10c3ba3..08ee08aec7 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -257,28 +257,12 @@ namespace Avalonia.Controls.Presenters return false; } - var rect = targetRect.TransformToAABB(transform.Value); - var offset = Offset; + var rectangle = targetRect.TransformToAABB(transform.Value).Deflate(new Thickness(Child.Margin.Left, Child.Margin.Top, 0, 0)); + Rect viewport = new Rect(Offset.X, Offset.Y, Viewport.Width, Viewport.Height); - if (rect.Bottom > offset.Y + Viewport.Height) - { - offset = offset.WithY((rect.Bottom - Viewport.Height) + Child.Margin.Top); - } - - if (rect.Y < offset.Y) - { - offset = offset.WithY(rect.Y); - } - - if (rect.Right > offset.X + Viewport.Width) - { - offset = offset.WithX((rect.Right - Viewport.Width) + Child.Margin.Left); - } - - if (rect.X < offset.X) - { - offset = offset.WithX(rect.X); - } + double minX = ComputeScrollOffsetWithMinimalScroll(viewport.Left, viewport.Right, rectangle.Left, rectangle.Right); + double minY = ComputeScrollOffsetWithMinimalScroll(viewport.Top, viewport.Bottom, rectangle.Top, rectangle.Bottom); + var offset = new Vector(minX, minY); if (Offset.NearlyEquals(offset)) { @@ -293,6 +277,47 @@ namespace Avalonia.Controls.Presenters return !Offset.NearlyEquals(oldOffset); } + /// + /// Computes the closest offset to ensure most of the child is visible in the viewport along an axis. + /// + /// The left or top of the viewport + /// The right or bottom of the viewport + /// The left or top of the child + /// The right or bottom of the child + /// + internal static double ComputeScrollOffsetWithMinimalScroll( + double viewportStart, + double viewportEnd, + double childStart, + double childEnd) + { + // If child is at least partially above viewport, i.e. top of child is above viewport top and bottom of child is above viewport bottom. + bool isChildAbove = MathUtilities.LessThan(childStart, viewportStart) && MathUtilities.LessThan(childEnd, viewportEnd); + + // If child is at least partially below viewport, i.e. top of child is below viewport top and bottom of child is below viewport bottom. + bool isChildBelow = MathUtilities.GreaterThan(childEnd, viewportEnd) && MathUtilities.GreaterThan(childStart, viewportStart); + bool isChildLarger = (childEnd - childStart) > (viewportEnd - viewportStart); + + // Value if no updates is needed. The child is fully visible in the viewport, or the viewport is completely within the child's bounds + var res = viewportStart; + + // The child is above the viewport and is smaller than the viewport, or if the child's top is below the viewport top + // and is larger than the viewport, we align the child top to the top of the viewport + if ((isChildAbove && !isChildLarger) + || (isChildBelow && isChildLarger)) + { + res = childStart; + } + // The child is above the viewport and is larger than the viewport, or if the child's smaller but is below the viewport, + // we align the child's bottom to the bottom of the viewport + else if (isChildAbove || isChildBelow) + { + res = (childEnd - (viewportEnd - viewportStart)); + } + + return res; + } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index e70ab52043..8ef5398078 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Layout; using Avalonia.UnitTests; using Xunit; +using Xunit.Sdk; namespace Avalonia.Controls.UnitTests.Presenters { @@ -399,6 +400,198 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(new Vector(150, 150), target.Offset); } + [Fact] + public void BringDescendantIntoView_Should_Not_Move_Child_If_Completely_In_View() + { + Border border = new Border + { + Width = 100, + Height = 20 + }; + var content = new StackPanel() + { + Orientation = Orientation.Vertical, + Width = 100, + }; + + for(int i = 0; i < 100; i++) + { + // border position will be (0,60) + var child = i == 3 ? border : new Border + { + Width = 100, + Height = 20, + }; + content.Children.Add(child); + } + var target = new ScrollContentPresenter + { + CanHorizontallyScroll = true, + CanVerticallyScroll = true, + Width = 200, + Height = 100, + Content = new Decorator + { + Child = content + } + }; + + target.UpdateChild(); + target.Measure(Size.Infinity); + target.Arrange(new Rect(0, 0, 100, 100)); + target.BringDescendantIntoView(border, new Rect(border.Bounds.Size)); + + Assert.Equal(new Vector(0, 0), target.Offset); + } + + [Fact] + public void BringDescendantIntoView_Should_Move_Child_At_Least_Partially_Above_Viewport() + { + Border border = new Border + { + Width = 100, + Height = 20 + }; + var content = new StackPanel() + { + Orientation = Orientation.Vertical, + Width = 100, + }; + + for(int i = 0; i < 100; i++) + { + // border position will be (0,60) + var child = i == 3 ? border : new Border + { + Width = 100, + Height = 20, + }; + content.Children.Add(child); + } + var target = new ScrollContentPresenter + { + CanHorizontallyScroll = true, + CanVerticallyScroll = true, + Width = 200, + Height = 100, + Content = new Decorator + { + Child = content + } + }; + + target.UpdateChild(); + target.Measure(Size.Infinity); + target.Arrange(new Rect(0, 0, 100, 100)); + // move border to above the view port + target.Offset = new Vector(0, 90); + target.BringDescendantIntoView(border, new Rect(border.Bounds.Size)); + + Assert.Equal(new Vector(0, 60), target.Offset); + + // move border to partially above the view port + target.Offset = new Vector(0, 70); + target.BringDescendantIntoView(border, new Rect(border.Bounds.Size)); + + Assert.Equal(new Vector(0, 60), target.Offset); + } + + [Fact] + public void BringDescendantIntoView_Should_Not_Move_Child_If_Completely_Covers_Viewport() + { + Border border = new Border + { + Width = 100, + Height = 200 + }; + var content = new StackPanel() + { + Orientation = Orientation.Vertical, + Width = 100, + }; + + for (int i = 0; i < 100; i++) + { + // border position will be (0,60) + var child = i == 3 ? border : new Border + { + Width = 100, + Height = 20, + }; + content.Children.Add(child); + } + var target = new ScrollContentPresenter + { + CanHorizontallyScroll = true, + CanVerticallyScroll = true, + Width = 200, + Height = 100, + Content = new Decorator + { + Child = content + } + }; + + target.UpdateChild(); + target.Measure(Size.Infinity); + target.Arrange(new Rect(0, 0, 100, 100)); + // move border such that it's partially above viewport and partially below viewport + target.Offset = new Vector(0, 90); + target.BringDescendantIntoView(border, new Rect(border.Bounds.Size)); + + Assert.Equal(new Vector(0, 90), target.Offset); + } + + [Fact] + public void BringDescendantIntoView_Should_Move_Child_At_Least_Partially_Below_Viewport() + { + Border border = new Border + { + Width = 100, + Height = 20 + }; + var content = new StackPanel() + { + Orientation = Orientation.Vertical, + Width = 100, + }; + + for (int i = 0; i < 100; i++) + { + // border position will be (0,180) + var child = i == 9 ? border : new Border + { + Width = 100, + Height = 20, + }; + content.Children.Add(child); + } + var target = new ScrollContentPresenter + { + CanHorizontallyScroll = true, + CanVerticallyScroll = true, + Width = 200, + Height = 100, + Content = new Decorator + { + Child = content + } + }; + + target.UpdateChild(); + target.Measure(Size.Infinity); + target.Arrange(new Rect(0, 0, 100, 100)); + + // border is at (0, 180) and below the viewport + target.BringDescendantIntoView(border, new Rect(border.Bounds.Size)); + + Assert.Equal(new Vector(0, 100), target.Offset); + + // move border to partially below the view port + target.Offset = new Vector(0, 90); + target.BringDescendantIntoView(border, new Rect(border.Bounds.Size)); + } + [Fact] public void Nested_Presenters_Should_Scroll_Outer_When_Content_Exceeds_Viewport() { From 6e1cab99f67b608f953afcbbcef0a04a1f666c0c Mon Sep 17 00:00:00 2001 From: Rastislav Svoboda Date: Sat, 8 Mar 2025 16:05:02 +0100 Subject: [PATCH 13/23] Fix: TextBox having selection and pressing up / down arrows (#18385) --- src/Avalonia.Controls/TextBox.cs | 31 +++- .../TextBoxTests.cs | 174 ++++++++++++++++++ 2 files changed, 198 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 3d3dc87b01..90d17c432a 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1467,6 +1467,11 @@ namespace Avalonia.Controls { selection = DetectSelection(); + if (!selection && SelectionStart != SelectionEnd) + { + ClearSelectionAndMoveCaretToTextPosition(LogicalDirection.Backward); + } + _presenter.MoveCaretVertical(LogicalDirection.Backward); if (caretIndex != _presenter.CaretIndex) @@ -1489,6 +1494,11 @@ namespace Avalonia.Controls { selection = DetectSelection(); + if (!selection && SelectionStart != SelectionEnd) + { + ClearSelectionAndMoveCaretToTextPosition(LogicalDirection.Forward); + } + _presenter.MoveCaretVertical(); if (caretIndex != _presenter.CaretIndex) @@ -1983,13 +1993,9 @@ namespace Avalonia.Controls { if (selectionStart != selectionEnd) { - // clear the selection and move to the appropriate side of previous selection - var newPosition = direction > 0 ? - Math.Max(selectionStart, selectionEnd) : - Math.Min(selectionStart, selectionEnd); - SetCurrentValue(SelectionStartProperty, newPosition); - SetCurrentValue(SelectionEndProperty, newPosition); - _presenter.MoveCaretToTextPosition(newPosition); + ClearSelectionAndMoveCaretToTextPosition(direction > 0 ? + LogicalDirection.Forward : + LogicalDirection.Backward); } else { @@ -2100,6 +2106,17 @@ namespace Avalonia.Controls _scrollViewer?.PageDown(); } + private void ClearSelectionAndMoveCaretToTextPosition(LogicalDirection direction) + { + var newPosition = direction == LogicalDirection.Forward ? + Math.Max(SelectionStart, SelectionEnd) : + Math.Min(SelectionStart, SelectionEnd); + SetCurrentValue(SelectionStartProperty, newPosition); + SetCurrentValue(SelectionEndProperty, newPosition); + // move caret to appropriate side of previous selection + _presenter?.MoveCaretToTextPosition(newPosition); + } + /// /// Scroll the to the specified line index. /// diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 36a9c8cc68..81f3641329 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1540,6 +1540,7 @@ namespace Avalonia.Controls.UnitTests [InlineData(0,4)] [InlineData(2,6)] [InlineData(0,6)] + [InlineData(3,4)] public void When_Selection_From_Left_To_Right_Pressing_Right_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd) { using (UnitTestApplication.Start(Services)) @@ -1568,6 +1569,7 @@ namespace Avalonia.Controls.UnitTests [InlineData(0,4)] [InlineData(2,6)] [InlineData(0,6)] + [InlineData(3,4)] public void When_Selection_From_Left_To_Right_Pressing_Left_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd) { using (UnitTestApplication.Start(Services)) @@ -1596,6 +1598,7 @@ namespace Avalonia.Controls.UnitTests [InlineData(4,0)] [InlineData(6,2)] [InlineData(6,0)] + [InlineData(4,3)] public void When_Selection_From_Right_To_Left_Pressing_Right_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd) { using (UnitTestApplication.Start(Services)) @@ -1624,6 +1627,7 @@ namespace Avalonia.Controls.UnitTests [InlineData(4,0)] [InlineData(6,2)] [InlineData(6,0)] + [InlineData(4,3)] public void When_Selection_From_Right_To_Left_Pressing_Left_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd) { using (UnitTestApplication.Start(Services)) @@ -1701,6 +1705,176 @@ namespace Avalonia.Controls.UnitTests } } + [Theory] + [InlineData(2,4)] + [InlineData(0,4)] + [InlineData(2,6)] + [InlineData(0,6)] + [InlineData(3,4)] + public void When_Selection_From_Left_To_Right_Pressing_Up_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd) + { + using (UnitTestApplication.Start(Services)) + { + var tb = new TextBox + { + Template = CreateTemplate(), + Text = "ABCDEF" + }; + + tb.Measure(Size.Infinity); + tb.CaretIndex = selectionStart; + tb.SelectionStart = selectionStart; + tb.SelectionEnd = selectionEnd; + + RaiseKeyEvent(tb, Key.Up, KeyModifiers.None); + + Assert.Equal(selectionStart, tb.SelectionStart); + Assert.Equal(selectionStart, tb.SelectionEnd); + Assert.Equal(selectionStart, tb.CaretIndex); + } + } + + [Theory] + [InlineData(4,2)] + [InlineData(4,0)] + [InlineData(6,2)] + [InlineData(6,0)] + [InlineData(4,3)] + public void When_Selection_From_Right_To_Left_Pressing_Up_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd) + { + using (UnitTestApplication.Start(Services)) + { + var tb = new TextBox + { + Template = CreateTemplate(), + Text = "ABCDEF" + }; + + tb.Measure(Size.Infinity); + tb.CaretIndex = selectionStart; + tb.SelectionStart = selectionStart; + tb.SelectionEnd = selectionEnd; + + RaiseKeyEvent(tb, Key.Up, KeyModifiers.None); + + Assert.Equal(selectionEnd, tb.SelectionStart); + Assert.Equal(selectionEnd, tb.SelectionEnd); + Assert.Equal(selectionEnd, tb.CaretIndex); + } + } + + [Theory] + [InlineData(0)] + [InlineData(2)] + [InlineData(4)] + [InlineData(6)] + public void When_Select_All_From_Position_Up_Should_Remove_Selection_Moving_Caret_To_Start(int caretIndex) + { + using (UnitTestApplication.Start(Services)) + { + var tb = new TextBox + { + Template = CreateTemplate(), + Text = "ABCDEF" + }; + + tb.Measure(Size.Infinity); + tb.CaretIndex = caretIndex; + + RaiseKeyEvent(tb, Key.A, KeyModifiers.Control); + RaiseKeyEvent(tb, Key.Up, KeyModifiers.None); + + Assert.Equal(0, tb.SelectionStart); + Assert.Equal(0, tb.SelectionEnd); + Assert.Equal(0, tb.CaretIndex); + } + } + + [Theory] + [InlineData(2,4)] + [InlineData(0,4)] + [InlineData(2,6)] + [InlineData(0,6)] + [InlineData(3,4)] + public void When_Selection_From_Left_To_Right_Pressing_Down_Should_Remove_Selection_Moving_Caret_To_End_Of_Previous_Selection(int selectionStart, int selectionEnd) + { + using (UnitTestApplication.Start(Services)) + { + var tb = new TextBox + { + Template = CreateTemplate(), + Text = "ABCDEF" + }; + + tb.Measure(Size.Infinity); + tb.CaretIndex = selectionStart; + tb.SelectionStart = selectionStart; + tb.SelectionEnd = selectionEnd; + + RaiseKeyEvent(tb, Key.Down, KeyModifiers.None); + + Assert.Equal(selectionEnd, tb.SelectionStart); + Assert.Equal(selectionEnd, tb.SelectionEnd); + Assert.Equal(selectionEnd, tb.CaretIndex); + } + } + + [Theory] + [InlineData(4,2)] + [InlineData(4,0)] + [InlineData(6,2)] + [InlineData(6,0)] + [InlineData(4,3)] + public void When_Selection_From_Right_To_Left_Pressing_Down_Should_Remove_Selection_Moving_Caret_To_Start_Of_Previous_Selection(int selectionStart, int selectionEnd) + { + using (UnitTestApplication.Start(Services)) + { + var tb = new TextBox + { + Template = CreateTemplate(), + Text = "ABCDEF" + }; + + tb.Measure(Size.Infinity); + tb.CaretIndex = selectionStart; + tb.SelectionStart = selectionStart; + tb.SelectionEnd = selectionEnd; + + RaiseKeyEvent(tb, Key.Down, KeyModifiers.None); + + Assert.Equal(selectionStart, tb.SelectionStart); + Assert.Equal(selectionStart, tb.SelectionEnd); + Assert.Equal(selectionStart, tb.CaretIndex); + } + } + + [Theory] + [InlineData(0)] + [InlineData(2)] + [InlineData(4)] + [InlineData(6)] + public void When_Select_All_From_Position_Down_Should_Remove_Selection_Moving_Caret_To_End(int caretIndex) + { + using (UnitTestApplication.Start(Services)) + { + var tb = new TextBox + { + Template = CreateTemplate(), + Text = "ABCDEF" + }; + + tb.Measure(Size.Infinity); + tb.CaretIndex = caretIndex; + + RaiseKeyEvent(tb, Key.A, KeyModifiers.Control); + RaiseKeyEvent(tb, Key.Down, KeyModifiers.None); + + Assert.Equal(tb.Text.Length, tb.SelectionStart); + Assert.Equal(tb.Text.Length, tb.SelectionEnd); + Assert.Equal(tb.Text.Length, tb.CaretIndex); + } + } + [Fact] public void TextBox_In_AdornerLayer_Will_Not_Cause_Collection_Modified_In_VisualLayerManager_Measure() { From 82329882ac6b3487da6af2a3675810b80a819000 Mon Sep 17 00:00:00 2001 From: Maxwell Katz Date: Sun, 9 Mar 2025 00:13:30 +0900 Subject: [PATCH 14/23] Support complex types in datatype (#18379) * Fix XamlTypeExtensionNode not being handled on the x:DataType transformer * Add testt with complex DataType * Make vm:MainWindowViewModel+TestItem nested type generic on BindingDemo --- samples/BindingDemo/BindingDemo.csproj | 1 + samples/BindingDemo/MainWindow.xaml | 13 ++++--- samples/BindingDemo/MainWindow.xaml.cs | 2 +- samples/BindingDemo/TestItemView.xaml | 4 +- .../ViewModels/MainWindowViewModel.cs | 22 +++++------ ...valoniaXamlIlDataContextTypeTransformer.cs | 6 ++- .../CompiledBindingExtensionTests.cs | 38 +++++++++++++++++++ 7 files changed, 65 insertions(+), 21 deletions(-) diff --git a/samples/BindingDemo/BindingDemo.csproj b/samples/BindingDemo/BindingDemo.csproj index faeb643d8a..33c6aa1440 100644 --- a/samples/BindingDemo/BindingDemo.csproj +++ b/samples/BindingDemo/BindingDemo.csproj @@ -2,6 +2,7 @@ Exe $(AvsCurrentTargetFramework) + diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index ed23f68d91..9d68c8da8a 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -3,6 +3,7 @@ x:Class="BindingDemo.MainWindow" xmlns:vm="using:BindingDemo.ViewModels" xmlns:local="using:BindingDemo" + xmlns:system="clr-namespace:System;assembly=System.Runtime" Title="AvaloniaUI Bindings Test" Width="800" Height="600" @@ -29,7 +30,7 @@ - + @@ -51,9 +52,9 @@ + Text="{Binding Value, Source={StaticResource SharedItem}, Mode=TwoWay, DataType={x:Type vm:MainWindowViewModel+TestItem, x:TypeArguments=x:String}}"/> + Text="{Binding Value, Source={StaticResource SharedItem}, Mode=TwoWay, DataType={x:Type vm:MainWindowViewModel+TestItem, x:TypeArguments=x:String}}"/> @@ -67,8 +68,8 @@ - - + + @@ -81,7 +82,7 @@ - + diff --git a/samples/BindingDemo/MainWindow.xaml.cs b/samples/BindingDemo/MainWindow.xaml.cs index 57466fe581..36d4feb108 100644 --- a/samples/BindingDemo/MainWindow.xaml.cs +++ b/samples/BindingDemo/MainWindow.xaml.cs @@ -9,7 +9,7 @@ namespace BindingDemo { public MainWindow() { - Resources["SharedItem"] = new MainWindowViewModel.TestItem() { StringValue = "shared" }; + Resources["SharedItem"] = new MainWindowViewModel.TestItem() { Value = "shared" }; this.InitializeComponent(); this.DataContext = new MainWindowViewModel(); this.AttachDevTools(); diff --git a/samples/BindingDemo/TestItemView.xaml b/samples/BindingDemo/TestItemView.xaml index 6eb59ffc83..fcf3a80ceb 100644 --- a/samples/BindingDemo/TestItemView.xaml +++ b/samples/BindingDemo/TestItemView.xaml @@ -2,9 +2,9 @@ xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:viewModels="using:BindingDemo.ViewModels" x:Class="BindingDemo.TestItemView" - x:DataType="viewModels:MainWindowViewModel+TestItem"> + x:DataType="{x:Type viewModels:MainWindowViewModel+TestItem, x:TypeArguments=x:String}"> - + diff --git a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs index f5f4f3531f..eb1a007695 100644 --- a/samples/BindingDemo/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingDemo/ViewModels/MainWindowViewModel.cs @@ -23,14 +23,14 @@ namespace BindingDemo.ViewModels public MainWindowViewModel() { - Items = new ObservableCollection( - Enumerable.Range(0, 20).Select(x => new TestItem + Items = new ObservableCollection>( + Enumerable.Range(0, 20).Select(x => new TestItem { - StringValue = "Item " + x, + Value = "Item " + x, Detail = "Item " + x + " details", })); - Selection = new SelectionModel { SingleSelect = false }; + Selection = new SelectionModel> { SingleSelect = false }; ShuffleItems = MiniCommand.Create(() => { @@ -58,8 +58,8 @@ namespace BindingDemo.ViewModels .Select(x => DateTimeOffset.Now); } - public ObservableCollection Items { get; } - public SelectionModel Selection { get; } + public ObservableCollection> Items { get; } + public SelectionModel> Selection { get; } public MiniCommand ShuffleItems { get; } public string BooleanString @@ -117,15 +117,15 @@ namespace BindingDemo.ViewModels } // Nested class, jsut so we can test it in XAML - public class TestItem : ViewModelBase + public class TestItem : ViewModelBase { - private string _stringValue = "String Value"; + private T _value; private string _detail; - public string StringValue + public T Value { - get { return _stringValue; } - set { this.RaiseAndSetIfChanged(ref this._stringValue, value); } + get { return _value; } + set { this.RaiseAndSetIfChanged(ref this._value, value); } } public string Detail diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index 6b4f32bce0..8632ea6ee0 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -44,7 +44,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { on.Children.RemoveAt(i); i--; - if (directive.Values[0] is XamlAstTextNode text) + if (directive.Values[0] is XamlTypeExtensionNode typeNode) + { + directiveDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, typeNode.Value.GetClrType()); + } + else if (directive.Values[0] is XamlAstTextNode text) { directiveDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 327f06f5eb..069c3a3426 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -2369,6 +2369,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Resolves_Nested_Generic_DataTypes() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = (Window)AvaloniaRuntimeXamlLoader.Load(@" + + + + +"); + var textBlock = window.GetControl("textBlock"); + + var dataContext = new TestDataContext + { + NestedGenericString = new TestDataContext.NestedGeneric + { + Value = "10" + } + }; + + window.DataContext = dataContext.NestedGenericString; + + Assert.Equal(dataContext.NestedGenericString.Value, textBlock.Text); + } + } + static void Throws(string type, Action cb) { try @@ -2459,6 +2490,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public INonIntegerIndexerDerived NonIntegerIndexerInterfaceProperty => NonIntegerIndexerProperty; + public NestedGeneric? NestedGenericString { get; init; } + string IHasExplicitProperty.ExplicitProperty => "Hello"; public string ExplicitProperty => "Bye"; @@ -2484,6 +2517,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } } + + public class NestedGeneric + { + public T Value { get; set; } + } } public class ListItemCollectionView : List From edaaf5bbda2320d4392bd5d76c1c8cc519727c07 Mon Sep 17 00:00:00 2001 From: Vadim Kutin Date: Sat, 8 Mar 2025 16:44:14 +0100 Subject: [PATCH 15/23] Improve KeyGesture.ToString() output in case when Key is set to Key.None (#18353) * Improve ToString() output in case when Key is set to Key.None * Update KeyGesture.Parse method to support only modifiers combinations * Update tests with empty combination and modifiers-only combinations --- src/Avalonia.Base/Input/KeyGesture.cs | 26 ++++++++++--------- .../Input/KeyGestureTests.cs | 5 ++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index 122176a127..83d99bf7a9 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -79,15 +79,10 @@ namespace Avalonia.Input { var partSpan = gesture.AsSpan(cstart, c - cstart).Trim(); - if (isLast) - { - key = ParseKey(partSpan.ToString()); - } - else + if (!TryParseKey(partSpan.ToString(), out key)) { keyModifiers |= ParseModifier(partSpan); } - cstart = c + 1; } } @@ -151,8 +146,11 @@ namespace Avalonia.Input s.Append(formatInfo.Meta); } - Plus(s); - s.Append(formatInfo.FormatKey(Key)); + if ((Key != Key.None) || (KeyModifiers == KeyModifiers.None)) + { + Plus(s); + s.Append(formatInfo.FormatKey(Key)); + } return StringBuilderCache.GetStringAndRelease(s); } @@ -163,12 +161,16 @@ namespace Avalonia.Input ResolveNumPadOperationKey(keyEvent.Key) == ResolveNumPadOperationKey(Key); // TODO: Move that to external key parser - private static Key ParseKey(string key) + private static bool TryParseKey(string keyStr, out Key key) { - if (s_keySynonyms.TryGetValue(key.ToLower(CultureInfo.InvariantCulture), out Key rv)) - return rv; + key = Key.None; + if (s_keySynonyms.TryGetValue(keyStr.ToLower(CultureInfo.InvariantCulture), out key)) + return true; + + if (EnumHelper.TryParse(keyStr, true, out key)) + return true; - return EnumHelper.Parse(key, true); + return false; } private static KeyModifiers ParseModifier(ReadOnlySpan modifier) diff --git a/tests/Avalonia.Base.UnitTests/Input/KeyGestureTests.cs b/tests/Avalonia.Base.UnitTests/Input/KeyGestureTests.cs index 33cd94ec98..790d1f9c49 100644 --- a/tests/Avalonia.Base.UnitTests/Input/KeyGestureTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/KeyGestureTests.cs @@ -13,6 +13,9 @@ namespace Avalonia.Base.UnitTests.Input new object[]{"Control++", new KeyGesture(Key.OemPlus, KeyModifiers.Control) }, new object[]{ "Shift+⌘+A", new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift) }, new object[]{ "Shift+Cmd+A", new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift) }, + new object[]{"None", new KeyGesture(Key.None)}, + new object[]{"Alt+Shift", new KeyGesture(Key.None, KeyModifiers.Alt | KeyModifiers.Shift)}, + }; public static readonly IEnumerable ToStringData = new object[][] @@ -23,6 +26,8 @@ namespace Avalonia.Base.UnitTests.Input new object[]{new KeyGesture(Key.A, KeyModifiers.Alt | KeyModifiers.Shift), "Shift+Alt+A"}, new object[]{new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift), "Ctrl+Shift+Alt+A"}, new object[]{new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift), "Shift+Cmd+A"}, + new object[]{new KeyGesture(Key.None), "None"}, + new object[]{new KeyGesture(Key.None, KeyModifiers.Alt | KeyModifiers.Shift), "Shift+Alt"}, }; [Theory] From d55421bdc9d738393c33f61ea2af10ab3d179956 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Sun, 9 Mar 2025 11:15:29 +0100 Subject: [PATCH 16/23] Introduce GlyphTypeface.FaceNames (#18392) Co-authored-by: Julien Lebosquain --- .../Media/Fonts/Tables/Name/NameTable.cs | 48 +++++-------------- src/Avalonia.Base/Media/IGlyphTypeface2.cs | 6 +++ src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 35 ++++++++++++-- 3 files changed, 51 insertions(+), 38 deletions(-) diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs index 9a4b664f47..c0c1048e51 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs @@ -2,26 +2,25 @@ // Licensed under the Apache License, Version 2.0. // Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts +using System.Collections; using System.Collections.Generic; using System.IO; +using Avalonia.Utilities; namespace Avalonia.Media.Fonts.Tables.Name { - internal class NameTable + internal class NameTable : IEnumerable { internal const string TableName = "name"; internal static readonly OpenTypeTag Tag = OpenTypeTag.Parse(TableName); private readonly NameRecord[] _names; - internal NameTable(NameRecord[] names, IReadOnlyList languages) + internal NameTable(NameRecord[] names) { _names = names; - Languages = languages; } - public IReadOnlyList Languages { get; } - /// /// Gets the name of the font. /// @@ -133,22 +132,6 @@ namespace Avalonia.Media.Fonts.Tables.Name } } - //var languageNames = Array.Empty(); - - //if (format == 1) - //{ - // // Format 1 adds language data. - // var langCount = reader.ReadUInt16(); - // languageNames = new StringLoader[langCount]; - - // for (var i = 0; i < langCount; i++) - // { - // languageNames[i] = StringLoader.Create(reader); - - // strings.Add(languageNames[i]); - // } - //} - foreach (var readable in strings) { var readableStartOffset = stringOffset + readable.Offset; @@ -158,22 +141,17 @@ namespace Avalonia.Media.Fonts.Tables.Name readable.LoadValue(reader); } - var cultures = new List(); - - foreach (var nameRecord in names) - { - if (nameRecord.NameID != KnownNameIds.FontFamilyName || nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) - { - continue; - } + return new NameTable(names); + } - if (!cultures.Contains(nameRecord.LanguageID)) - { - cultures.Add(nameRecord.LanguageID); - } - } + public IEnumerator GetEnumerator() + { + return new ImmutableReadOnlyListStructEnumerator(_names); + } - return new NameTable(names, cultures); + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } } } diff --git a/src/Avalonia.Base/Media/IGlyphTypeface2.cs b/src/Avalonia.Base/Media/IGlyphTypeface2.cs index 84b892dae3..3bd2b1e767 100644 --- a/src/Avalonia.Base/Media/IGlyphTypeface2.cs +++ b/src/Avalonia.Base/Media/IGlyphTypeface2.cs @@ -29,5 +29,11 @@ namespace Avalonia.Media /// Gets supported font features. /// IReadOnlyList SupportedFeatures { get; } + + /// + /// Gets the localized face names. + /// Keys are culture identifiers. + /// + IReadOnlyDictionary FaceNames { get; } } } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index 9170393034..2def64c18d 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs @@ -111,18 +111,45 @@ namespace Avalonia.Skia if(_nameTable != null) { - var familyNames = new Dictionary(_nameTable.Languages.Count); + var familyNames = new Dictionary(1); + var faceNames = new Dictionary(1); - foreach (var language in _nameTable.Languages) + foreach (var nameRecord in _nameTable) { - familyNames.Add(language, _nameTable.FontFamilyName(language)); + if(nameRecord.NameID == KnownNameIds.FontFamilyName) + { + if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) + { + continue; + } + + if (!familyNames.ContainsKey(nameRecord.LanguageID)) + { + familyNames[nameRecord.LanguageID] = nameRecord.Value; + } + } + + if(nameRecord.NameID == KnownNameIds.FontSubfamilyName) + { + if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) + { + continue; + } + + if (!faceNames.ContainsKey(nameRecord.LanguageID)) + { + faceNames[nameRecord.LanguageID] = nameRecord.Value; + } + } } FamilyNames = familyNames; + FaceNames = faceNames; } else { FamilyNames = new Dictionary { { (ushort)CultureInfo.InvariantCulture.LCID, FamilyName } }; + FaceNames = new Dictionary { { (ushort)CultureInfo.InvariantCulture.LCID, Weight.ToString() } }; } } @@ -130,6 +157,8 @@ namespace Avalonia.Skia public IReadOnlyDictionary FamilyNames { get; } + public IReadOnlyDictionary FaceNames { get; } + public IReadOnlyList SupportedFeatures { get From 156f587cfadd503505079d9518f89184ea1769dd Mon Sep 17 00:00:00 2001 From: Johan Polson <40406620+johanpolson@users.noreply.github.com> Date: Sun, 9 Mar 2025 11:46:09 +0100 Subject: [PATCH 17/23] Use "handled" for keybord input in Browser (#18349) Co-authored-by: Julien Lebosquain --- .../Avalonia.Browser/Interop/InputHelper.cs | 15 +++++++++++---- .../webapp/modules/avalonia/input.ts | 19 +++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index 29ddb36e9e..088c1d4a9c 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -13,16 +13,23 @@ internal static partial class InputHelper return Task.CompletedTask; } + public static Task RedirectInputRetunAsync(int topLevelId, Func handler, T @default) + { + if (BrowserTopLevelImpl.TryGetTopLevel(topLevelId) is { } topLevelImpl) + return Task.FromResult(handler(topLevelImpl)); + return Task.FromResult(@default); + } + [JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribeInputEvents(JSObject htmlElement, int topLevelId); [JSExport] - public static Task OnKeyDown(int topLevelId, string code, string key, int modifier) => - RedirectInputAsync(topLevelId, t => t.InputHandler.OnKeyDown(code, key, modifier)); + public static Task OnKeyDown(int topLevelId, string code, string key, int modifier) => + RedirectInputRetunAsync(topLevelId, t => t.InputHandler.OnKeyDown(code, key, modifier), false); [JSExport] - public static Task OnKeyUp(int topLevelId, string code, string key, int modifier) => - RedirectInputAsync(topLevelId, t => t.InputHandler.OnKeyUp(code, key, modifier)); + public static Task OnKeyUp(int topLevelId, string code, string key, int modifier) => + RedirectInputRetunAsync(topLevelId, t => t.InputHandler.OnKeyUp(code, key, modifier), false); [JSExport] public static Task OnBeforeInput(int topLevelId, string inputType, int start, int end) => diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index 5d14597642..704028271d 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -95,16 +95,23 @@ export class InputHelper { public static subscribeKeyEvents(element: HTMLInputElement, topLevelId: number) { const keyDownHandler = (args: KeyboardEvent) => { - JsExports.InputHelper.OnKeyDown(topLevelId, args.code, args.key, this.getModifiers(args)); - if (this.clipboardState !== ClipboardState.Pending) { - args.preventDefault(); - } + JsExports.InputHelper.OnKeyDown(topLevelId, args.code, args.key, this.getModifiers(args)) + .then((handled: boolean) => { + if (!handled || this.clipboardState !== ClipboardState.Pending) { + args.preventDefault(); + } + }); }; element.addEventListener("keydown", keyDownHandler); const keyUpHandler = (args: KeyboardEvent) => { - JsExports.InputHelper.OnKeyUp(topLevelId, args.code, args.key, this.getModifiers(args)); - args.preventDefault(); + JsExports.InputHelper.OnKeyUp(topLevelId, args.code, args.key, this.getModifiers(args)) + .then((handled: boolean) => { + if (!handled) { + args.preventDefault(); + } + }); + if (this.rejectClipboard) { this.rejectClipboard(); } From ce72eced6b62845228d1ee291c51b80a1b80c36d Mon Sep 17 00:00:00 2001 From: StefanKoell Date: Sun, 9 Mar 2025 21:46:32 +0100 Subject: [PATCH 18/23] Implement SetItems method in ResourceDictionary (#18354) * Implement AddOrUpdateRange method in ResourceDictionary * Change to SetItems method nam, remove flag --------- Co-authored-by: Julien Lebosquain --- src/Avalonia.Base/Controls/ResourceDictionary.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Controls/ResourceDictionary.cs b/src/Avalonia.Base/Controls/ResourceDictionary.cs index 748882a450..ded0a815a5 100644 --- a/src/Avalonia.Base/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Base/Controls/ResourceDictionary.cs @@ -40,7 +40,8 @@ namespace Avalonia.Controls set { Inner[key] = value; - Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } } @@ -150,6 +151,19 @@ namespace Avalonia.Controls public void AddNotSharedDeferred(object key, IDeferredContent deferredContent) => Add(key, new NotSharedDeferredItem(deferredContent)); + public void SetItems(IEnumerable> values) + { + try + { + foreach (var value in values) + Inner[value.Key] = value.Value; + } + finally + { + Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); + } + } + public void Clear() { if (_inner?.Count > 0) From c416fc716d6e853ddce69307a5bf4b2f7d8415c3 Mon Sep 17 00:00:00 2001 From: Dong Bin <14807942+rabbitism@users.noreply.github.com> Date: Tue, 11 Mar 2025 16:28:30 +0800 Subject: [PATCH 19/23] fix: InputMethod should remove handler. (#18414) --- src/Avalonia.Base/Input/InputMethod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Input/InputMethod.cs b/src/Avalonia.Base/Input/InputMethod.cs index 9df48a7d2e..b8b6c564a9 100644 --- a/src/Avalonia.Base/Input/InputMethod.cs +++ b/src/Avalonia.Base/Input/InputMethod.cs @@ -43,7 +43,7 @@ namespace Avalonia.Input public static void RemoveTextInputMethodClientRequeryRequestedHandler(Interactive element, EventHandler handler) { - element.AddHandler(TextInputMethodClientRequeryRequestedEvent, handler); + element.RemoveHandler(TextInputMethodClientRequeryRequestedEvent, handler); } private InputMethod() From 6a010a89f2d13adba0a6cb3433f47ebc76eb467f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Krysi=C5=84ski?= Date: Tue, 11 Mar 2025 09:33:48 +0100 Subject: [PATCH 20/23] Added surface dispose to DrawingSurfaceDemoBase (#18412) --- samples/GpuInterop/DrawingSurfaceDemoBase.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/samples/GpuInterop/DrawingSurfaceDemoBase.cs b/samples/GpuInterop/DrawingSurfaceDemoBase.cs index be5d920b89..b076f5b489 100644 --- a/samples/GpuInterop/DrawingSurfaceDemoBase.cs +++ b/samples/GpuInterop/DrawingSurfaceDemoBase.cs @@ -33,7 +33,11 @@ public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { if (_initialized) + { + Surface?.Dispose(); FreeGraphicsResources(); + } + _initialized = false; base.OnDetachedFromLogicalTree(e); } From e189ef9c53e887d44f495ff9e0573195e0427bcd Mon Sep 17 00:00:00 2001 From: Emmanuel Hansen Date: Tue, 11 Mar 2025 19:02:30 -0400 Subject: [PATCH 21/23] Fix - Search for visual parents when hittesting for pointer events (#18416) * search for visual parents when hittesting * Add unit test for hit testing on disabled visual --------- Co-authored-by: Julien Lebosquain --- src/Avalonia.Controls/TopLevel.cs | 2 +- .../Input/PointerOverTests.cs | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index fa03cc7be8..3a71b32685 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -866,7 +866,7 @@ namespace Avalonia.Controls var candidate = hitTestElement; while (candidate?.IsEffectivelyEnabled == false) { - candidate = (candidate as Visual)?.Parent as IInputElement; + candidate = (candidate as Visual)?.VisualParent as IInputElement; } return candidate; diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs index 45277ca75f..4f9531cd63 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs @@ -6,6 +6,7 @@ using Avalonia.Controls; using Avalonia.Headless; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Media; using Avalonia.Rendering; using Avalonia.UnitTests; @@ -494,6 +495,60 @@ namespace Avalonia.Base.UnitTests.Input result); } + [Fact] + public void Disabled_Element_Should_Set_PointerOver_On_Visual_Parent() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(); + + var disabledChild = new Border + { + Background = Brushes.Red, + Width = 100, + Height = 100, + IsEnabled = false + }; + + var visualParent = new Border + { + Background = Brushes.Black, + Width = 100, + Height = 100, + Child = disabledChild + }; + + var logicalParent = new Border + { + Background = Brushes.Blue, + Width = 100, + Height = 100 + }; + + // Change the logical parent and check that we're correctly hit testing on the visual tree. + // This scenario is made up because it's easy to test. + // In the real world, this happens with nested Popups from MenuItems (but that's very cumbersome to test). + ((ISetLogicalParent) disabledChild).SetParent(null); + ((ISetLogicalParent) disabledChild).SetParent(logicalParent); + + var root = CreateInputRoot( + impl.Object, + new Panel + { + Children = { visualParent } + }, + renderer.Object); + + Assert.False(visualParent.IsPointerOver); + SetHit(renderer, disabledChild); + + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, new Point(50, 50))); + Assert.True(visualParent.IsPointerOver); + Assert.False(logicalParent.IsPointerOver); + } + private static void AddEnteredExitedHandlers( EventHandler handler, params IInputElement[] controls) From 3c7c4690181e3e5f79b32c2e5fefde91b801abbd Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 12 Mar 2025 03:01:01 +0100 Subject: [PATCH 22/23] Raise pointer events on captured element (#18420) --- .../Input/PointerOverPreProcessor.cs | 14 ++++++++++++-- .../Input/PointerOverTests.cs | 15 +++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index b989def335..e79e0c3591 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -79,7 +79,9 @@ namespace Avalonia.Input else if (pointerDevice.TryGetPointer(args) is { } pointer && pointer.Type != PointerType.Touch) { - var element = pointer.Captured ?? args.InputHitTestResult.firstEnabledAncestor; + var element = GetEffectivePointerOverElement( + args.InputHitTestResult.firstEnabledAncestor, + pointer.Captured); SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position, new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), @@ -96,7 +98,10 @@ namespace Avalonia.Input if (dirtyRect.Contains(clientPoint)) { - var element = pointer.Captured ?? _inputRoot.InputHitTest(clientPoint); + var element = GetEffectivePointerOverElement( + _inputRoot.InputHitTest(clientPoint), + pointer.Captured); + SetPointerOver(pointer, _inputRoot, element, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); } else if (!((Visual)_inputRoot).Bounds.Contains(clientPoint)) @@ -106,6 +111,11 @@ namespace Avalonia.Input } } + private static IInputElement? GetEffectivePointerOverElement(IInputElement? hitTestElement, IInputElement? captured) + => captured is not null && hitTestElement != captured ? + null : + hitTestElement; + private void ClearPointerOver() { if (_currentPointer is (var pointer, var position)) diff --git a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs index 4f9531cd63..0c1e790bac 100644 --- a/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/PointerOverTests.cs @@ -120,7 +120,7 @@ namespace Avalonia.Base.UnitTests.Input } [Fact] - public void HitTest_Should_Be_Ignored_If_Element_Captured() + public void HitTest_Should_Ignore_Non_Captured_Elements() { using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); @@ -145,8 +145,19 @@ namespace Avalonia.Base.UnitTests.Input } }, renderer.Object); - SetHit(renderer, canvas); pointer.SetupGet(p => p.Captured).Returns(decorator); + + // Move the pointer over the canvas: the captured decorator should lose the pointer over state. + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.False(decorator.IsPointerOver); + Assert.False(border.IsPointerOver); + Assert.False(canvas.IsPointerOver); + Assert.False(root.IsPointerOver); + + // Move back the pointer over the decorator: raise events normally for it since it's captured. + SetHit(renderer, decorator); impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); Assert.True(decorator.IsPointerOver); From 07a76145f4ae55f4a62ac6cbfa5b95762c938c0c Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Wed, 12 Mar 2025 10:31:38 +0100 Subject: [PATCH 23/23] Remove DataGrid from the main repository (#18401) --- .gitmodules | 3 + Avalonia.sln | 14 +- .../AppWithoutLifetime.csproj | 1 - samples/ControlCatalog/App.xaml | 5 +- samples/ControlCatalog/App.xaml.cs | 5 - samples/ControlCatalog/ControlCatalog.csproj | 1 - samples/ControlCatalog/Models/Countries.cs | 256 - samples/ControlCatalog/Models/Country.cs | 41 - .../Models/GDPValueConverter.cs | 37 - .../Models/GDPdLengthConverter.cs | 31 - .../ControlCatalog/Pages/DataGridPage.xaml | 144 +- .../ControlCatalog/Pages/DataGridPage.xaml.cs | 104 +- samples/GpuInterop/GpuInterop.csproj | 1 - samples/Sandbox/Sandbox.csproj | 1 - .../SingleProjectSandbox.csproj | 1 - src/Avalonia.Controls.DataGrid | 1 + .../Peers/DataGridAutomationPeer.cs | 18 - .../Peers/DataGridCellAutomationPeer.cs | 22 - .../DataGridColumnHeaderAutomationPeer.cs | 22 - ...ridColumnHeadersPresenterAutomationPeer.cs | 23 - .../DataGridDetailsPresenterAutomationPeer.cs | 14 - .../Peers/DataGridRowAutomationPeer.cs | 20 - .../Avalonia.Controls.DataGrid.csproj | 22 - .../Collections/DataGridCollectionView.cs | 4380 ------------ .../Collections/DataGridGroupDescription.cs | 1369 ---- .../Collections/DataGridSortDescription.cs | 324 - .../Collections/IDataGridCollectionView.cs | 233 - src/Avalonia.Controls.DataGrid/DataGrid.cs | 6236 ----------------- .../DataGridBoundColumn.cs | 153 - .../DataGridCell.cs | 274 - .../DataGridCellCollection.cs | 71 - .../DataGridCellCoordinates.cs | 57 - .../DataGridCheckBoxColumn.cs | 334 - .../DataGridClipboard.cs | 204 - .../DataGridColumn.cs | 1198 ---- .../DataGridColumnCollection.cs | 581 -- .../DataGridColumnHeader.cs | 878 --- .../DataGridColumns.cs | 1790 ----- .../DataGridDataConnection.cs | 710 -- .../DataGridDisplayData.cs | 364 - .../DataGridEnumerations.cs | 106 - .../DataGridError.cs | 190 - .../DataGridFillerColumn.cs | 70 - .../DataGridLength.cs | 541 -- src/Avalonia.Controls.DataGrid/DataGridRow.cs | 1106 --- .../DataGridRowGroupHeader.cs | 471 -- .../DataGridRowGroupInfo.cs | 57 - .../DataGridRowHeader.cs | 224 - .../DataGridRows.cs | 3045 -------- .../DataGridSelectedItemsCollection.cs | 470 -- .../DataGridTemplateColumn.cs | 147 - .../DataGridTextColumn.cs | 287 - .../DataGridValueConverter.cs | 44 - src/Avalonia.Controls.DataGrid/EventArgs.cs | 569 -- src/Avalonia.Controls.DataGrid/Extensions.cs | 25 - .../IndexToValueTable.cs | 850 --- .../Primitives/DataGridCellsPresenter.cs | 358 - .../DataGridColumnHeadersPresenter.cs | 436 -- .../Primitives/DataGridDetailsPresenter.cs | 141 - .../Primitives/DataGridFrozenGrid.cs | 43 - .../Primitives/DataGridRowsPresenter.cs | 214 - .../Properties/AssemblyInfo.cs | 5 - src/Avalonia.Controls.DataGrid/Range.cs | 69 - .../Themes/Fluent.xaml | 598 -- .../Themes/Simple.xaml | 376 - .../Utils/CellEditBinding.cs | 160 - .../Utils/DataGridHelper.cs | 22 - .../Utils/KeyboardHelper.cs | 32 - .../Utils/ReflectionHelper.cs | 585 -- .../Utils/TreeHelper.cs | 60 - .../Utils/ValidationUtil.cs | 172 - .../Avalonia.Diagnostics.csproj | 16 +- .../Diagnostics/Views/MainWindow.xaml | 2 +- ...valonia.Controls.DataGrid.UnitTests.csproj | 22 - .../Collections/ComparerTests.cs | 226 - .../DataGridSortDescriptionTests.cs | 133 - .../DataGridRowTests.cs | 177 - .../Utils/ReflectionHelperTests.cs | 20 - .../Avalonia.LeakTests.csproj | 1 - tests/Avalonia.LeakTests/ControlTests.cs | 41 - 80 files changed, 36 insertions(+), 32018 deletions(-) delete mode 100644 samples/ControlCatalog/Models/Countries.cs delete mode 100644 samples/ControlCatalog/Models/Country.cs delete mode 100644 samples/ControlCatalog/Models/GDPValueConverter.cs delete mode 100644 samples/ControlCatalog/Models/GDPdLengthConverter.cs create mode 160000 src/Avalonia.Controls.DataGrid delete mode 100644 src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridAutomationPeer.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridCellAutomationPeer.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridColumnHeaderAutomationPeer.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridColumnHeadersPresenterAutomationPeer.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridDetailsPresenterAutomationPeer.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Automation/Peers/DataGridRowAutomationPeer.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj delete mode 100644 src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Collections/DataGridGroupDescription.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Collections/DataGridSortDescription.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Collections/IDataGridCollectionView.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGrid.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridCell.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridCellCollection.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridCellCoordinates.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridCheckBoxColumn.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridClipboard.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridColumn.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridColumns.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridDisplayData.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridEnumerations.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridError.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridFillerColumn.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridLength.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridRow.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridRowGroupInfo.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridRows.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridSelectedItemsCollection.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridTextColumn.cs delete mode 100644 src/Avalonia.Controls.DataGrid/DataGridValueConverter.cs delete mode 100644 src/Avalonia.Controls.DataGrid/EventArgs.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Extensions.cs delete mode 100644 src/Avalonia.Controls.DataGrid/IndexToValueTable.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Primitives/DataGridColumnHeadersPresenter.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Primitives/DataGridDetailsPresenter.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Primitives/DataGridFrozenGrid.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Primitives/DataGridRowsPresenter.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Range.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml delete mode 100644 src/Avalonia.Controls.DataGrid/Themes/Simple.xaml delete mode 100644 src/Avalonia.Controls.DataGrid/Utils/CellEditBinding.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Utils/DataGridHelper.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Utils/KeyboardHelper.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Utils/ReflectionHelper.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Utils/TreeHelper.cs delete mode 100644 src/Avalonia.Controls.DataGrid/Utils/ValidationUtil.cs delete mode 100644 tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj delete mode 100644 tests/Avalonia.Controls.DataGrid.UnitTests/Collections/ComparerTests.cs delete mode 100644 tests/Avalonia.Controls.DataGrid.UnitTests/Collections/DataGridSortDescriptionTests.cs delete mode 100644 tests/Avalonia.Controls.DataGrid.UnitTests/DataGridRowTests.cs delete mode 100644 tests/Avalonia.Controls.DataGrid.UnitTests/Utils/ReflectionHelperTests.cs diff --git a/.gitmodules b/.gitmodules index 2d11fdfa9e..79694800b2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github"] path = src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github url = https://github.com/kekekeks/XamlX.git +[submodule "src/Avalonia.Controls.DataGrid"] + path = src/Avalonia.Controls.DataGrid + url = https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid.git diff --git a/Avalonia.sln b/Avalonia.sln index 1b977839ea..9d60fe2e45 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -170,14 +170,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Avalonia.Dialogs\Avalonia.Dialogs.csproj", "{4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Headless", "src\Headless\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" @@ -301,6 +297,7 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Win32.Automation", "src\Windows\Avalonia.Win32.Automation\Avalonia.Win32.Automation.csproj", "{0097673D-DBCE-476E-82FE-E78A56E58AA2}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XEmbedSample", "samples\XEmbedSample\XEmbedSample.csproj", "{255614F5-CB64-4ECA-A026-E0B1AF6A2EF4}" EndProject Global @@ -497,10 +494,6 @@ Global {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.Build.0 = Debug|Any CPU {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.ActiveCfg = Release|Any CPU {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.Build.0 = Release|Any CPU - {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|Any CPU.Build.0 = Release|Any CPU {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -509,10 +502,6 @@ Global {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}.Release|Any CPU.Build.0 = Release|Any CPU - {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {351337F5-D66F-461B-A957-4EF60BDB4BA6}.Release|Any CPU.Build.0 = Release|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -754,7 +743,6 @@ Global {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} - {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC} = {FF237916-7150-496B-89ED-6CA3292896E7} {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E} = {FF237916-7150-496B-89ED-6CA3292896E7} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} diff --git a/samples/AppWithoutLifetime/AppWithoutLifetime.csproj b/samples/AppWithoutLifetime/AppWithoutLifetime.csproj index 2e959798d0..303daa451f 100644 --- a/samples/AppWithoutLifetime/AppWithoutLifetime.csproj +++ b/samples/AppWithoutLifetime/AppWithoutLifetime.csproj @@ -9,7 +9,6 @@ - diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 157009a5e3..022118d3ab 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -28,11 +28,8 @@ - - + - - diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 2b55c5bad8..a29a23ff61 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -18,7 +18,6 @@ namespace ControlCatalog private FluentTheme? _fluentTheme; private SimpleTheme? _simpleTheme; private IStyle? _colorPickerFluent, _colorPickerSimple; - private IStyle? _dataGridFluent, _dataGridSimple; public App() { @@ -35,8 +34,6 @@ namespace ControlCatalog _simpleTheme = (SimpleTheme)Resources["SimpleTheme"]!; _colorPickerFluent = (IStyle)Resources["ColorPickerFluent"]!; _colorPickerSimple = (IStyle)Resources["ColorPickerSimple"]!; - _dataGridFluent = (IStyle)Resources["DataGridFluent"]!; - _dataGridSimple = (IStyle)Resources["DataGridSimple"]!; SetCatalogThemes(CatalogTheme.Fluent); } @@ -83,13 +80,11 @@ namespace ControlCatalog { app._themeStylesContainer[0] = app._fluentTheme!; app._themeStylesContainer[1] = app._colorPickerFluent!; - app._themeStylesContainer[2] = app._dataGridFluent!; } else if (theme == CatalogTheme.Simple) { app._themeStylesContainer[0] = app._simpleTheme!; app._themeStylesContainer[1] = app._colorPickerSimple!; - app._themeStylesContainer[2] = app._dataGridSimple!; } if (shouldReopenWindow) diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index edcbcbe988..8ef96169df 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -26,7 +26,6 @@ - diff --git a/samples/ControlCatalog/Models/Countries.cs b/samples/ControlCatalog/Models/Countries.cs deleted file mode 100644 index 20b6a85137..0000000000 --- a/samples/ControlCatalog/Models/Countries.cs +++ /dev/null @@ -1,256 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Linq; - -namespace ControlCatalog.Models -{ - public static class Countries - { - static IEnumerable GetCountries() - { - yield return new Country("Afghanistan", "ASIA (EX. NEAR EAST)", 31056997, 647500, 48, 0, 23.06, 163.07, 700, 36, 3.2, 46.6, 20.34); - yield return new Country("Albania", "EASTERN EUROPE", 3581655, 28748, 124.6, 1.26, -4.93, 21.52, 4500, 86.5, 71.2, 15.11, 5.22); - yield return new Country("Algeria", "NORTHERN AFRICA", 32930091, 2381740, 13.8, 0.04, -0.39, 31, 6000, 70, 78.1, 17.14, 4.61); - yield return new Country("American Samoa", "OCEANIA", 57794, 199, 290.4, 58.29, -20.71, 9.27, 8000, 97, 259.5, 22.46, 3.27); - yield return new Country("Andorra", "WESTERN EUROPE", 71201, 468, 152.1, 0, 6.6, 4.05, 19000, 100, 497.2, 8.71, 6.25); - yield return new Country("Angola", "SUB-SAHARAN AFRICA", 12127071, 1246700, 9.7, 0.13, 0, 191.19, 1900, 42, 7.8, 45.11, 24.2); - yield return new Country("Anguilla", "LATIN AMER. & CARIB", 13477, 102, 132.1, 59.8, 10.76, 21.03, 8600, 95, 460, 14.17, 5.34); - yield return new Country("Antigua & Barbuda", "LATIN AMER. & CARIB", 69108, 443, 156, 34.54, -6.15, 19.46, 11000, 89, 549.9, 16.93, 5.37); - yield return new Country("Argentina", "LATIN AMER. & CARIB", 39921833, 2766890, 14.4, 0.18, 0.61, 15.18, 11200, 97.1, 220.4, 16.73, 7.55); - yield return new Country("Armenia", "C.W. OF IND. STATES", 2976372, 29800, 99.9, 0, -6.47, 23.28, 3500, 98.6, 195.7, 12.07, 8.23); - yield return new Country("Aruba", "LATIN AMER. & CARIB", 71891, 193, 372.5, 35.49, 0, 5.89, 28000, 97, 516.1, 11.03, 6.68); - yield return new Country("Australia", "OCEANIA", 20264082, 7686850, 2.6, 0.34, 3.98, 4.69, 29000, 100, 565.5, 12.14, 7.51); - yield return new Country("Austria", "WESTERN EUROPE", 8192880, 83870, 97.7, 0, 2, 4.66, 30000, 98, 452.2, 8.74, 9.76); - yield return new Country("Azerbaijan", "C.W. OF IND. STATES", 7961619, 86600, 91.9, 0, -4.9, 81.74, 3400, 97, 137.1, 20.74, 9.75); - yield return new Country("The Bahamas", "LATIN AMER. & CARIB", 303770, 13940, 21.8, 25.41, -2.2, 25.21, 16700, 95.6, 460.6, 17.57, 9.05); - yield return new Country("Bahrain", "NEAR EAST", 698585, 665, 1050.5, 24.21, 1.05, 17.27, 16900, 89.1, 281.3, 17.8, 4.14); - yield return new Country("Bangladesh", "ASIA (EX. NEAR EAST)", 147365352, 144000, 1023.4, 0.4, -0.71, 62.6, 1900, 43.1, 7.3, 29.8, 8.27); - yield return new Country("Barbados", "LATIN AMER. & CARIB", 279912, 431, 649.5, 22.51, -0.31, 12.5, 15700, 97.4, 481.9, 12.71, 8.67); - yield return new Country("Belarus", "C.W. OF IND. STATES", 10293011, 207600, 49.6, 0, 2.54, 13.37, 6100, 99.6, 319.1, 11.16, 14.02); - yield return new Country("Belgium", "WESTERN EUROPE", 10379067, 30528, 340, 0.22, 1.23, 4.68, 29100, 98, 462.6, 10.38, 10.27); - yield return new Country("Belize", "LATIN AMER. & CARIB", 287730, 22966, 12.5, 1.68, 0, 25.69, 4900, 94.1, 115.7, 28.84, 5.72); - yield return new Country("Benin", "SUB-SAHARAN AFRICA", 7862944, 112620, 69.8, 0.11, 0, 85, 1100, 40.9, 9.7, 38.85, 12.22); - yield return new Country("Bermuda", "NORTHERN AMERICA", 65773, 53, 1241, 194.34, 2.49, 8.53, 36000, 98, 851.4, 11.4, 7.74); - yield return new Country("Bhutan", "ASIA (EX. NEAR EAST)", 2279723, 47000, 48.5, 0, 0, 100.44, 1300, 42.2, 14.3, 33.65, 12.7); - yield return new Country("Bolivia", "LATIN AMER. & CARIB", 8989046, 1098580, 8.2, 0, -1.32, 53.11, 2400, 87.2, 71.9, 23.3, 7.53); - yield return new Country("Bosnia & Herzegovina", "EASTERN EUROPE", 4498976, 51129, 88, 0.04, 0.31, 21.05, 6100,null, 215.4, 8.77, 8.27); - yield return new Country("Botswana", "SUB-SAHARAN AFRICA", 1639833, 600370, 2.7, 0, 0, 54.58, 9000, 79.8, 80.5, 23.08, 29.5); - yield return new Country("Brazil", "LATIN AMER. & CARIB", 188078227, 8511965, 22.1, 0.09, -0.03, 29.61, 7600, 86.4, 225.3, 16.56, 6.17); - yield return new Country("British Virgin Is.", "LATIN AMER. & CARIB", 23098, 153, 151, 52.29, 10.01, 18.05, 16000, 97.8, 506.5, 14.89, 4.42); - yield return new Country("Brunei", "ASIA (EX. NEAR EAST)", 379444, 5770, 65.8, 2.79, 3.59, 12.61, 18600, 93.9, 237.2, 18.79, 3.45); - yield return new Country("Bulgaria", "EASTERN EUROPE", 7385367, 110910, 66.6, 0.32, -4.58, 20.55, 7600, 98.6, 336.3, 9.65, 14.27); - yield return new Country("Burkina Faso", "SUB-SAHARAN AFRICA", 13902972, 274200, 50.7, 0, 0, 97.57, 1100, 26.6, 7, 45.62, 15.6); - yield return new Country("Burma", "ASIA (EX. NEAR EAST)", 47382633, 678500, 69.8, 0.28, -1.8, 67.24, 1800, 85.3, 10.1, 17.91, 9.83); - yield return new Country("Burundi", "SUB-SAHARAN AFRICA", 8090068, 27830, 290.7, 0, -0.06, 69.29, 600, 51.6, 3.4, 42.22, 13.46); - yield return new Country("Cambodia", "ASIA (EX. NEAR EAST)", 13881427, 181040, 76.7, 0.24, 0, 71.48, 1900, 69.4, 2.6, 26.9, 9.06); - yield return new Country("Cameroon", "SUB-SAHARAN AFRICA", 17340702, 475440, 36.5, 0.08, 0, 68.26, 1800, 79, 5.7, 33.89, 13.47); - yield return new Country("Canada", "NORTHERN AMERICA", 33098932, 9984670, 3.3, 2.02, 5.96, 4.75, 29800, 97, 552.2, 10.78, 7.8); - yield return new Country("Cape Verde", "SUB-SAHARAN AFRICA", 420979, 4033, 104.4, 23.93, -12.07, 47.77, 1400, 76.6, 169.6, 24.87, 6.55); - yield return new Country("Cayman Islands", "LATIN AMER. & CARIB", 45436, 262, 173.4, 61.07, 18.75, 8.19, 35000, 98, 836.3, 12.74, 4.89); - yield return new Country("Central African Rep.", "SUB-SAHARAN AFRICA", 4303356, 622984, 6.9, 0, 0, 91, 1100, 51, 2.3, 33.91, 18.65); - yield return new Country("Chad", "SUB-SAHARAN AFRICA", 9944201, 1284000, 7.7, 0, -0.11, 93.82, 1200, 47.5, 1.3, 45.73, 16.38); - yield return new Country("Chile", "LATIN AMER. & CARIB", 16134219, 756950, 21.3, 0.85, 0, 8.8, 9900, 96.2, 213, 15.23, 5.81); - yield return new Country("China", "ASIA (EX. NEAR EAST)", 1313973713, 9596960, 136.9, 0.15, -0.4, 24.18, 5000, 90.9, 266.7, 13.25, 6.97); - yield return new Country("Colombia", "LATIN AMER. & CARIB", 43593035, 1138910, 38.3, 0.28, -0.31, 20.97, 6300, 92.5, 176.2, 20.48, 5.58); - yield return new Country("Comoros", "SUB-SAHARAN AFRICA", 690948, 2170, 318.4, 15.67, 0, 74.93, 700, 56.5, 24.5, 36.93, 8.2); - yield return new Country("Congo, Dem.Rep.", "SUB - SAHARAN AFRICA", 62660551, 2345410, 26.7, 0, 0, 94.69, 700, 65.5, 0.2, 43.69, 13.27); - yield return new Country("Congo, Repub.of the", "SUB - SAHARAN AFRICA", 3702314, 342000, 10.8, 0.05, -0.17, 93.86, 700, 83.8, 3.7, 42.57, 12.93); - yield return new Country("Cook Islands", "OCEANIA", 21388, 240, 89.1, 50,null,null, 5000, 95, 289.9, 21,null); - yield return new Country("Costa Rica", "LATIN AMER. & CARIB", 4075261, 51100, 79.8, 2.52, 0.51, 9.95, 9100, 96, 340.7, 18.32, 4.36); - yield return new Country("Cote d'Ivoire", "SUB-SAHARAN AFRICA", 17654843, 322460, 54.8, 0.16, -0.07, 90.83, 1400, 50.9, 14.6, 35.11, 14.84); - yield return new Country("Croatia", "EASTERN EUROPE", 4494749, 56542, 79.5, 10.32, 1.58, 6.84, 10600, 98.5, 420.4, 9.61, 11.48); - yield return new Country("Cuba", "LATIN AMER. & CARIB", 11382820, 110860, 102.7, 3.37, -1.58, 6.33, 2900, 97, 74.7, 11.89, 7.22); - yield return new Country("Cyprus", "NEAR EAST", 784301, 9250, 84.8, 7.01, 0.43, 7.18, 19200, 97.6,null, 12.56, 7.68); - yield return new Country("Czech Republic", "EASTERN EUROPE", 10235455, 78866, 129.8, 0, 0.97, 3.93, 15700, 99.9, 314.3, 9.02, 10.59); - yield return new Country("Denmark", "WESTERN EUROPE", 5450661, 43094, 126.5, 16.97, 2.48, 4.56, 31100, 100, 614.6, 11.13, 10.36); - yield return new Country("Djibouti", "SUB-SAHARAN AFRICA", 486530, 23000, 21.2, 1.37, 0, 104.13, 1300, 67.9, 22.8, 39.53, 19.31); - yield return new Country("Dominica", "LATIN AMER. & CARIB", 68910, 754, 91.4, 19.63, -13.87, 14.15, 5400, 94, 304.8, 15.27, 6.73); - yield return new Country("Dominican Republic", "LATIN AMER. & CARIB", 9183984, 48730, 188.5, 2.64, -3.22, 32.38, 6000, 84.7, 97.4, 23.22, 5.73); - yield return new Country("East Timor", "ASIA (EX. NEAR EAST)", 1062777, 15007, 70.8, 4.7, 0, 47.41, 500, 58.6,null, 26.99, 6.24); - yield return new Country("Ecuador", "LATIN AMER. & CARIB", 13547510, 283560, 47.8, 0.79, -8.58, 23.66, 3300, 92.5, 125.6, 22.29, 4.23); - yield return new Country("Egypt", "NORTHERN AFRICA", 78887007, 1001450, 78.8, 0.24, -0.22, 32.59, 4000, 57.7, 131.8, 22.94, 5.23); - yield return new Country("El Salvador", "LATIN AMER. & CARIB", 6822378, 21040, 324.3, 1.46, -3.74, 25.1, 4800, 80.2, 142.4, 26.61, 5.78); - yield return new Country("Equatorial Guinea", "SUB-SAHARAN AFRICA", 540109, 28051, 19.3, 1.06, 0, 85.13, 2700, 85.7, 18.5, 35.59, 15.06); - yield return new Country("Eritrea", "SUB-SAHARAN AFRICA", 4786994, 121320, 39.5, 1.84, 0, 74.87, 700, 58.6, 7.9, 34.33, 9.6); - yield return new Country("Estonia", "BALTICS", 1324333, 45226, 29.3, 8.39, -3.16, 7.87, 12300, 99.8, 333.8, 10.04, 13.25); - yield return new Country("Ethiopia", "SUB-SAHARAN AFRICA", 74777981, 1127127, 66.3, 0, 0, 95.32, 700, 42.7, 8.2, 37.98, 14.86); - yield return new Country("Faroe Islands", "WESTERN EUROPE", 47246, 1399, 33.8, 79.84, 1.41, 6.24, 22000,null, 503.8, 14.05, 8.7); - yield return new Country("Fiji", "OCEANIA", 905949, 18270, 49.6, 6.18, -3.14, 12.62, 5800, 93.7, 112.6, 22.55, 5.65); - yield return new Country("Finland", "WESTERN EUROPE", 5231372, 338145, 15.5, 0.37, 0.95, 3.57, 27400, 100, 405.3, 10.45, 9.86); - yield return new Country("France", "WESTERN EUROPE", 60876136, 547030, 111.3, 0.63, 0.66, 4.26, 27600, 99, 586.4, 11.99, 9.14); - yield return new Country("French Guiana", "LATIN AMER. & CARIB", 199509, 91000, 2.2, 0.42, 6.27, 12.07, 8300, 83, 255.6, 20.46, 4.88); - yield return new Country("French Polynesia", "OCEANIA", 274578, 4167, 65.9, 60.6, 2.94, 8.44, 17500, 98, 194.5, 16.68, 4.69); - yield return new Country("Gabon", "SUB-SAHARAN AFRICA", 1424906, 267667, 5.3, 0.33, 0, 53.64, 5500, 63.2, 27.4, 36.16, 12.25); - yield return new Country("Gambia, The", "SUB - SAHARAN AFRICA", 1641564, 11300, 145.3, 0.71, 1.57, 72.02, 1700, 40.1, 26.8, 39.37, 12.25); - yield return new Country("Gaza Strip", "NEAR EAST", 1428757, 360, 3968.8, 11.11, 1.6, 22.93, 600,null, 244.3, 39.45, 3.8); - yield return new Country("Georgia", "C.W. OF IND. STATES", 4661473, 69700, 66.9, 0.44, -4.7, 18.59, 2500, 99, 146.6, 10.41, 9.23); - yield return new Country("Germany", "WESTERN EUROPE", 82422299, 357021, 230.9, 0.67, 2.18, 4.16, 27600, 99, 667.9, 8.25, 10.62); - yield return new Country("Ghana", "SUB-SAHARAN AFRICA", 22409572, 239460, 93.6, 0.23, -0.64, 51.43, 2200, 74.8, 14.4, 30.52, 9.72); - yield return new Country("Gibraltar", "WESTERN EUROPE", 27928, 7, 3989.7, 171.43, 0, 5.13, 17500,null, 877.7, 10.74, 9.31); - yield return new Country("Greece", "WESTERN EUROPE", 10688058, 131940, 81, 10.37, 2.35, 5.53, 20000, 97.5, 589.7, 9.68, 10.24); - yield return new Country("Greenland", "NORTHERN AMERICA", 56361, 2166086, 0, 2.04, -8.37, 15.82, 20000,null, 448.9, 15.93, 7.84); - yield return new Country("Grenada", "LATIN AMER. & CARIB", 89703, 344, 260.8, 35.17, -13.92, 14.62, 5000, 98, 364.5, 22.08, 6.88); - yield return new Country("Guadeloupe", "LATIN AMER. & CARIB", 452776, 1780, 254.4, 17.19, -0.15, 8.6, 8000, 90, 463.8, 15.05, 6.09); - yield return new Country("Guam", "OCEANIA", 171019, 541, 316.1, 23.2, 0, 6.94, 21000, 99, 492, 18.79, 4.48); - yield return new Country("Guatemala", "LATIN AMER. & CARIB", 12293545, 108890, 112.9, 0.37, -1.67, 35.93, 4100, 70.6, 92.1, 29.88, 5.2); - yield return new Country("Guernsey", "WESTERN EUROPE", 65409, 78, 838.6, 64.1, 3.84, 4.71, 20000,null, 842.4, 8.81, 10.01); - yield return new Country("Guinea", "SUB-SAHARAN AFRICA", 9690222, 245857, 39.4, 0.13, -3.06, 90.37, 2100, 35.9, 2.7, 41.76, 15.48); - yield return new Country("Guinea-Bissau", "SUB-SAHARAN AFRICA", 1442029, 36120, 39.9, 0.97, -1.57, 107.17, 800, 42.4, 7.4, 37.22, 16.53); - yield return new Country("Guyana", "LATIN AMER. & CARIB", 767245, 214970, 3.6, 0.21, -2.07, 33.26, 4000, 98.8, 143.5, 18.28, 8.28); - yield return new Country("Haiti", "LATIN AMER. & CARIB", 8308504, 27750, 299.4, 6.38, -3.4, 73.45, 1600, 52.9, 16.9, 36.44, 12.17); - yield return new Country("Honduras", "LATIN AMER. & CARIB", 7326496, 112090, 65.4, 0.73, -1.99, 29.32, 2600, 76.2, 67.5, 28.24, 5.28); - yield return new Country("Hong Kong", "ASIA (EX. NEAR EAST)", 6940432, 1092, 6355.7, 67.12, 5.24, 2.97, 28800, 93.5, 546.7, 7.29, 6.29); - yield return new Country("Hungary", "EASTERN EUROPE", 9981334, 93030, 107.3, 0, 0.86, 8.57, 13900, 99.4, 336.2, 9.72, 13.11); - yield return new Country("Iceland", "WESTERN EUROPE", 299388, 103000, 2.9, 4.83, 2.38, 3.31, 30900, 99.9, 647.7, 13.64, 6.72); - yield return new Country("India", "ASIA (EX. NEAR EAST)", 1095351995, 3287590, 333.2, 0.21, -0.07, 56.29, 2900, 59.5, 45.4, 22.01, 8.18); - yield return new Country("Indonesia", "ASIA (EX. NEAR EAST)", 245452739, 1919440, 127.9, 2.85, 0, 35.6, 3200, 87.9, 52, 20.34, 6.25); - yield return new Country("Iran", "ASIA (EX. NEAR EAST)", 68688433, 1648000, 41.7, 0.15, -0.84, 41.58, 7000, 79.4, 276.4, 17, 5.55); - yield return new Country("Iraq", "NEAR EAST", 26783383, 437072, 61.3, 0.01, 0, 50.25, 1500, 40.4, 38.6, 31.98, 5.37); - yield return new Country("Ireland", "WESTERN EUROPE", 4062235, 70280, 57.8, 2.06, 4.99, 5.39, 29600, 98, 500.5, 14.45, 7.82); - yield return new Country("Isle of Man", "WESTERN EUROPE", 75441, 572, 131.9, 27.97, 5.36, 5.93, 21000,null, 676, 11.05, 11.19); - yield return new Country("Israel", "NEAR EAST", 6352117, 20770, 305.8, 1.31, 0.68, 7.03, 19800, 95.4, 462.3, 17.97, 6.18); - yield return new Country("Italy", "WESTERN EUROPE", 58133509, 301230, 193, 2.52, 2.07, 5.94, 26700, 98.6, 430.9, 8.72, 10.4); - yield return new Country("Jamaica", "LATIN AMER. & CARIB", 2758124, 10991, 250.9, 9.3, -4.92, 12.36, 3900, 87.9, 124, 20.82, 6.52); - yield return new Country("Japan", "ASIA (EX. NEAR EAST)", 127463611, 377835, 337.4, 7.87, 0, 3.26, 28200, 99, 461.2, 9.37, 9.16); - yield return new Country("Jersey", "WESTERN EUROPE", 91084, 116, 785.2, 60.34, 2.76, 5.24, 24800,null, 811.3, 9.3, 9.28); - yield return new Country("Jordan", "NEAR EAST", 5906760, 92300, 64, 0.03, 6.59, 17.35, 4300, 91.3, 104.5, 21.25, 2.65); - yield return new Country("Kazakhstan", "C.W. OF IND. STATES", 15233244, 2717300, 5.6, 0, -3.35, 29.21, 6300, 98.4, 164.1, 16, 9.42); - yield return new Country("Kenya", "SUB-SAHARAN AFRICA", 34707817, 582650, 59.6, 0.09, -0.1, 61.47, 1000, 85.1, 8.1, 39.72, 14.02); - yield return new Country("Kiribati", "OCEANIA", 105432, 811, 130, 140.94, 0, 48.52, 800,null, 42.7, 30.65, 8.26); - yield return new Country("North Korea", "ASIA(EX.NEAR EAST)", 23113019, 120540, 191.8, 2.07, 0, 24.04, 1300, 99, 42.4, 15.54, 7.13); - yield return new Country("South Korea", "ASIA(EX.NEAR EAST)", 48846823, 98480, 496, 2.45, 0, 7.05, 17800, 97.9, 486.1, 10, 5.85); - yield return new Country("Kuwait", "NEAR EAST", 2418393, 17820, 135.7, 2.8, 14.18, 9.95, 19000, 83.5, 211, 21.94, 2.41); - yield return new Country("Kyrgyzstan", "C.W. OF IND. STATES", 5213898, 198500, 26.3, 0, -2.45, 35.64, 1600, 97, 84, 22.8, 7.08); - yield return new Country("Laos", "ASIA (EX. NEAR EAST)", 6368481, 236800, 26.9, 0, 0, 85.22, 1700, 66.4, 14.1, 35.49, 11.55); - yield return new Country("Latvia", "BALTICS", 2274735, 64589, 35.2, 0.82, -2.23, 9.55, 10200, 99.8, 321.4, 9.24, 13.66); - yield return new Country("Lebanon", "NEAR EAST", 3874050, 10400, 372.5, 2.16, 0, 24.52, 4800, 87.4, 255.6, 18.52, 6.21); - yield return new Country("Lesotho", "SUB-SAHARAN AFRICA", 2022331, 30355, 66.6, 0, -0.74, 84.23, 3000, 84.8, 23.7, 24.75, 28.71); - yield return new Country("Liberia", "SUB-SAHARAN AFRICA", 3042004, 111370, 27.3, 0.52, 0, 128.87, 1000, 57.5, 2.3, 44.77, 23.1); - yield return new Country("Libya", "NORTHERN AFRICA", 5900754, 1759540, 3.4, 0.1, 0, 24.6, 6400, 82.6, 127.1, 26.49, 3.48); - yield return new Country("Liechtenstein", "WESTERN EUROPE", 33987, 160, 212.4, 0, 4.85, 4.7, 25000, 100, 585.5, 10.21, 7.18); - yield return new Country("Lithuania", "BALTICS", 3585906, 65200, 55, 0.14, -0.71, 6.89, 11400, 99.6, 223.4, 8.75, 10.98); - yield return new Country("Luxembourg", "WESTERN EUROPE", 474413, 2586, 183.5, 0, 8.97, 4.81, 55100, 100, 515.4, 11.94, 8.41); - yield return new Country("Macau", "ASIA (EX. NEAR EAST)", 453125, 28, 16183, 146.43, 4.86, 4.39, 19400, 94.5, 384.9, 8.48, 4.47); - yield return new Country("Macedonia", "EASTERN EUROPE", 2050554, 25333, 80.9, 0, -1.45, 10.09, 6700,null, 260, 12.02, 8.77); - yield return new Country("Madagascar", "SUB-SAHARAN AFRICA", 18595469, 587040, 31.7, 0.82, 0, 76.83, 800, 68.9, 3.6, 41.41, 11.11); - yield return new Country("Malawi", "SUB-SAHARAN AFRICA", 13013926, 118480, 109.8, 0, 0, 103.32, 600, 62.7, 7.9, 43.13, 19.33); - yield return new Country("Malaysia", "ASIA (EX. NEAR EAST)", 24385858, 329750, 74, 1.42, 0, 17.7, 9000, 88.7, 179, 22.86, 5.05); - yield return new Country("Maldives", "ASIA (EX. NEAR EAST)", 359008, 300, 1196.7, 214.67, 0, 56.52, 3900, 97.2, 90, 34.81, 7.06); - yield return new Country("Mali", "SUB-SAHARAN AFRICA", 11716829, 1240000, 9.5, 0, -0.33, 116.79, 900, 46.4, 6.4, 49.82, 16.89); - yield return new Country("Malta", "WESTERN EUROPE", 400214, 316, 1266.5, 62.28, 2.07, 3.89, 17700, 92.8, 505, 10.22, 8.1); - yield return new Country("Marshall Islands", "OCEANIA", 60422, 11854, 5.1, 3.12, -6.04, 29.45, 1600, 93.7, 91.2, 33.05, 4.78); - yield return new Country("Martinique", "LATIN AMER. & CARIB", 436131, 1100, 396.5, 31.82, -0.05, 7.09, 14400, 97.7, 394.4, 13.74, 6.48); - yield return new Country("Mauritania", "SUB-SAHARAN AFRICA", 3177388, 1030700, 3.1, 0.07, 0, 70.89, 1800, 41.7, 12.9, 40.99, 12.16); - yield return new Country("Mauritius", "SUB-SAHARAN AFRICA", 1240827, 2040, 608.3, 8.68, -0.9, 15.03, 11400, 85.6, 289.3, 15.43, 6.86); - yield return new Country("Mayotte", "SUB-SAHARAN AFRICA", 201234, 374, 538.1, 49.52, 6.78, 62.4, 2600,null, 49.7, 40.95, 7.7); - yield return new Country("Mexico", "LATIN AMER. & CARIB", 107449525, 1972550, 54.5, 0.47, -4.87, 20.91, 9000, 92.2, 181.6, 20.69, 4.74); - yield return new Country("Micronesia, Fed.St.", "OCEANIA", 108004, 702, 153.9, 870.66, -20.99, 30.21, 2000, 89, 114.8, 24.68, 4.75); - yield return new Country("Moldova", "C.W. OF IND. STATES", 4466706, 33843, 132, 0, -0.26, 40.42, 1800, 99.1, 208.1, 15.7, 12.64); - yield return new Country("Monaco", "WESTERN EUROPE", 32543, 2, 16271.5, 205, 7.75, 5.43, 27000, 99, 1035.6, 9.19, 12.91); - yield return new Country("Mongolia", "ASIA (EX. NEAR EAST)", 2832224, 1564116, 1.8, 0, 0, 53.79, 1800, 97.8, 55.1, 21.59, 6.95); - yield return new Country("Montserrat", "LATIN AMER. & CARIB", 9439, 102, 92.5, 39.22, 0, 7.35, 3400, 97,null, 17.59, 7.1); - yield return new Country("Morocco", "NORTHERN AFRICA", 33241259, 446550, 74.4, 0.41, -0.98, 41.62, 4000, 51.7, 40.4, 21.98, 5.58); - yield return new Country("Mozambique", "SUB-SAHARAN AFRICA", 19686505, 801590, 24.6, 0.31, 0, 130.79, 1200, 47.8, 3.5, 35.18, 21.35); - yield return new Country("Namibia", "SUB-SAHARAN AFRICA", 2044147, 825418, 2.5, 0.19, 0, 48.98, 7200, 84, 62.6, 24.32, 18.86); - yield return new Country("Nauru", "OCEANIA", 13287, 21, 632.7, 142.86, 0, 9.95, 5000,null, 143, 24.76, 6.7); - yield return new Country("Nepal", "ASIA (EX. NEAR EAST)", 28287147, 147181, 192.2, 0, 0, 66.98, 1400, 45.2, 15.9, 30.98, 9.31); - yield return new Country("Netherlands", "WESTERN EUROPE", 16491461, 41526, 397.1, 1.09, 2.91, 5.04, 28600, 99, 460.8, 10.9, 8.68); - yield return new Country("Netherlands Antilles", "LATIN AMER. & CARIB", 221736, 960, 231, 37.92, -0.41, 10.03, 11400, 96.7, 365.3, 14.78, 6.45); - yield return new Country("New Caledonia", "OCEANIA", 219246, 19060, 11.5, 11.83, 0, 7.72, 15000, 91, 252.2, 18.11, 5.69); - yield return new Country("New Zealand", "OCEANIA", 4076140, 268680, 15.2, 5.63, 4.05, 5.85, 21600, 99, 441.7, 13.76, 7.53); - yield return new Country("Nicaragua", "LATIN AMER. & CARIB", 5570129, 129494, 43, 0.7, -1.22, 29.11, 2300, 67.5, 39.7, 24.51, 4.45); - yield return new Country("Niger", "SUB-SAHARAN AFRICA", 12525094, 1267000, 9.9, 0, -0.67, 121.69, 800, 17.6, 1.9, 50.73, 20.91); - yield return new Country("Nigeria", "SUB-SAHARAN AFRICA", 131859731, 923768, 142.7, 0.09, 0.26, 98.8, 900, 68, 9.3, 40.43, 16.94); - yield return new Country("N. Mariana Islands", "OCEANIA", 82459, 477, 172.9, 310.69, 9.61, 7.11, 12500, 97, 254.7, 19.43, 2.29); - yield return new Country("Norway", "WESTERN EUROPE", 4610820, 323802, 14.2, 7.77, 1.74, 3.7, 37800, 100, 461.7, 11.46, 9.4); - yield return new Country("Oman", "NEAR EAST", 3102229, 212460, 14.6, 0.98, 0.28, 19.51, 13100, 75.8, 85.5, 36.24, 3.81); - yield return new Country("Pakistan", "ASIA (EX. NEAR EAST)", 165803560, 803940, 206.2, 0.13, -2.77, 72.44, 2100, 45.7, 31.8, 29.74, 8.23); - yield return new Country("Palau", "OCEANIA", 20579, 458, 44.9, 331.66, 2.85, 14.84, 9000, 92, 325.6, 18.03, 6.8); - yield return new Country("Panama", "LATIN AMER. & CARIB", 3191319, 78200, 40.8, 3.18, -0.91, 20.47, 6300, 92.6, 137.9, 21.74, 5.36); - yield return new Country("Papua New Guinea", "OCEANIA", 5670544, 462840, 12.3, 1.11, 0, 51.45, 2200, 64.6, 10.9, 29.36, 7.25); - yield return new Country("Paraguay", "LATIN AMER. & CARIB", 6506464, 406750, 16, 0, -0.08, 25.63, 4700, 94, 49.2, 29.1, 4.49); - yield return new Country("Peru", "LATIN AMER. & CARIB", 28302603, 1285220, 22, 0.19, -1.05, 31.94, 5100, 90.9, 79.5, 20.48, 6.23); - yield return new Country("Philippines", "ASIA (EX. NEAR EAST)", 89468677, 300000, 298.2, 12.1, -1.5, 23.51, 4600, 92.6, 38.4, 24.89, 5.41); - yield return new Country("Poland", "EASTERN EUROPE", 38536869, 312685, 123.3, 0.16, -0.49, 8.51, 11100, 99.8, 306.3, 9.85, 9.89); - yield return new Country("Portugal", "WESTERN EUROPE", 10605870, 92391, 114.8, 1.94, 3.57, 5.05, 18000, 93.3, 399.2, 10.72, 10.5); - yield return new Country("Puerto Rico", "LATIN AMER. & CARIB", 3927188, 13790, 284.8, 3.63, -1.46, 8.24, 16800, 94.1, 283.1, 12.77, 7.65); - yield return new Country("Qatar", "NEAR EAST", 885359, 11437, 77.4, 4.92, 16.29, 18.61, 21500, 82.5, 232, 15.56, 4.72); - yield return new Country("Reunion", "SUB-SAHARAN AFRICA", 787584, 2517, 312.9, 8.22, 0, 7.78, 5800, 88.9, 380.9, 18.9, 5.49); - yield return new Country("Romania", "EASTERN EUROPE", 22303552, 237500, 93.9, 0.09, -0.13, 26.43, 7000, 98.4, 196.9, 10.7, 11.77); - yield return new Country("Russia", "C.W. OF IND. STATES", 142893540, 17075200, 8.4, 0.22, 1.02, 15.39, 8900, 99.6, 280.6, 9.95, 14.65); - yield return new Country("Rwanda", "SUB-SAHARAN AFRICA", 8648248, 26338, 328.4, 0, 0, 91.23, 1300, 70.4, 2.7, 40.37, 16.09); - yield return new Country("Saint Helena", "SUB-SAHARAN AFRICA", 7502, 413, 18.2, 14.53, 0, 19, 2500, 97, 293.3, 12.13, 6.53); - yield return new Country("Saint Kitts & Nevis", "LATIN AMER. & CARIB", 39129, 261, 149.9, 51.72, -7.11, 14.49, 8800, 97, 638.9, 18.02, 8.33); - yield return new Country("Saint Lucia", "LATIN AMER. & CARIB", 168458, 616, 273.5, 25.65, -2.67, 13.53, 5400, 67, 303.3, 19.68, 5.08); - yield return new Country("St Pierre & Miquelon", "NORTHERN AMERICA", 7026, 242, 29, 49.59, -4.86, 7.54, 6900, 99, 683.2, 13.52, 6.83); - yield return new Country("Saint Vincent and the Grenadines", "LATIN AMER. & CARIB", 117848, 389, 303, 21.59, -7.64, 14.78, 2900, 96, 190.9, 16.18, 5.98); - yield return new Country("Samoa", "OCEANIA", 176908, 2944, 60.1, 13.69, -11.7, 27.71, 5600, 99.7, 75.2, 16.43, 6.62); - yield return new Country("San Marino", "WESTERN EUROPE", 29251, 61, 479.5, 0, 10.98, 5.73, 34600, 96, 704.3, 10.02, 8.17); - yield return new Country("Sao Tome & Principe", "SUB-SAHARAN AFRICA", 193413, 1001, 193.2, 20.88, -2.72, 43.11, 1200, 79.3, 36.2, 40.25, 6.47); - yield return new Country("Saudi Arabia", "NEAR EAST", 27019731, 1960582, 13.8, 0.13, -2.71, 13.24, 11800, 78.8, 140.6, 29.34, 2.58); - yield return new Country("Senegal", "SUB-SAHARAN AFRICA", 11987121, 196190, 61.1, 0.27, 0.2, 55.51, 1600, 40.2, 22.2, 32.78, 9.42); - yield return new Country("Serbia", "EASTERN EUROPE", 9396411, 88361, 106.3, 0, -1.33, 12.89, 2200, 93, 285.8,null,null); - yield return new Country("Seychelles", "SUB-SAHARAN AFRICA", 81541, 455, 179.2, 107.91, -5.69, 15.53, 7800, 58, 262.4, 16.03, 6.29); - yield return new Country("Sierra Leone", "SUB-SAHARAN AFRICA", 6005250, 71740, 83.7, 0.56, 0, 143.64, 500, 31.4, 4, 45.76, 23.03); - yield return new Country("Singapore", "ASIA (EX. NEAR EAST)", 4492150, 693, 6482.2, 27.85, 11.53, 2.29, 23700, 92.5, 411.4, 9.34, 4.28); - yield return new Country("Slovakia", "EASTERN EUROPE", 5439448, 48845, 111.4, 0, 0.3, 7.41, 13300,null, 220.1, 10.65, 9.45); - yield return new Country("Slovenia", "EASTERN EUROPE", 2010347, 20273, 99.2, 0.23, 1.12, 4.45, 19000, 99.7, 406.1, 8.98, 10.31); - yield return new Country("Solomon Islands", "OCEANIA", 552438, 28450, 19.4, 18.67, 0, 21.29, 1700,null, 13.4, 30.01, 3.92); - yield return new Country("Somalia", "SUB-SAHARAN AFRICA", 8863338, 637657, 13.9, 0.47, 5.37, 116.7, 500, 37.8, 11.3, 45.13, 16.63); - yield return new Country("South Africa", "SUB-SAHARAN AFRICA", 44187637, 1219912, 36.2, 0.23, -0.29, 61.81, 10700, 86.4, 107, 18.2, 22); - yield return new Country("Spain", "WESTERN EUROPE", 40397842, 504782, 80, 0.98, 0.99, 4.42, 22000, 97.9, 453.5, 10.06, 9.72); - yield return new Country("Sri Lanka", "ASIA (EX. NEAR EAST)", 20222240, 65610, 308.2, 2.04, -1.31, 14.35, 3700, 92.3, 61.5, 15.51, 6.52); - yield return new Country("Sudan", "SUB-SAHARAN AFRICA", 41236378, 2505810, 16.5, 0.03, -0.02, 62.5, 1900, 61.1, 16.3, 34.53, 8.97); - yield return new Country("Suriname", "LATIN AMER. & CARIB", 439117, 163270, 2.7, 0.24, -8.81, 23.57, 4000, 93, 184.7, 18.02, 7.27); - yield return new Country("Swaziland", "SUB-SAHARAN AFRICA", 1136334, 17363, 65.5, 0, 0, 69.27, 4900, 81.6, 30.8, 27.41, 29.74); - yield return new Country("Sweden", "WESTERN EUROPE", 9016596, 449964, 20, 0.72, 1.67, 2.77, 26800, 99, 715, 10.27, 10.31); - yield return new Country("Switzerland", "WESTERN EUROPE", 7523934, 41290, 182.2, 0, 4.05, 4.39, 32700, 99, 680.9, 9.71, 8.49); - yield return new Country("Syria", "NEAR EAST", 18881361, 185180, 102, 0.1, 0, 29.53, 3300, 76.9, 153.8, 27.76, 4.81); - yield return new Country("Taiwan", "ASIA (EX. NEAR EAST)", 23036087, 35980, 640.3, 4.35, 0, 6.4, 23400, 96.1, 591, 12.56, 6.48); - yield return new Country("Tajikistan", "C.W. OF IND. STATES", 7320815, 143100, 51.2, 0, -2.86, 110.76, 1000, 99.4, 33.5, 32.65, 8.25); - yield return new Country("Tanzania", "SUB-SAHARAN AFRICA", 37445392, 945087, 39.6, 0.15, -2.06, 98.54, 600, 78.2, 4, 37.71, 16.39); - yield return new Country("Thailand", "ASIA (EX. NEAR EAST)", 64631595, 514000, 125.7, 0.63, 0, 20.48, 7400, 92.6, 108.9, 13.87, 7.04); - yield return new Country("Togo", "SUB-SAHARAN AFRICA", 5548702, 56785, 97.7, 0.1, 0, 66.61, 1500, 60.9, 10.6, 37.01, 9.83); - yield return new Country("Tonga", "OCEANIA", 114689, 748, 153.3, 56.02, 0, 12.62, 2200, 98.5, 97.7, 25.37, 5.28); - yield return new Country("Trinidad & Tobago", "LATIN AMER. & CARIB", 1065842, 5128, 207.9, 7.06, -10.83, 24.31, 9500, 98.6, 303.5, 12.9, 10.57); - yield return new Country("Tunisia", "NORTHERN AFRICA", 10175014, 163610, 62.2, 0.7, -0.57, 24.77, 6900, 74.2, 123.6, 15.52, 5.13); - yield return new Country("Turkey", "NEAR EAST", 70413958, 780580, 90.2, 0.92, 0, 41.04, 6700, 86.5, 269.5, 16.62, 5.97); - yield return new Country("Turkmenistan", "C.W. OF IND. STATES", 5042920, 488100, 10.3, 0, -0.86, 73.08, 5800, 98, 74.6, 27.61, 8.6); - yield return new Country("Turks & Caicos Is", "LATIN AMER. & CARIB", 21152, 430, 49.2, 90.47, 11.68, 15.67, 9600, 98, 269.5, 21.84, 4.21); - yield return new Country("Tuvalu", "OCEANIA", 11810, 26, 454.2, 92.31, 0, 20.03, 1100,null, 59.3, 22.18, 7.11); - yield return new Country("Uganda", "SUB-SAHARAN AFRICA", 28195754, 236040, 119.5, 0, 0, 67.83, 1400, 69.9, 3.6, 47.35, 12.24); - yield return new Country("Ukraine", "C.W. OF IND. STATES", 46710816, 603700, 77.4, 0.46, -0.39, 20.34, 5400, 99.7, 259.9, 8.82, 14.39); - yield return new Country("United Arab Emirates", "NEAR EAST", 2602713, 82880, 31.4, 1.59, 1.03, 14.51, 23200, 77.9, 475.3, 18.96, 4.4); - yield return new Country("United Kingdom", "WESTERN EUROPE", 60609153, 244820, 247.6, 5.08, 2.19, 5.16, 27700, 99, 543.5, 10.71, 10.13); - yield return new Country("United States", "NORTHERN AMERICA", 298444215, 9631420, 31, 0.21, 3.41, 6.5, 37800, 97, 898, 14.14, 8.26); - yield return new Country("Uruguay", "LATIN AMER. & CARIB", 3431932, 176220, 19.5, 0.37, -0.32, 11.95, 12800, 98, 291.4, 13.91, 9.05); - yield return new Country("Uzbekistan", "C.W. OF IND. STATES", 27307134, 447400, 61, 0, -1.72, 71.1, 1700, 99.3, 62.9, 26.36, 7.84); - yield return new Country("Vanuatu", "OCEANIA", 208869, 12200, 17.1, 20.72, 0, 55.16, 2900, 53, 32.6, 22.72, 7.82); - yield return new Country("Venezuela", "LATIN AMER. & CARIB", 25730435, 912050, 28.2, 0.31, -0.04, 22.2, 4800, 93.4, 140.1, 18.71, 4.92); - yield return new Country("Vietnam", "ASIA (EX. NEAR EAST)", 84402966, 329560, 256.1, 1.05, -0.45, 25.95, 2500, 90.3, 187.7, 16.86, 6.22); - yield return new Country("Virgin Islands", "LATIN AMER. & CARIB", 108605, 1910, 56.9, 9.84, -8.94, 8.03, 17200,null, 652.8, 13.96, 6.43); - yield return new Country("Wallis and Futuna", "OCEANIA", 16025, 274, 58.5, 47.08,null,null, 3700, 50, 118.6,null,null); - yield return new Country("West Bank", "NEAR EAST", 2460492, 5860, 419.9, 0, 2.98, 19.62, 800,null, 145.2, 31.67, 3.92); - yield return new Country("Yemen", "NEAR EAST", 21456188, 527970, 40.6, 0.36, 0, 61.5, 800, 50.2, 37.2, 42.89, 8.3); - yield return new Country("Zambia", "SUB-SAHARAN AFRICA", 11502010, 752614, 15.3, 0, 0, 88.29, 800, 80.6, 8.2, 41, 19.93); - yield return new Country("Zimbabwe", "SUB-SAHARAN AFRICA", 12236805, 390580, 31.3, 0, 0, 67.69, 1900, 90.7, 26.8, 28.01, 21.84); - } - - static IReadOnlyList? _all; - - public static IReadOnlyList All - { - get - { - if(_all == null) - { - _all = GetCountries().ToList().AsReadOnly(); - } - - return _all; - } - - } - } -} diff --git a/samples/ControlCatalog/Models/Country.cs b/samples/ControlCatalog/Models/Country.cs deleted file mode 100644 index 072383a31c..0000000000 --- a/samples/ControlCatalog/Models/Country.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace ControlCatalog.Models -{ - public class Country - { - public string Name { get; private set; } - public string Region { get; private set; } - public int Population { get; private set; } - //Square Miles - public int Area { get; private set; } - //Per Square Mile - public double PopulationDensity { get; private set; } - //Coast / Area - public double CoastLine { get; private set; } - public double? NetMigration { get; private set; } - //per 1000 births - public double? InfantMortality { get; private set; } - public int GDP { get; private set; } - public double? LiteracyPercent { get; private set; } - //per 1000 - public double? Phones { get; private set; } - public double? BirthRate { get; private set; } - public double? DeathRate { get; private set; } - - public Country(string name, string region, int population, int area, double density, double coast, double? migration, - double? infantMorality, int gdp, double? literacy, double? phones, double? birth, double? death) - { - Name = name; - Region = region; - Population = population; - Area = area; - PopulationDensity = density; - CoastLine = coast; - NetMigration = migration; - InfantMortality = infantMorality; - GDP = gdp; - LiteracyPercent = literacy; - BirthRate = birth; - DeathRate = death; - } - } -} diff --git a/samples/ControlCatalog/Models/GDPValueConverter.cs b/samples/ControlCatalog/Models/GDPValueConverter.cs deleted file mode 100644 index cad1574e7a..0000000000 --- a/samples/ControlCatalog/Models/GDPValueConverter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.Linq; -using Avalonia.Data.Converters; -using Avalonia.Media; - -namespace ControlCatalog.Models -{ - public class GDPValueConverter : IValueConverter - { - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is int gdp) - { - if (gdp <= 5000) - return new SolidColorBrush(Colors.Orange, 0.6); - else if (gdp <= 10000) - return new SolidColorBrush(Colors.Yellow, 0.6); - else - return new SolidColorBrush(Colors.LightGreen, 0.6); - } - - return value; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } - } -} diff --git a/samples/ControlCatalog/Models/GDPdLengthConverter.cs b/samples/ControlCatalog/Models/GDPdLengthConverter.cs deleted file mode 100644 index 034e664305..0000000000 --- a/samples/ControlCatalog/Models/GDPdLengthConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace ControlCatalog.Models; - -internal class GDPdLengthConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is double d) - { - return new Avalonia.Controls.DataGridLength(d,Avalonia.Controls.DataGridLengthUnitType.Pixel,d,d); - } - else if (value is decimal d2) - { - var dv =System.Convert.ToDouble(d2); - return new Avalonia.Controls.DataGridLength(dv, Avalonia.Controls.DataGridLengthUnitType.Pixel, dv, dv); - } - return value; - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is Avalonia.Controls.DataGridLength width) - { - return System.Convert.ToDecimal(width.DisplayValue); - } - return value; - } -} diff --git a/samples/ControlCatalog/Pages/DataGridPage.xaml b/samples/ControlCatalog/Pages/DataGridPage.xaml index a968b50666..3187e13f9f 100644 --- a/samples/ControlCatalog/Pages/DataGridPage.xaml +++ b/samples/ControlCatalog/Pages/DataGridPage.xaml @@ -1,141 +1,11 @@ - + x:Class="ControlCatalog.Pages.DataGridPage"> + + + + + - - - - - - - - - - - - - - - A control for displaying and interacting with a data source. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -