Skip to content

Commit

Permalink
Fit subviews that are larger than the proposed size
Browse files Browse the repository at this point in the history
  • Loading branch information
tevelee committed Jul 23, 2024
1 parent 60c564e commit 5fdf6ba
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 7 deletions.
14 changes: 11 additions & 3 deletions Sources/Flow/Internal/Layout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,14 @@ struct FlowLayout: Sendable {
of subviews: some Subviews,
cache: FlowLayoutCache
) -> Lines {
let sizes = cache.subviewsCache.map(\.ideal)
let spacings = if let itemSpacing {
let sizes: [Size] = zip(cache.subviewsCache, subviews).map { cache, subview in
if cache.ideal.fits(in: proposedSize) {
cache.ideal
} else {
subview.sizeThatFits(proposedSize).size(on: axis)
}
}
let spacings: [CGFloat] = if let itemSpacing {
[0] + Array(repeating: itemSpacing, count: subviews.count - 1)
} else {
[0] + cache.subviewsCache.adjacentPairs().map { lhs, rhs in
Expand All @@ -133,7 +139,7 @@ struct FlowLayout: Sendable {
FlowLineBreaker()
}

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

guard remainingSpace > 0 else { return }

if justification.isStretchingItems {
let sumOfMax = subviewsInPriorityOrder.sum { $0.spacing + $0.cache.max.breadth }
let potentialGrowth = sumOfMax - sumOfIdeal
Expand Down
11 changes: 11 additions & 0 deletions Sources/Flow/Internal/Size.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ struct Size: Sendable {
case .vertical: \.depth
}
}

@usableFromInline
func fits(in proposedSize: ProposedViewSize) -> Bool {
if let proposedWidth = proposedSize.width, self[.horizontal] > proposedWidth {
return false
}
if let proposedHeight = proposedSize.height, self[.vertical] > proposedHeight {
return false
}
return true
}
}

extension Axis {
Expand Down
17 changes: 17 additions & 0 deletions Tests/FlowTests/FlowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,4 +325,21 @@ final class FlowTests: XCTestCase {
+------+
""")
}

func test_HFlow_text() {
// Given
let sut: FlowLayout = .horizontal(alignment: .center, itemSpacing: 1, lineSpacing: 0)

// When
let result = sut.layout([WrappingText(size: 6×1), 1×1, 1×1, 1×1], in: 5×3)

// Then
XCTAssertEqual(render(result), """
+-----+
|XXXXX|
|XXXXX|
|X X X|
+-----+
""")
}
}
24 changes: 20 additions & 4 deletions Tests/FlowTests/Utils/TestSubview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import SwiftUI
import XCTest
@testable import Flow

final class TestSubview: Flow.Subview, CustomStringConvertible {
class TestSubview: Flow.Subview, CustomStringConvertible {
var spacing = ViewSpacing()
var priority: Double = 1
var placement: (position: CGPoint, size: CGSize)?
Expand Down Expand Up @@ -58,7 +58,22 @@ final class TestSubview: Flow.Subview, CustomStringConvertible {
}
}

extension [TestSubview]: Subviews {}
final class WrappingText: TestSubview {
override func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
let area = idealSize.width * idealSize.height
if let proposedWidth = proposal.width, idealSize.width > proposedWidth {
let height = (Int(1)...).first { area <= proposedWidth * CGFloat($0) }!
return CGSize(width: proposedWidth, height: CGFloat(height))
}
if let proposedHeight = proposal.height, idealSize.height > proposedHeight {
let width = (Int(1)...).first { area <= proposedHeight * CGFloat($0) }!
return CGSize(width: CGFloat(width), height: proposedHeight)
}
return super.sizeThatFits(proposal)
}
}

extension [TestSubview]: Flow.Subviews {}

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

Expand Down Expand Up @@ -92,6 +107,8 @@ func render(_ layout: LayoutDescription, border: Bool = true) -> String {
struct Point: Hashable {
let x, y: Int
}
let width = Int(layout.reportedSize.width)
let height = Int(layout.reportedSize.height)

var positions: Set<Point> = []
for view in layout.subviews {
Expand All @@ -101,14 +118,13 @@ func render(_ layout: LayoutDescription, border: Bool = true) -> String {
for x in Int(point.x) ..< Int(point.x + placement.size.width) {
let result = positions.insert(Point(x: x, y: y))
precondition(result.inserted, "Boxes should not overlap")
precondition(x >= 0 && x < width && y >= 0 && y < height, "Out of bounds")
}
}
} else {
fatalError("Should be placed")
}
}
let width = Int(layout.reportedSize.width)
let height = Int(layout.reportedSize.height)
var result = ""
if border {
result += "+" + String(repeating: "-", count: width) + "+\n"
Expand Down

0 comments on commit 5fdf6ba

Please sign in to comment.