Skip to content

Commit

Permalink
Refactor line breaking
Browse files Browse the repository at this point in the history
  • Loading branch information
tevelee committed Jun 28, 2024
1 parent 3f6bd56 commit ec576d1
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 83 deletions.
49 changes: 13 additions & 36 deletions Sources/Flow/Internal/Layout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,6 @@ struct FlowLayout {
of subviews: some Subviews,
cache: FlowLayoutCache
) -> Lines {
var lines: Lines = []
let proposedBreadth = proposedSize.replacingUnspecifiedDimensions().value(on: axis)

let sizes = cache.subviewsCache.map(\.ideal)
let spacings = if let itemSpacing {
[0] + Array(repeating: itemSpacing, count: subviews.count - 1)
Expand All @@ -130,40 +127,19 @@ struct FlowLayout {
}
}

for index in subviews.indices {
let (subview, size, spacing, cache) = (subviews[index], sizes[index], spacings[index], cache.subviewsCache[index])
if let lastIndex = lines.indices.last {
let additionalBreadth = spacing + size.breadth
if lines[lastIndex].size.breadth + additionalBreadth <= proposedBreadth {
lines[lastIndex].append((subview, cache), size: size, spacing: spacing)
continue
}
}
lines.append(.init(item: [.init(item: (subview: subview, cache: cache), size: size)], size: size))
let lineBreaker: LineBreaking = if distributeItemsEvenly {
KnuthPlassLineBreaker()
} else {
FlowLineBreaker()
}
distributeItems(in: &lines, proposedSize: proposedSize, subviews: subviews, sizes: sizes, spacings: spacings, cache: cache)
updateFlexibleItems(in: &lines, proposedSize: proposedSize)
updateLineSpacings(in: &lines)
return lines
}

private func distributeItems(
in lines: inout Lines,
proposedSize: ProposedViewSize,
subviews: some Subviews,
sizes: [Size],
spacings: [CGFloat],
cache: FlowLayoutCache
) {
guard distributeItemsEvenly else { return }

let breakpoints = knuthPlassLineBreakingAlgorithm(
proposedBreadth: proposedSize.replacingUnspecifiedDimensions().value(on: axis),
sizes: sizes,
spacings: spacings
let breakpoints = lineBreaker.wrapItemsToLines(
sizes: sizes.map(\.breadth),
spacings: spacings,
in: proposedSize.replacingUnspecifiedDimensions().value(on: axis)
)

var newLines: Lines = []
var lines: Lines = []
for (start, end) in breakpoints.adjacentPairs() {
var line = ItemWithSpacing<Line>(item: [], size: .zero)
for index in start ..< end {
Expand All @@ -172,10 +148,11 @@ struct FlowLayout {
let spacing = index == start ? 0 : spacings[index] // Reset spacing for the first item in each line
line.append((subview, cache.subviewsCache[index]), size: size, spacing: spacing)
}
newLines.append(line)
lines.append(line)
}

lines = newLines
updateFlexibleItems(in: &lines, proposedSize: proposedSize)
updateLineSpacings(in: &lines)
return lines
}

private func updateFlexibleItems(in lines: inout Lines, proposedSize: ProposedViewSize) {
Expand Down
114 changes: 67 additions & 47 deletions Sources/Flow/Internal/LineBreaking.swift
Original file line number Diff line number Diff line change
@@ -1,57 +1,77 @@
import CoreFoundation

@inlinable
func knuthPlassLineBreakingAlgorithm(
proposedBreadth: CGFloat,
sizes: [Size],
spacings: [CGFloat]
) -> [Int] {
let breaks = calculateOptimalBreaks(
proposedBreadth: proposedBreadth,
sizes: sizes,
spacings: spacings
)

var breakpoints: [Int] = []
var i = sizes.count
while let breakPoint = breaks[i] {
breakpoints.insert(i, at: 0)
i = breakPoint
}
breakpoints.insert(0, at: 0)
return breakpoints
@usableFromInline
protocol LineBreaking {
@inlinable
func wrapItemsToLines(sizes: [CGFloat], spacings: [CGFloat], in availableSpace: CGFloat) -> [Int]
}

@usableFromInline
func calculateOptimalBreaks(
proposedBreadth: CGFloat,
sizes: [Size],
spacings: [CGFloat]
) -> [Int?] {
let count = sizes.count
var costs: [CGFloat] = Array(repeating: .infinity, count: count + 1)
var breaks: [Int?] = Array(repeating: nil, count: count + 1)

costs[0] = 0

for end in 1 ... count {
var totalBreadth: CGFloat = 0
for start in (0 ..< end).reversed() {
let size = sizes[start].breadth
let spacing = (end - start) == 1 ? 0 : spacings[start + 1]
totalBreadth += size + spacing
if totalBreadth > proposedBreadth {
break
}
let remainingSpace = proposedBreadth - totalBreadth
let bias = CGFloat(count - end) * 0.5 // Introduce a small bias to prefer breaks that fill earlier lines more
let cost = costs[start] + remainingSpace * remainingSpace + bias
if cost < costs[end] {
costs[end] = cost
breaks[end] = start
struct FlowLineBreaker: LineBreaking {
@inlinable
func wrapItemsToLines(sizes: [CGFloat], spacings: [CGFloat], in availableSpace: CGFloat) -> [Int] {
var breakpoints: [Int] = []
var currentLineSize: CGFloat = 0

for (index, size) in sizes.enumerated() {
let requiredSpace = spacings[index] + size
if currentLineSize + requiredSpace > availableSpace {
breakpoints.append(index)
currentLineSize = size
} else {
currentLineSize += requiredSpace
}
}

if breakpoints.first != 0 {
breakpoints.insert(0, at: 0)
}
breakpoints.append(sizes.endIndex)

return breakpoints
}
}

@usableFromInline
struct KnuthPlassLineBreaker: LineBreaking {
@inlinable
func wrapItemsToLines(sizes: [CGFloat], spacings: [CGFloat], in availableSpace: CGFloat) -> [Int] {
let count = sizes.count
var costs: [CGFloat] = Array(repeating: .infinity, count: count + 1)
var breaks: [Int?] = Array(repeating: nil, count: count + 1)

costs[0] = 0

for end in 1 ... count {
var totalBreadth: CGFloat = 0
for start in (0 ..< end).reversed() {
let size = sizes[start]
let spacing = (end - start) == 1 ? 0 : spacings[start + 1]
totalBreadth += size + spacing
if totalBreadth > availableSpace {
break
}
let remainingSpace = availableSpace - totalBreadth
let bias = CGFloat(count - end) * 0.5 // Introduce a small bias to prefer breaks that fill earlier lines more
let cost = costs[start] + remainingSpace * remainingSpace + bias
if cost < costs[end] {
costs[end] = cost
breaks[end] = start
}
}
}

return breaks
if breaks.compactMap({ $0 }).isEmpty {
return Array(0 ... sizes.endIndex)
}

var breakpoints: [Int] = []
var i = sizes.count
while let breakPoint = breaks[i] {
breakpoints.insert(i, at: 0)
i = breakPoint
}
breakpoints.insert(0, at: 0)
return breakpoints
}
}
2 changes: 2 additions & 0 deletions Sources/Flow/Support.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ public enum Justification {
/// Primarily the items are being stretched as much as they allow and then spaces too if needed
case stretchItemsAndSpaces

@inlinable
var isStretchingItems: Bool {
switch self {
case .stretchItems, .stretchItemsAndSpaces: true
case .stretchSpaces: false
}
}

@inlinable
var isStretchingSpaces: Bool {
switch self {
case .stretchSpaces, .stretchItemsAndSpaces: true
Expand Down

0 comments on commit ec576d1

Please sign in to comment.