Skip to content

Commit

Permalink
Alignment for both axes
Browse files Browse the repository at this point in the history
as suggested by @fred-bowker
  • Loading branch information
tevelee committed Aug 14, 2024
1 parent 9798fa5 commit b528bd0
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 66 deletions.
24 changes: 12 additions & 12 deletions Sources/Flow/Example/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ struct ContentView: View {
@State private var itemSpacing: CGFloat? = nil
@State private var lineSpacing: CGFloat? = nil
@State private var justified: Justified = .none
@State private var horizontalAlignment: HAlignment = .center
@State private var verticalAlignment: VAlignment = .center
@State private var horizontalAlignment: HAlignment = .leading
@State private var verticalAlignment: VAlignment = .top
@State private var distributeItemsEvenly: Bool = false
private let texts = "This is a long text that wraps nicely in flow layout".components(separatedBy: " ").map { string in
AnyView(Text(string))
Expand Down Expand Up @@ -67,10 +67,8 @@ struct ContentView: View {
}
}
Section(header: Text("Alignment")) {
switch axis {
case .horizontal: picker($verticalAlignment)
case .vertical: picker($horizontalAlignment)
}
picker($horizontalAlignment)
picker($verticalAlignment)
}
Section(header: Text("Spacing")) {
stepper("Item", $itemSpacing)
Expand Down Expand Up @@ -132,19 +130,21 @@ struct ContentView: View {
case .horizontal:
return AnyLayout(
HFlow(
alignment: verticalAlignment.value,
itemSpacing: itemSpacing,
rowSpacing: lineSpacing,
horizontalAlignment: horizontalAlignment.value,
verticalAlignment: verticalAlignment.value,
horizontalSpacing: itemSpacing,
verticalSpacing: lineSpacing,
justification: justified.justification,
distributeItemsEvenly: distributeItemsEvenly
)
)
case .vertical:
return AnyLayout(
VFlow(
alignment: horizontalAlignment.value,
itemSpacing: itemSpacing,
columnSpacing: lineSpacing,
horizontalAlignment: horizontalAlignment.value,
verticalAlignment: verticalAlignment.value,
horizontalSpacing: lineSpacing,
verticalSpacing: itemSpacing,
justification: justified.justification,
distributeItemsEvenly: distributeItemsEvenly
)
Expand Down
67 changes: 67 additions & 0 deletions Sources/Flow/HFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,40 @@ public struct HFlow<Content: View>: View {
)
}

/// Creates a horizontal flow with the given spacing and alignment.
///
/// - Parameters:
/// - horizonalAlignment: The guide for aligning the subviews horizontally.
/// - horizonalSpacing: The distance between subviews on the horizontal axis.
/// - verticalAlignment: The guide for aligning the subviews vertically.
/// - verticalSpacing: The distance between subviews on the vertical axis.
/// - justification: Whether the layout should fill the remaining
/// available space in each row by stretching either items or spaces.
/// - distributeItemsEvenly: Instead of prioritizing the first rows, this
/// mode tries to distribute items more evenly by minimizing the empty
/// spaces left in each row, while respecting their order.
/// - content: A view builder that creates the content of this flow.
@inlinable
public init(
horizontalAlignment: HorizontalAlignment,
verticalAlignment: VerticalAlignment,
horizontalSpacing: CGFloat? = nil,
verticalSpacing: CGFloat? = nil,
justification: Justification? = nil,
distributeItemsEvenly: Bool = false,
@ViewBuilder content contentBuilder: () -> Content
) {
content = contentBuilder()
layout = HFlowLayout(
horizontalAlignment: horizontalAlignment,
verticalAlignment: verticalAlignment,
horizontalSpacing: horizontalSpacing,
verticalSpacing: verticalSpacing,
justification: justification,
distributeItemsEvenly: distributeItemsEvenly
)
}

@inlinable
public var body: some View {
layout {
Expand Down Expand Up @@ -165,6 +199,39 @@ extension HFlow: Layout where Content == EmptyView {
}
}

/// Creates a horizontal flow with the given spacing and alignment.
///
/// - Parameters:
/// - horizonalAlignment: The guide for aligning the subviews horizontally.
/// - horizonalSpacing: The distance between subviews on the horizontal axis.
/// - verticalAlignment: The guide for aligning the subviews vertically.
/// - verticalSpacing: The distance between subviews on the vertical axis.
/// - justification: Whether the layout should fill the remaining
/// available space in each row by stretching either items or spaces.
/// - distributeItemsEvenly: Instead of prioritizing the first rows, this
/// mode tries to distribute items more evenly by minimizing the empty
/// spaces left in each row, while respecting their order.
@inlinable
public init(
horizontalAlignment: HorizontalAlignment,
verticalAlignment: VerticalAlignment,
horizontalSpacing: CGFloat? = nil,
verticalSpacing: CGFloat? = nil,
justification: Justification? = nil,
distributeItemsEvenly: Bool = false
) {
self.init(
horizontalAlignment: horizontalAlignment,
verticalAlignment: verticalAlignment,
horizontalSpacing: horizontalSpacing,
verticalSpacing: verticalSpacing,
justification: justification,
distributeItemsEvenly: distributeItemsEvenly
) {
EmptyView()
}
}

@inlinable
nonisolated public func sizeThatFits(
proposal: ProposedViewSize,
Expand Down
38 changes: 35 additions & 3 deletions Sources/Flow/HFlowLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,43 @@ public struct HFlowLayout: Sendable {
rowSpacing: CGFloat? = nil,
justification: Justification? = nil,
distributeItemsEvenly: Bool = false
) {
self.init(
horizontalAlignment: .leading,
verticalAlignment: alignment,
horizontalSpacing: itemSpacing,
verticalSpacing: rowSpacing,
justification: justification,
distributeItemsEvenly: distributeItemsEvenly
)
}

/// Creates a horizontal flow with the given spacing and alignment.
///
/// - Parameters:
/// - horizonalAlignment: The guide for aligning the subviews horizontally.
/// - horizonalSpacing: The distance between subviews on the horizontal axis.
/// - verticalAlignment: The guide for aligning the subviews vertically.
/// - verticalSpacing: The distance between subviews on the vertical axis.
/// - justification: Whether the layout should fill the remaining
/// available space in each row by stretching either items or spaces.
/// - distributeItemsEvenly: Instead of prioritizing the first rows, this
/// mode tries to distribute items more evenly by minimizing the empty
/// spaces left in each row, while respecting their order.
@inlinable
public init(
horizontalAlignment: HorizontalAlignment,
verticalAlignment: VerticalAlignment,
horizontalSpacing: CGFloat? = nil,
verticalSpacing: CGFloat? = nil,
justification: Justification? = nil,
distributeItemsEvenly: Bool = false
) {
layout = .horizontal(
alignment: alignment,
itemSpacing: itemSpacing,
lineSpacing: rowSpacing,
horizontalAlignment: horizontalAlignment,
verticalAlignment: verticalAlignment,
horizontalSpacing: horizontalSpacing,
verticalSpacing: verticalSpacing,
justification: justification,
distributeItemsEvenly: distributeItemsEvenly
)
Expand Down
70 changes: 46 additions & 24 deletions Sources/Flow/Internal/Layout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ struct FlowLayout: Sendable {
@usableFromInline
let distributeItemsEvenly: Bool
@usableFromInline
let align: @Sendable (any Dimensions) -> CGFloat
let alignmentOnBreadth: @Sendable (any Dimensions) -> CGFloat
@usableFromInline
let alignmentOnDepth: @Sendable (any Dimensions) -> CGFloat

@inlinable
init(
Expand All @@ -29,14 +31,16 @@ struct FlowLayout: Sendable {
lineSpacing: CGFloat? = nil,
justification: Justification? = nil,
distributeItemsEvenly: Bool = false,
align: @escaping @Sendable (any Dimensions) -> CGFloat
alignmentOnBreadth: @escaping @Sendable (any Dimensions) -> CGFloat,
alignmentOnDepth: @escaping @Sendable (any Dimensions) -> CGFloat
) {
self.axis = axis
self.itemSpacing = itemSpacing
self.lineSpacing = lineSpacing
self.justification = justification
self.distributeItemsEvenly = distributeItemsEvenly
self.align = align
self.alignmentOnBreadth = alignmentOnBreadth
self.alignmentOnDepth = alignmentOnDepth
}

private struct ItemWithSpacing<T> {
Expand Down Expand Up @@ -132,7 +136,7 @@ struct FlowLayout: Sendable {
let itemDepth = item.size.depth
if itemDepth > 0 {
let dimensions = item.item.subview.dimensions(proposedSize)
let alignedPosition = align(dimensions)
let alignedPosition = alignmentOnDepth(dimensions)
position.depth += (alignedPosition / itemDepth) * (lineDepth - itemDepth)
}
let point = CGPoint(size: position, axis: axis)
Expand Down Expand Up @@ -184,6 +188,7 @@ struct FlowLayout: Sendable {
}
updateFlexibleItems(in: &lines, proposedSize: proposedSize)
updateLineSpacings(in: &lines)
updateAlignment(in: &lines)
return lines
}

Expand Down Expand Up @@ -265,6 +270,21 @@ struct FlowLayout: Sendable {
}
}
}

private func updateAlignment(in lines: inout Lines) {
let breadth = lines.map { $0.item.sum { $0.leadingSpace + $0.size.breadth } }.max() ?? 0
for index in lines.indices where !lines[index].item.isEmpty {
lines[index].item[0].leadingSpace += determineLeadingSpace(in: lines[index], breadth: breadth)
}
}

private func determineLeadingSpace(in line: Lines.Element, breadth: CGFloat) -> CGFloat {
guard let item = line.item.first(where: { $0.item.cache.ideal.breadth > 0 })?.item else { return 0 }
let lineSize = line.item.sum { $0.leadingSpace + $0.size.breadth }
let value = alignmentOnBreadth(item.subview.dimensions(.unspecified)) / item.cache.ideal.breadth
let remainingSpace = breadth - lineSize
return value * remainingSpace
}
}

extension FlowLayout: Layout {
Expand All @@ -275,40 +295,42 @@ extension FlowLayout: Layout {

@inlinable
static func vertical(
alignment: HorizontalAlignment,
itemSpacing: CGFloat?,
lineSpacing: CGFloat?,
horizontalAlignment: HorizontalAlignment = .center,
verticalAlignment: VerticalAlignment = .top,
horizontalSpacing: CGFloat? = nil,
verticalSpacing: CGFloat? = nil,
justification: Justification? = nil,
distributeItemsEvenly: Bool = false
) -> FlowLayout {
.init(
self.init(
axis: .vertical,
itemSpacing: itemSpacing,
lineSpacing: lineSpacing,
itemSpacing: verticalSpacing,
lineSpacing: horizontalSpacing,
justification: justification,
distributeItemsEvenly: distributeItemsEvenly
) {
$0[alignment]
}
distributeItemsEvenly: distributeItemsEvenly,
alignmentOnBreadth: { $0[verticalAlignment] },
alignmentOnDepth: { $0[horizontalAlignment] }
)
}

@inlinable
static func horizontal(
alignment: VerticalAlignment,
itemSpacing: CGFloat?,
lineSpacing: CGFloat?,
horizontalAlignment: HorizontalAlignment = .leading,
verticalAlignment: VerticalAlignment = .center,
horizontalSpacing: CGFloat? = nil,
verticalSpacing: CGFloat? = nil,
justification: Justification? = nil,
distributeItemsEvenly: Bool = false
) -> FlowLayout {
.init(
self.init(
axis: .horizontal,
itemSpacing: itemSpacing,
lineSpacing: lineSpacing,
itemSpacing: horizontalSpacing,
lineSpacing: verticalSpacing,
justification: justification,
distributeItemsEvenly: distributeItemsEvenly
) {
$0[alignment]
}
distributeItemsEvenly: distributeItemsEvenly,
alignmentOnBreadth: { $0[horizontalAlignment] },
alignmentOnDepth: { $0[verticalAlignment] }
)
}
}

Expand Down
67 changes: 67 additions & 0 deletions Sources/Flow/VFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,40 @@ public struct VFlow<Content: View>: View {
)
}

/// Creates a vertical flow with the given spacing and alignment.
///
/// - Parameters:
/// - horizonalAlignment: The guide for aligning the subviews horizontally.
/// - horizonalSpacing: The distance between subviews on the horizontal axis.
/// - verticalAlignment: The guide for aligning the subviews vertically.
/// - verticalSpacing: The distance between subviews on the vertical axis.
/// - justification: Whether the layout should fill the remaining
/// available space in each column by stretching either items or spaces.
/// - distributeItemsEvenly: Instead of prioritizing the first columns, this
/// mode tries to distribute items more evenly by minimizing the empty
/// spaces left in each column, while respecting their order.
/// - content: A view builder that creates the content of this flow.
@inlinable
public init(
horizontalAlignment: HorizontalAlignment,
verticalAlignment: VerticalAlignment,
horizontalSpacing: CGFloat? = nil,
verticalSpacing: CGFloat? = nil,
justification: Justification? = nil,
distributeItemsEvenly: Bool = false,
@ViewBuilder content contentBuilder: () -> Content
) {
content = contentBuilder()
layout = VFlowLayout(
horizontalAlignment: horizontalAlignment,
verticalAlignment: verticalAlignment,
horizontalSpacing: horizontalSpacing,
verticalSpacing: verticalSpacing,
justification: justification,
distributeItemsEvenly: distributeItemsEvenly
)
}

public var body: some View {
layout {
content
Expand Down Expand Up @@ -164,6 +198,39 @@ extension VFlow: Layout where Content == EmptyView {
}
}

/// Creates a vertical flow with the given spacing and alignment.
///
/// - Parameters:
/// - horizonalAlignment: The guide for aligning the subviews horizontally.
/// - horizonalSpacing: The distance between subviews on the horizontal axis.
/// - verticalAlignment: The guide for aligning the subviews vertically.
/// - verticalSpacing: The distance between subviews on the vertical axis.
/// - justification: Whether the layout should fill the remaining
/// available space in each column by stretching either items or spaces.
/// - distributeItemsEvenly: Instead of prioritizing the first columns, this
/// mode tries to distribute items more evenly by minimizing the empty
/// spaces left in each column, while respecting their order.
@inlinable
public init(
horizontalAlignment: HorizontalAlignment,
verticalAlignment: VerticalAlignment,
horizontalSpacing: CGFloat? = nil,
verticalSpacing: CGFloat? = nil,
justification: Justification? = nil,
distributeItemsEvenly: Bool = false
) {
self.init(
horizontalAlignment: horizontalAlignment,
verticalAlignment: verticalAlignment,
horizontalSpacing: horizontalSpacing,
verticalSpacing: verticalSpacing,
justification: justification,
distributeItemsEvenly: distributeItemsEvenly
) {
EmptyView()
}
}

@inlinable
nonisolated public func sizeThatFits(
proposal: ProposedViewSize,
Expand Down
Loading

1 comment on commit b528bd0

@fred-bowker
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great, thanks for making this change

Please sign in to comment.