From 1162e02b06923fc148827740e41d389cdd9ac48f Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Sun, 15 Nov 2015 20:01:13 +0200 Subject: [PATCH] Implement "space between" and "space around" options of stack layout justify content --- AsyncDisplayKit/Layout/ASStackLayoutDefines.h | 15 +++++ .../Private/ASStackPositionedLayout.mm | 60 ++++++++++++++++-- .../ASStackLayoutSpecSnapshotTests.mm | 50 +++++++++++++++ ...estJustifiedSpaceAroundWithOneChild@2x.png | Bin 0 -> 1933 bytes ...tifiedSpaceAroundWithRemainingSpace@2x.png | Bin 0 -> 3436 bytes ...stJustifiedSpaceBetweenWithOneChild@2x.png | Bin 0 -> 1563 bytes ...ifiedSpaceBetweenWithRemainingSpace@2x.png | Bin 0 -> 2698 bytes ...derflowBehaviors_justifySpaceAround@2x.png | Bin 0 -> 3339 bytes ...erflowBehaviors_justifySpaceBetween@2x.png | Bin 0 -> 2658 bytes 9 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithOneChild@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithOneChild@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceAround@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceBetween@2x.png diff --git a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h index 0ca7bb3c49..61329398e3 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h @@ -33,6 +33,21 @@ typedef NS_ENUM(NSUInteger, ASStackLayoutJustifyContent) { On underflow, children are right/bottom-aligned within this spec's bounds. */ ASStackLayoutJustifyContentEnd, + /** + On overflow or if the stack has only 1 child, this value is identical to ASStackLayoutJustifyContentStart. + Otherwise, the starting edge of the first child is at the starting edge of the stack, + the ending edge of the last child is at the ending edge of the stack, and the remaining children + are distributed so that the spacing between any two adjacent ones is the same. + If there is a remaining space after spacing division, it is combined with the last spacing (i.e the one between the last 2 children). + */ + ASStackLayoutJustifyContentSpaceBetween, + /** + On overflow or if the stack has only 1 child, this value is identical to ASStackLayoutJustifyContentCenter. + Otherwise, children are distributed such that the spacing between any two adjacent ones is the same, + and the spacing between the first/last child and the stack edges is half the size of the spacing between children. + If there is a remaining space after spacing division, it is combined with the last spacing (i.e the one between the last child and the stack ending edge). + */ + ASStackLayoutJustifyContentSpaceAround }; /** Orientation of children along cross axis */ diff --git a/AsyncDisplayKit/Private/ASStackPositionedLayout.mm b/AsyncDisplayKit/Private/ASStackPositionedLayout.mm index 4d23cf6786..428319d317 100644 --- a/AsyncDisplayKit/Private/ASStackPositionedLayout.mm +++ b/AsyncDisplayKit/Private/ASStackPositionedLayout.mm @@ -15,6 +15,7 @@ #import "ASStackLayoutSpecUtilities.h" #import "ASLayoutable.h" #import "ASLayoutOptions.h" +#import "ASAssert.h" static CGFloat crossOffset(const ASStackLayoutSpecStyle &style, const ASStackUnpositionedItem &l, @@ -33,8 +34,20 @@ static CGFloat crossOffset(const ASStackLayoutSpecStyle &style, } } +/** + * Positions children according to the stack style and positioning properties. + * + * @param style The layout style of the overall stack layout + * @param firstChildOffset Offset of the first child + * @param extraSpacing Spacing between children, in addition to spacing set to the stack's layout style + * @param lastChildOffset Offset of the last child + * @param unpositionedLayout Unpositioned children of the stack + * @param constrainedSize Constrained size of the stack + */ static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style, - const CGFloat offset, + const CGFloat firstChildOffset, + const CGFloat extraSpacing, + const CGFloat lastChildOffset, const ASStackUnpositionedLayout &unpositionedLayout, const ASSizeRange &constrainedSize) { @@ -48,12 +61,16 @@ static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style const auto maxCrossSize = crossDimension(style.direction, constrainedSize.max); const CGFloat crossSize = MIN(MAX(minCrossSize, largestChildCrossSize), maxCrossSize); - CGPoint p = directionPoint(style.direction, offset, 0); + CGPoint p = directionPoint(style.direction, firstChildOffset, 0); BOOL first = YES; + const auto lastChild = unpositionedLayout.items.back().child; + CGFloat offset = 0; + auto stackedChildren = AS::map(unpositionedLayout.items, [&](const ASStackUnpositionedItem &l) -> ASLayout *{ - p = p + directionPoint(style.direction, l.child.spacingBefore, 0); + offset = (l.child == lastChild) ? lastChildOffset : 0; + p = p + directionPoint(style.direction, l.child.spacingBefore + offset, 0); if (!first) { - p = p + directionPoint(style.direction, style.spacing, 0); + p = p + directionPoint(style.direction, style.spacing + extraSpacing, 0); } first = NO; l.layout.position = p + directionPoint(style.direction, 0, crossOffset(style, l, crossSize)); @@ -64,16 +81,45 @@ static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style return {stackedChildren, crossSize}; } +static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style, + const CGFloat firstChildOffset, + const ASStackUnpositionedLayout &unpositionedLayout, + const ASSizeRange &constrainedSize) +{ + return stackedLayout(style, firstChildOffset, 0, 0, unpositionedLayout, constrainedSize); +} + ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnpositionedLayout &unpositionedLayout, const ASStackLayoutSpecStyle &style, const ASSizeRange &constrainedSize) { - switch (style.justifyContent) { + const auto numOfItems = unpositionedLayout.items.size(); + ASDisplayNodeCAssertTrue(numOfItems > 0); + const CGFloat violation = unpositionedLayout.violation; + ASStackLayoutJustifyContent justifyContent = style.justifyContent; + + // Handle edge cases of "space between" and "space around" + if (justifyContent == ASStackLayoutJustifyContentSpaceBetween && (violation < 0 || numOfItems == 1)) { + justifyContent = ASStackLayoutJustifyContentStart; + } else if (justifyContent == ASStackLayoutJustifyContentSpaceAround && (violation < 0 || numOfItems == 1)) { + justifyContent = ASStackLayoutJustifyContentCenter; + } + + switch (justifyContent) { case ASStackLayoutJustifyContentStart: return stackedLayout(style, 0, unpositionedLayout, constrainedSize); case ASStackLayoutJustifyContentCenter: - return stackedLayout(style, floorf(unpositionedLayout.violation / 2), unpositionedLayout, constrainedSize); + return stackedLayout(style, floorf(violation / 2), unpositionedLayout, constrainedSize); case ASStackLayoutJustifyContentEnd: - return stackedLayout(style, unpositionedLayout.violation, unpositionedLayout, constrainedSize); + return stackedLayout(style, violation, unpositionedLayout, constrainedSize); + case ASStackLayoutJustifyContentSpaceBetween: { + const auto numOfSpacings = numOfItems - 1; + return stackedLayout(style, 0, floorf(violation / numOfSpacings), fmodf(violation, numOfSpacings), unpositionedLayout, constrainedSize); + } + case ASStackLayoutJustifyContentSpaceAround: { + // Spacing between items are twice the spacing on the edges + CGFloat spacingUnit = floorf(violation / (numOfItems * 2)); + return stackedLayout(style, spacingUnit, spacingUnit * 2, 0, unpositionedLayout, constrainedSize); + } } } diff --git a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm index 2252078da6..d70c7c17dd 100644 --- a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm @@ -98,6 +98,8 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentCenter flex:NO sizeRange:kSize identifier:@"justifyCenter"]; [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentEnd flex:NO sizeRange:kSize identifier:@"justifyEnd"]; [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flex:YES sizeRange:kSize identifier:@"flex"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flex:NO sizeRange:kSize identifier:@"justifySpaceBetween"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flex:NO sizeRange:kSize identifier:@"justifySpaceAround"]; } - (void)testOverflowBehaviors @@ -108,6 +110,10 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentCenter flex:NO sizeRange:kSize identifier:@"justifyCenter"]; [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentEnd flex:NO sizeRange:kSize identifier:@"justifyEnd"]; [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flex:YES sizeRange:kSize identifier:@"flex"]; + // On overflow, "space between" is identical to "content start" + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flex:NO sizeRange:kSize identifier:@"justifyStart"]; + // On overflow, "space around" is identical to "content center" + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flex:NO sizeRange:kSize identifier:@"justifyCenter"]; } - (void)testOverflowBehaviorsWhenAllFlexShrinkChildrenHaveBeenClampedToZeroButViolationStillExists @@ -246,6 +252,50 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:subnodes identifier:@"variableHeight"]; } +- (void)testJustifiedSpaceBetweenWithOneChild +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .justifyContent = ASStackLayoutJustifyContentSpaceBetween + }; + + ASStaticSizeDisplayNode *child = ASDisplayNodeWithBackgroundColor([UIColor redColor]); + child.staticSize = {50, 50}; + + // width 300px; height 0-INF + static ASSizeRange kVariableHeight = {{300, 0}, {300, INFINITY}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:@[child] identifier:nil]; +} + +- (void)testJustifiedSpaceAroundWithOneChild +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .justifyContent = ASStackLayoutJustifyContentSpaceAround + }; + + ASStaticSizeDisplayNode *child = ASDisplayNodeWithBackgroundColor([UIColor redColor]); + child.staticSize = {50, 50}; + + // width 300px; height 0-INF + static ASSizeRange kVariableHeight = {{300, 0}, {300, INFINITY}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:@[child] identifier:nil]; +} + +- (void)testJustifiedSpaceBetweenWithRemainingSpace +{ + // width 301px; height 0-300px; 1px remaining + static ASSizeRange kSize = {{301, 0}, {301, 300}}; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flex:NO sizeRange:kSize identifier:nil]; +} + +- (void)testJustifiedSpaceAroundWithRemainingSpace +{ + // width 305px; height 0-300px; 5px remaining + static ASSizeRange kSize = {{305, 0}, {305, 300}}; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flex:NO sizeRange:kSize identifier:nil]; +} + - (void)testChildThatChangesCrossSizeWhenMainSizeIsFlexed { ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithOneChild@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithOneChild@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..da37b6226298820deed9694f5be04a8aa2f94f11 GIT binary patch literal 1933 zcmeAS@N?(olHy`uVBq!ia0y~yV2S{;Q#jawWR#|H~$`lQ*5KTYy=S!PC{xWt~$(69C2Fr$Yb$ literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..088b49a293a71e5fd13bab6d420226df07c89b77 GIT binary patch literal 3436 zcmeAS@N?(olHy`uVBq!ia0y~yU`hhAQ#jawr0Rs2OF)XTILO_JVcj{Imp~3@fk$L9 z1A~|<2s3&HseAwm%4E9uhX83NAO-;=Al87=KsMWwE*Ad@K)S%w#WAFU@y)D@U5^8J zoPYnfznd!g>q1c1CAGeHCugQ=GW+G8+@yQr&buk|Z@;bkUeC`i66;uCA!WPx<9_jf zC58pNf3NRs)GL*V=S^3w$lPOc{QT)nk}~|qlOsR!iQG$ZTGN{B9rUtDpc*K*@|C{C z^9l<&@hP=Dk)JOgb+}{UvB2#Ncj=kdCLRvfHY0ntXZJsOT@+XlA(*jYF2wTZp5ues-$hHJ~a8+Q~=aOl19g>9dg-HC>Pgc#Sd)en}u z=xSPgx-Ej&HC&Ug_lDN&>aAA6tWxHO4rF~=c-{I^$>bwE8MV74I;7b-uXJ2A{}=f~ zf!V3Ca2n5<-~Xm+xQ2_!#F`z+(%9uym~mu}$b0>x>-Q|-GRbRcjKBTz^Y6Zs9+!QE zj2C=;nWT^>qY|L}to7#iLpDEJfIc|3Shr42`TN>EK_#vSFQz)LXEv!no5&+3!wW%W_raLfGLs;^7I zE(lF^SzC>!cX8gY&4rF^EliJdENA@xc@^CU0_Vk(^zDMcArP1QwfdXn^oh%6tE4%+ zhR49^P3Kc9p9&f&h)Aq|`|*2n%6#-#C@GfO{L=c!8Fx6C!zbR=vOu$cW2o8ptWqan z7$%$kyzqJ{N(z#weLTNaKXgH%GS3`U=Fj`;RjtsxUa5I6zAL`65tIVfzy0#F_IS%8 zZLU5&=_FH;*rgUGNpUl3_0Ke)2m9bdM)&;VCu+6Vqo?9)yjeA6C&6CGF}~^d(FHXh ztUO!2)drk|9%OyWK8~J_x8}bIeccRl!78Ig*L+d*8opX)FAWKOi)g-Q^>wBgK2V&e ze&dQ(es@-4w za}i2F-nc0JebxQ>S{HT*vaQoP9CimS1R`E9i`%OQ%ExQ;)T922#0D0W+!R>7@P*dV zg@TK7e~3Z6zra=O+Q;u*C1}~;vysf_ zw`yN^o}aD@jz^8_=Zoe(OGh(d_Pqn)C)vT7@I;>I98~+GQ;XM!r9)D%uicj_v=n6c z>zF;ae2RmH!MRC|mG}1xp{Ai3(;xj^`eW*JND9cduC2RoD4Ds-Gl^}BT`bR$-e#7p zBYO;f$IW1$5As39mZRsBpX{3&j+%;d?=Co&K3xamh1)*cf^B-y@XhZtFg<3pT9kSy9@LruT|(y%{+4=jgJ3w%;#>s(Xg`qn}Q1j=Fce$ZtD!*4u#c(c`VQ|LS_)1f#^lV|ic|^#A|CcV|t2bx$s+?s+>}^NiL! oqczWH%`;l_jMh8}G^}}kCrF#_{N+>ttbrIjUHx3vIVCg!01&tt;s5{u literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithOneChild@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithOneChild@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..88d0aaf203e820da4a9a00d4aa2a8dd02eb5c599 GIT binary patch literal 1563 zcmeAS@N?(olHy`uVBq!ia0y~yV2S{;Q#jawWR#E0+i(^OyCb`?$~<3%)s#f|1vurVCG;t3QE#%M-w#1Xo4QH#m@ugfcIu6&%WQW7gTtAy85}S Ib4q9e0Q2vK3jhEB literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4f859861308d9e5a2a4eecae946a7c42f1251250 GIT binary patch literal 2698 zcmeAS@N?(olHy`uVBq!ia0y~yV2T2=Q#jawWZaf{azKi)ILO_JVcj{Imp~3@fk$L9 z1A~|<2s3&HseAwm%4E9uhX83NAO-;=Al87=KsKxKc_*nY3=Ay)JzX3_Dj46~J((vR z$m0_D|G&RB=O%#!4yh?WIQil7gL|6$oD$rbqPjEQM)=8z`YHGrWz5eFn~~9GzTrcfkV2G8 zcG}afDsIL_qV5VlCb#O9a$ahESNPE6P#wMW>HkMNYI+WYvph06?AM*)n)`KMmcF_2 zhZzpDGPWH5{9{Uucw;Sxz*-9?7oho<@64Ozy&yK{(x>xlMJBFwQ1CO(+0r3)F)DG( ziHDMnCKogMpWlr9$gTw%jphKpUw4qG~|eQPUw&cpOJY4WuHJ*xzthAc9hCBTAbZ*Q3)tH@_Msc*;gBgEql0xKB~ZNSt6)(O&6j?H~obi%RN1fG^LWiGlXcmCVt5_P8aFyHUZiwQrY2KN2=b!r!v!+n40 zc-xPCs$k#ORL^_o{y-8GI}I6vYtwaKZx-d)((N1dPy5OgnIg_3nQgsdK+o)Cc24;% z;33@mt>T+ysK4U_LztH1XJ74pEs!BTd&{5eD|Ke72*JWw$h~#OHx;lgZz{LVYdw(9 z?JNJomB9{M9~S@Vq>{jLNwX+m6sPiXUVP)P;A?X0@y-7&wt|h;Fk7md ze`WbX0&G^O+G-J4fQ6l^5I7zP39!ZIZWasv0Qv(QEvE~j<~r=YvE=Fgs=X)QM*%fB z{sktMU1b^-zdu4WJodjMJqs3Eru|F$?%Y1Aru)Bd)#Rt4pd{(m4Gfai#~Q!Sa|OG} z^7Wm)%O^uqO4xIQtW&!{sWE?*?aq1ru+(T(FHrG#C&Yqc|NFOZ!P4*6-i7;Wz!vP| zn(}cb+yZ0whVocPumw98vF+L14zu7qL)O_XU<=kdSI&2aTd@4+-ltZO2rr%b;4Cjo zN08XDiupqj2G57t?aA9!Vty{1EXFRQOVxyGzN zX@Q!c#UzJ0X*V)v9#H!*QTP2#PN1>Va@4N0NL+)Z!L9EOOuShS%8M^sz&RGYPapoDid zJEpE2X5_WKj g-;5T7BcmWZIcwRcnpLN3fCVChr>mdKI;Vst0DB75@&Et; literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceAround@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceAround@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1d71834f705aee1dfb507219ddab9719513a94c6 GIT binary patch literal 3339 zcmeAS@N?(olHy`uVBq!ia0y~yV2S{;Q#jawWR#d{8S!cN2d<+w+V+ z-ww%7W47o2KT#&(Vqo~@bAjL4KC-Qx-~3eDR!*+}sbYT41QER}n*ww)4Nhu(1B^k3CgI*T>?)#RoFI_v3yXE@}|f4>`PO z@w&9Ri{J4+;tjHAKOJ6WVPkcD*Oa?Y zD>crDZssoA{3aMhciPMon_sRm&P+Yfc=JPKZ2p3ydN&IWcpNTTaM3GdC1dti&${dX z0%mAG)8nam_dI;E&`G$Hk2O5y{qU3R#jl|BOQ#x`@0LZ`f1AM)^=1+;dyk0@yVP2j zsZtyESRT74<8L1bG~uS0z44CuXs$o!=eEE8P1`Dv3m7-rw!ICtV?{G!-K-V6ZP{CQ zCY2rT-xuGRVtJrTd{rVaE|L!{@_!Wh_3F$mMh_0GkyyLR8qMQdU$N}3jt6?Y*qqPq z|5wqaE}LW$j&fh!@M6Vk!vxEp@#_CI>|gmrNfso8M8~F|DdgO8LsgnjA~S-|B&t)( zG@&xNXQ$!4T9?FiGroWOQO)*y@jfq=(~Mzz0(iUoc1bZ`yV&{S`?8?65&g=Xz4Kabb@ z?neOquDtGk;oe*FDB-rNYSX(5B4RHgIFgn>bxb~wFsFG`$m z{iU)$yB_T70&zCG@1Iwnbm)%V^soUbu^xCXz2G%BSMkx3+nIlFBrBn~{GLp5e&6!> zU=u#%bkCQ(a~{L>R@Rb#|811!0=nSHH;HZGKUGkS2=~2mcU~|!)$M-hejh#2cH4EP zUu{1WW4JkK%c?vSk4Kd@-QT+&?C~Aa?DPNs>M}x0Gf$t?&;7Ibj{`8q0&`2}8Xk8= ziL*V1$L>hHx4Q;TbJf*wyo*>|S=1xGpqD|hLi zy{mZ8l8og?fH#^vjXpAm99O^R9bnaenR9k2kW|fcdvy&Ln&)Fp1Iw%VfT`|Hn|$qlltk+~|H$T7tHGsQq+wKOIhx0_uX5IX z-v=t?@?>iMz1H?ZN;9AN85sWmpL?Yz67(QPux`ITd0i2AGFrYf-NF)U*0vm#?tk7{QP%WScj3N$UTm;L-xEZ!d z+}NnVN*B#&OD|R!AdJ#`5A|3!-3Vi-}*Yoh3c_?--oUo$!sBj<~+%pef2nB~zivV!( zMgyb9dp&|b?#YdyC&C#A2l~7P$Lp3EKHGV0HkBj;fY(za4keah3*&L;C*FP$f2YTM zh18=QKTivAlXZ(W+i6PogP-^FHGF=$*){8tK8zrtE1OEMKUA9?gX3`KoJoEc{&d0L z>6l>`;S;Kojj0=cEj*Tlt4`vAjHIc*lBD8ng*T?eNsc#W{W4>~>&oty@Glr%kORHp zr155{7f7i#y4Nox_eBG_&I#;SmUp&?uAjmK2f5W`N=T)mJJP;p1am9Llj|d@S zcq~pj{c@dazKOb83FFHAN+-z*s-4GpyLAdZoW4ycDsRBDP&~wo>wCOzggmTqO?)rqy0Ye?PPFP2BsD zkO=wK;)Q8+K)E0xM{L#1kaia_iLgZ2ycmx3h)ovMZ~k0QK`XjTG?tWRsU&94gF!ZY z0piaN7_SCE$+O4L-rt9F$&j)N1x8dls=$jBCeKnwo#nTAV+}~a1WlL2u}MN_O1|*A_9p|x* zs$zl@uke6^ISz)|I+5hscY^)hyCDlPQ1GhT4_&9>ffd~8`8gz* z5_Xa@{!Bo{BCm>qGCATe+}g6qK%V)5xF7(nbP$E*o|ZE8{b`MT2^plH-4OApHt`#^ z$=VN)vbk2eHqoM&!QNB_As5u5dkKNugYJI!m+F@{VFI;jU>}c)0u9WHnymguD{!Nf zb7+5UPlA6l+`k<7UcNKtAaD1>RLdyyT0w0832XQ@^5wK`_KycdT-!^w?1yviK-;rO zv7JO~`H<4=*gnXE=)q=Yj_n<5