From 53c4a546a8e8fb42e76e75d3f370461f74ec7312 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Mon, 10 Jun 2024 20:24:11 -0700 Subject: [PATCH] Add priority to allow adjusting how extra space in a run/row in a flow should be used. --- BlueprintUI/Sources/Layout/Flow.swift | 152 ++++++++++++++---- .../Tests/Sources/FlowTests.swift | 58 +++++++ .../test_priority_one-scaling_iOS-15.png | Bin 0 -> 963 bytes .../test_priority_one-scaling_iOS-16.png | Bin 0 -> 951 bytes .../test_priority_one-scaling_iOS-17.png | Bin 0 -> 1036 bytes .../test_priority_two-even-scaling_iOS-15.png | Bin 0 -> 911 bytes .../test_priority_two-even-scaling_iOS-16.png | Bin 0 -> 899 bytes .../test_priority_two-even-scaling_iOS-17.png | Bin 0 -> 984 bytes .../test_priority_two-scaling_iOS-15.png | Bin 0 -> 904 bytes .../test_priority_two-scaling_iOS-16.png | Bin 0 -> 892 bytes .../test_priority_two-scaling_iOS-17.png | Bin 0 -> 977 bytes CHANGELOG.md | 2 + 12 files changed, 182 insertions(+), 30 deletions(-) create mode 100644 BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_one-scaling_iOS-15.png create mode 100644 BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_one-scaling_iOS-16.png create mode 100644 BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_one-scaling_iOS-17.png create mode 100644 BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_two-even-scaling_iOS-15.png create mode 100644 BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_two-even-scaling_iOS-16.png create mode 100644 BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_two-even-scaling_iOS-17.png create mode 100644 BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_two-scaling_iOS-15.png create mode 100644 BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_two-scaling_iOS-16.png create mode 100644 BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_two-scaling_iOS-17.png diff --git a/BlueprintUI/Sources/Layout/Flow.swift b/BlueprintUI/Sources/Layout/Flow.swift index a1adffb70..aa014b809 100644 --- a/BlueprintUI/Sources/Layout/Flow.swift +++ b/BlueprintUI/Sources/Layout/Flow.swift @@ -116,6 +116,25 @@ extension Flow { case bottom } + /// When there is extra space in a run, how the extra space should be used. + public enum Priority { + + public static let `default` = Self.fixed + + /// The item will take up only the space it asked for. + case fixed + + /// The item will be stretched to fill any extra space in each run. + case grows + + var scales: Bool { + switch self { + case .fixed: false + case .grows: true + } + } + } + /// A child placed within the flow layout. public struct Child: ElementBuilderChild { @@ -136,13 +155,21 @@ extension Flow { } /// Creates a new child item with the given element. - public init(_ element: Element, key: AnyHashable? = nil) { + public init(_ element: Element, key: AnyHashable? = nil, priority: Priority = .default) { self.key = key - traits = .init() self.element = element + + traits = .init(priority: priority) } - public struct Traits {} + public struct Traits { + + public var priority: Priority + + public init(priority: Flow.Priority = .default) { + self.priority = priority + } + } } } @@ -150,8 +177,11 @@ extension Flow { extension Element { /// Wraps the element in a `Flow.Child` to allow customizing the item in the flow layout. - public func flowChild(key: AnyHashable? = nil) -> Flow.Child { - .init(self, key: key) + public func flowChild( + priority: Flow.Priority = .default, + key: AnyHashable? = nil + ) -> Flow.Child { + .init(self, key: key, priority: priority) } } @@ -178,7 +208,12 @@ extension Flow { cache: inout () ) -> CGSize { size( - for: subelements.map { $0.sizeThatFits(_:) }, + for: subelements.map { + .init( + traits: $0.traits(forLayoutType: Self.self), + size: $0.sizeThatFits(_:) + ) + }, in: proposal ) } @@ -191,7 +226,12 @@ extension Flow { ) { zip( frames( - for: subelements.map { $0.sizeThatFits(_:) }, + for: subelements.map { + .init( + traits: $0.traits(forLayoutType: Self.self), + size: $0.sizeThatFits(_:) + ) + }, in: .init(size) ), subelements @@ -200,10 +240,17 @@ extension Flow { } } - typealias ElementSize = (SizeConstraint) -> CGSize + + /// Shim type. Once legacy layout is removed, we can remove this shim and just use `Child` directly. + private struct FlowChild { + typealias ElementSize = (SizeConstraint) -> CGSize + + var traits: Traits + var size: ElementSize + } private func frames( - for elements: [ElementSize], + for elements: [FlowChild], in constraint: SizeConstraint ) -> [CGRect] { @@ -217,7 +264,7 @@ extension Flow { for element in elements { let elementSize: CGSize = { - let size = element(constraint) + let size = element.size(constraint) return CGSize( width: min(size.width, constraint.width.maximum), @@ -237,14 +284,19 @@ extension Flow { ) } - row.addItem(of: elementSize) + row.add( + .init( + size: elementSize, + traits: element.traits + ) + ) } return frames + row.itemFrames() } private func size( - for elements: [ElementSize], + for elements: [FlowChild], in constraint: SizeConstraint ) -> CGSize { frames( @@ -268,7 +320,12 @@ extension Flow { )] ) -> CGSize { size( - for: items.map { $0.content.measure(in:) }, + for: items.map { + .init( + traits: $0.traits, + size: $0.content.measure(in:) + ) + }, in: constraint ) } @@ -281,7 +338,12 @@ extension Flow { )] ) -> [LayoutAttributes] { frames( - for: items.map { $0.content.measure(in:) }, + for: items.map { + .init( + traits: $0.traits, + size: $0.content.measure(in:) + ) + }, in: .init(size) ).map(LayoutAttributes.init(frame:)) } @@ -336,7 +398,7 @@ extension Flow.Layout { struct Item { let size: CGSize - let xOffset: CGFloat + let traits: Flow.Layout.Traits } /// `True` if we can fit an item of the given size in the row. @@ -347,32 +409,58 @@ extension Flow.Layout { } /// Adds item of given size to the row layout. - mutating func addItem(of size: CGSize) { - items.append( - .init( - size: size, - xOffset: totalItemWidth + itemSpacing * CGFloat(items.count) - ) - ) - totalItemWidth += size.width - height = max(size.height, height) + mutating func add(_ item: Item) { + items.append(item) + + totalItemWidth += item.size.width + height = max(item.size.height, height) } - /// Compute frames for the items in the row layout. func itemFrames() -> [CGRect] { + let totalSpacing = (CGFloat(items.count) - 1) * itemSpacing + + let scalingConstant: CGFloat = items + .map { + switch $0.traits.priority { + case .fixed: 0.0 + case .grows: 1.0 + } + } + .reduce(0, +) + + let scalableWidth = items + .filter(\.traits.priority.scales) + .map(\.size.width) + .reduce(0, +) + + let hasScalingItems = scalingConstant > 0.0 + let extraWidth = maxWidth - totalItemWidth - totalSpacing - let firstItemX: CGFloat = { + + let firstItemX: CGFloat = if hasScalingItems { + 0.0 + } else { switch lineAlignment { case .center: extraWidth / 2.0 case .trailing: extraWidth case .leading: 0.0 } - }() + } + + var xOrigin: CGFloat = firstItemX return items.map { item in - .init( - x: firstItemX + item.xOffset, + let percentOfScalableWidth = item.size.width / scalableWidth + + let width = if item.traits.priority.scales { + item.size.width + (extraWidth * percentOfScalableWidth) + } else { + item.size.width + } + + let frame = CGRect( + x: xOrigin, y: { switch itemAlignment { case .fill: origin @@ -381,7 +469,7 @@ extension Flow.Layout { case .bottom: origin + (height - item.size.height) } }(), - width: item.size.width, + width: width, height: { switch itemAlignment { case .fill: height @@ -389,6 +477,10 @@ extension Flow.Layout { } }() ) + + xOrigin = frame.maxX + itemSpacing + + return frame } } } diff --git a/BlueprintUICommonControls/Tests/Sources/FlowTests.swift b/BlueprintUICommonControls/Tests/Sources/FlowTests.swift index 05d839006..8035b420e 100644 --- a/BlueprintUICommonControls/Tests/Sources/FlowTests.swift +++ b/BlueprintUICommonControls/Tests/Sources/FlowTests.swift @@ -57,6 +57,64 @@ class FlowTests: XCTestCase { compareSnapshot(of: flow) } + + func test_priority() { + func flow( + lineAlignment: Flow.LineAlignment, + itemAlignment: Flow.ItemAlignment, + @ElementBuilder _ children: () -> [Flow.Child] + ) -> Element { + Flow( + lineAlignment: lineAlignment, + lineSpacing: 10, + itemAlignment: itemAlignment, + itemSpacing: 2 + ) { + children() + } + .constrainedTo(width: .absolute(200)) + } + + compareSnapshot( + of: flow(lineAlignment: .leading, itemAlignment: .center) { + ConstrainedSize(width: 100, height: 40, color: .green) + ConstrainedSize(width: 90, height: 40, color: .red) + ConstrainedSize(width: 40, height: 40, color: .purple) + + ConstrainedSize(width: 80, height: 40, color: .lightGray) + .flowChild(priority: .grows) + }, + identifier: "one-scaling" + ) + + compareSnapshot( + of: flow(lineAlignment: .leading, itemAlignment: .center) { + ConstrainedSize(width: 100, height: 40, color: .green) + ConstrainedSize(width: 90, height: 40, color: .red) + + ConstrainedSize(width: 40, height: 40, color: .purple) + .flowChild(priority: .grows) + + ConstrainedSize(width: 40, height: 40, color: .lightGray) + .flowChild(priority: .grows) + }, + identifier: "two-even-scaling" + ) + + compareSnapshot( + of: flow(lineAlignment: .leading, itemAlignment: .center) { + ConstrainedSize(width: 100, height: 40, color: .green) + ConstrainedSize(width: 90, height: 40, color: .red) + + ConstrainedSize(width: 40, height: 40, color: .purple) + .flowChild(priority: .grows) + + ConstrainedSize(width: 80, height: 40, color: .lightGray) + .flowChild(priority: .grows) + }, + identifier: "two-scaling" + ) + } } extension ConstrainedSize { diff --git a/BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_one-scaling_iOS-15.png b/BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_one-scaling_iOS-15.png new file mode 100644 index 0000000000000000000000000000000000000000..1611fb53363262ffc5a67d353bf74639754be086 GIT binary patch literal 963 zcmeAS@N?(olHy`uVBq!ia0vp^CxAGLgAGXj{U&)0NHG=%xjQkeJ16rJ$Z<)H@J#dd zWzYh$IT#q*GZ|PwN`P1jh#44|7cep~18GK(*a9ZF?1=@;aCQ_(p}}0wr$9<3)5Sjo zNHc-NfIt_BHJ~(*&A3|h!Q;6=y4=&nF{Fa=&E11t%>g1V2mk!{w>Ivc|(>l+dd2vZs%;}wl!vSIOlVA7AF4z zI|IZ2|7%>ty@65C1d4*US2yM|2MRD>)QPFKv4Bgs*T8EBx?-?+u= z@3$zhY*d)Y*u*lYGV)C8fxUiI e3Fa8QUat@P#1VNoU}*{{r+B*hxvX16El3Oqbb(j{ zN(0%9t3@9?o(rVQJzX3_Dj46~J=oP8AmVcH&wqbw2ZjDd+m;q@Xmpit*wHU|dAl-MgEp~4#sKoi$= zO96#c_@qH5mIg8pkR=uWF#P|&#zov4 z7zItBD0q8yV=i-`0P{ud*sm*27C#9P7LvAM?$V1$wrj76tLISoF!{je&)xh$hL8(~ z00$Ebr-hxOgMg%i0*B4~=bz*L{r&&`{`>D=4=2oJY!dMQ=)%S!RB$c#>Ak(JDjbeF z9w`k9jP8r?XMg!RTcAZjOvT7yLc``f?eFh&#T+@39F>@X21@;nTfF{$ivr6=g^7$! zEOSbucb4aGH+f|?`V!{aqN2Wfv z{nuZ-Hdln%mVP(v-Q8%NCV?YOV8@vJ+Upj-pA&A|>up8v?^%t+_JHc!_w`=X4-P-5^-Mg<&7;YQb>qnJfj=}5o`mj$N Uk%t48rhsyar>mdKI;Vst0Ay@IUYh$`Yd`A* z{r_8Y9;|Ox?@&Kc{_aY6tPlD1*)(u+v8Yp;o`=TP`C`M~GT-TXj?kPC+Z z2NMgYg`J{{`|n>5C(LDR67c`%!p0#~a4q-gy}hj}9F963 zDGdsY?u+kdfB8CFphZDU#mHeo!{$8g@9%WQ966F4m6(ABO8t#ny#9WR0?S5)iHuDw zb4sIkmgjHea1!7al2q_GaPh@=soKA4EQ%ehojd|657yj1wYR$4RiG(i!U+aPrarg* z*I&FgSA^M?emCsh-DsUAfg?>|$C&)u>lVMC6K>ng1V2mk!{w>Ivc|(>l+dd2vZs%;}wl!vSIOlVA7AF4z zI|IZ2|7%>ty@65S2a1BXHw?KB8wjvC*xr5rQGKe`gk#UsxLfBsXf-;Bzweu~wbthV zi=6rfxp18s4V*cQ9|dL{FkobqY)d}L!g9nof$1>IwXN&kRv+WpxBqhO{oCJrBsoqv zvMMU1w>;WDhsDJq!?Z(Jfk{b$spy#XvD>@nX;0IhX3xU55hw*j|6X}LmrbcixWCTj zWoeT@i-NNPD_A0Cf#g{f zJ$6M#e^cfFtJz{w_iGMFp2KNE$>wcu+`(qBWM=;PV#x_H@MT)=&3>>M9G5Qrcv&n2 zG4RsaTbtw|_AFWY@$z#Oh=EIHZ%ws_+T;Cr`FT%>fu82sr|ZD>dVfg3y$hdFm Rj7U(b_jL7hS?83{1OS>16El3Oqbb(j{ zN(0%9t3@9?o(rVQJzX3_Dj46~J=oP8AmVcH&wqbw2ZjDd+m;q@Xmpit*wHU|dAl-MgEp~4#sKoi$= zO96#c_@qH5mIg8pkR=uWF#P|&#zov4 z7zKWyD0q9rkn6C40E>g|-S;2Wr)o_&_B@Tdb*_U}ql5VSzByZKeGah5sc(=A*O}44 znZx)|V8#IhMn=iDs(%zHVL#SI4iJ% zC1N&S3h$rxm%~W_B&ypx-%i~T%u%_!M)$X1ivo)xhhvnKeXSozo<-4PS7h`zWe%{K zEhcrp=78imoFC%sv#X=ARFP*)$NgiU) zlBFLnKUaYmxMcR$RC}mB-jA1`_kUYh$`Yd`A* z{r_8Y9;|Ox?@&Kc{_aY6t#%_Ui-Yam_aD`#YE3xyJdL|`u7g&igZTTtIa_Oe4zS3nZ;%VunbE+R z!}w8P#sLFHM#;A1lPoMpj1!m+vs~M{?rrrko_+f-*WSPVy+@Mcgd?k>LVC-i?Q>XM z95PHhbQPGC6qt&RSs%N-d!F_*?P>NbY#V`6K=kjG*K^sFiiG>?Twazo3A89UE3kqk zVm4k1@1ORU!$|-ns@pr?PTdjAQMtQD_qSk+0*fMtW0aJAtsh99MbTqdWb`*>4zQXn zCUw8&faE!xCX{U6_QoA-21{ng1V2mk!{w>Ivc|(>l+dd2vZs%;}wl!vSIOlVA7AF4z zI|IZ2|7%>ty@65S4vK=e*AH?YHV|NOn7Xd&%95sor?S`PCNQZL{L{T9U72Ig>G8n0 z@#D{9+?*Z>+8xdcjowULMV!aAIW;*y%r;wZADK7D7>&Y-2)rj8s=0!;!cx$c|SsX^E+VzJ#*1d+tKhLHG# aZUy_aWem25oMe`RlDns?pUXO@geCwz01|@$ literal 0 HcmV?d00001 diff --git a/BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_two-scaling_iOS-16.png b/BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_two-scaling_iOS-16.png new file mode 100644 index 0000000000000000000000000000000000000000..7c8baf90413c6d4c37c3c8d5ed1451aa1b3452d5 GIT binary patch literal 892 zcmeAS@N?(olHy`uVBq!ia0vp^CxAGLgAGXj{U&)0NHG=%xjQkeJ16rJ$gxO`@J#dd zWzYh$IT#q*GZ|PwN`P1jh#44|7BIm@PAp)Cv!g(g!lBvsfs{<9i+>16El3Oqbb(j{ zN(0%9t3@9?o(rVQJzX3_Dj46~J=oP8AmVcH&wqbw2ZjDd+m;q@Xmpit*wHU|dAl-MgEp~4#sKoi$= zO96#c_@qH5mIg8pkR=uWF#P|&#zov4 z7zOU2D0qAQAm?EN0Tzd;>#D9SX*zf+du?t4lUl()-CNR?Irf|$4~!c>{yfIb>5-t_ z;jGZ;&BRs2d0d-Qqr(`)N$%Te`&s^CRCaYr!MTQ}J=~8>p0GTQ_nc?EK!MBY%;~j` zjv_9OB1`JicP8x&TAFR@$l)Z=B%qS(zImM*gxw+*+dV}PNt|m4iBITOuuogYV0*|( RW;rOid%F6$taD0e0swN45r+T( literal 0 HcmV?d00001 diff --git a/BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_two-scaling_iOS-17.png b/BlueprintUICommonControls/Tests/Sources/Resources/ReferenceImages/FlowTests/test_priority_two-scaling_iOS-17.png new file mode 100644 index 0000000000000000000000000000000000000000..899002598714751a8526f7590809ef893c65caee GIT binary patch literal 977 zcmeAS@N?(olHy`uVBq!ia0vp^CxAGLgAGXj{U&)0NHG=%xjQkeJ16rJ$f-z;@J#dd zWzYh$IT%UYh$`Yd`A* z{r_8Y9;|Ox?@&Kc{_aY6tFc6Ca@xrU}a+>cD2usn|UoM*g1fy?R4>9vlI zA})?1OX|~iChZJbnr-UH;Uv%`ppxspd7T=B-69s-Jw*^noNEY)Pv};#Pg}-dd&o&< QIVin*y85}Sb4q9e0AuYJBme*a literal 0 HcmV?d00001 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0deac32a0..c843f3b5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `Flow` children now support a layout `priority`, to specify if they should grow to use the extra space in a row. + ### Removed ### Changed