Skip to content

Commit 5fdf6ba

Browse files
committed
Fit subviews that are larger than the proposed size
1 parent 60c564e commit 5fdf6ba

4 files changed

Lines changed: 59 additions & 7 deletions

File tree

Sources/Flow/Internal/Layout.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,14 @@ struct FlowLayout: Sendable {
118118
of subviews: some Subviews,
119119
cache: FlowLayoutCache
120120
) -> Lines {
121-
let sizes = cache.subviewsCache.map(\.ideal)
122-
let spacings = if let itemSpacing {
121+
let sizes: [Size] = zip(cache.subviewsCache, subviews).map { cache, subview in
122+
if cache.ideal.fits(in: proposedSize) {
123+
cache.ideal
124+
} else {
125+
subview.sizeThatFits(proposedSize).size(on: axis)
126+
}
127+
}
128+
let spacings: [CGFloat] = if let itemSpacing {
123129
[0] + Array(repeating: itemSpacing, count: subviews.count - 1)
124130
} else {
125131
[0] + cache.subviewsCache.adjacentPairs().map { lhs, rhs in
@@ -133,7 +139,7 @@ struct FlowLayout: Sendable {
133139
FlowLineBreaker()
134140
}
135141

136-
let breakpoints = lineBreaker.wrapItemsToLines(
142+
let breakpoints: [Int] = lineBreaker.wrapItemsToLines(
137143
sizes: sizes.map(\.breadth),
138144
spacings: spacings,
139145
in: proposedSize.replacingUnspecifiedDimensions(by: .infinity).value(on: axis)
@@ -185,6 +191,8 @@ struct FlowLayout: Sendable {
185191
let sumOfIdeal = subviewsInPriorityOrder.sum { $0.spacing + $0.cache.ideal.breadth }
186192
var remainingSpace = proposedSize.value(on: axis) - sumOfIdeal
187193

194+
guard remainingSpace > 0 else { return }
195+
188196
if justification.isStretchingItems {
189197
let sumOfMax = subviewsInPriorityOrder.sum { $0.spacing + $0.cache.max.breadth }
190198
let potentialGrowth = sumOfMax - sumOfIdeal

Sources/Flow/Internal/Size.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ struct Size: Sendable {
3434
case .vertical: \.depth
3535
}
3636
}
37+
38+
@usableFromInline
39+
func fits(in proposedSize: ProposedViewSize) -> Bool {
40+
if let proposedWidth = proposedSize.width, self[.horizontal] > proposedWidth {
41+
return false
42+
}
43+
if let proposedHeight = proposedSize.height, self[.vertical] > proposedHeight {
44+
return false
45+
}
46+
return true
47+
}
3748
}
3849

3950
extension Axis {

Tests/FlowTests/FlowTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,4 +325,21 @@ final class FlowTests: XCTestCase {
325325
+------+
326326
""")
327327
}
328+
329+
func test_HFlow_text() {
330+
// Given
331+
let sut: FlowLayout = .horizontal(alignment: .center, itemSpacing: 1, lineSpacing: 0)
332+
333+
// When
334+
let result = sut.layout([WrappingText(size: 6×1), 1×1, 1×1, 1×1], in: 5×3)
335+
336+
// Then
337+
XCTAssertEqual(render(result), """
338+
+-----+
339+
|XXXXX|
340+
|XXXXX|
341+
|X X X|
342+
+-----+
343+
""")
344+
}
328345
}

Tests/FlowTests/Utils/TestSubview.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import SwiftUI
22
import XCTest
33
@testable import Flow
44

5-
final class TestSubview: Flow.Subview, CustomStringConvertible {
5+
class TestSubview: Flow.Subview, CustomStringConvertible {
66
var spacing = ViewSpacing()
77
var priority: Double = 1
88
var placement: (position: CGPoint, size: CGSize)?
@@ -58,7 +58,22 @@ final class TestSubview: Flow.Subview, CustomStringConvertible {
5858
}
5959
}
6060

61-
extension [TestSubview]: Subviews {}
61+
final class WrappingText: TestSubview {
62+
override func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
63+
let area = idealSize.width * idealSize.height
64+
if let proposedWidth = proposal.width, idealSize.width > proposedWidth {
65+
let height = (Int(1)...).first { area <= proposedWidth * CGFloat($0) }!
66+
return CGSize(width: proposedWidth, height: CGFloat(height))
67+
}
68+
if let proposedHeight = proposal.height, idealSize.height > proposedHeight {
69+
let width = (Int(1)...).first { area <= proposedHeight * CGFloat($0) }!
70+
return CGSize(width: CGFloat(width), height: proposedHeight)
71+
}
72+
return super.sizeThatFits(proposal)
73+
}
74+
}
75+
76+
extension [TestSubview]: Flow.Subviews {}
6277

6378
typealias LayoutDescription = (subviews: [TestSubview], reportedSize: CGSize)
6479

@@ -92,6 +107,8 @@ func render(_ layout: LayoutDescription, border: Bool = true) -> String {
92107
struct Point: Hashable {
93108
let x, y: Int
94109
}
110+
let width = Int(layout.reportedSize.width)
111+
let height = Int(layout.reportedSize.height)
95112

96113
var positions: Set<Point> = []
97114
for view in layout.subviews {
@@ -101,14 +118,13 @@ func render(_ layout: LayoutDescription, border: Bool = true) -> String {
101118
for x in Int(point.x) ..< Int(point.x + placement.size.width) {
102119
let result = positions.insert(Point(x: x, y: y))
103120
precondition(result.inserted, "Boxes should not overlap")
121+
precondition(x >= 0 && x < width && y >= 0 && y < height, "Out of bounds")
104122
}
105123
}
106124
} else {
107125
fatalError("Should be placed")
108126
}
109127
}
110-
let width = Int(layout.reportedSize.width)
111-
let height = Int(layout.reportedSize.height)
112128
var result = ""
113129
if border {
114130
result += "+" + String(repeating: "-", count: width) + "+\n"

0 commit comments

Comments
 (0)