From 6169d172857c1e86111c7482bd8d074928689d08 Mon Sep 17 00:00:00 2001 From: Laszlo Teveli Date: Fri, 28 Jun 2024 11:43:45 +0200 Subject: [PATCH] Refactor line breaking --- Sources/Flow/Internal/Layout.swift | 49 +++-------- Sources/Flow/Internal/LineBreaking.swift | 105 ++++++++++++----------- Sources/Flow/Support.swift | 2 + 3 files changed, 72 insertions(+), 84 deletions(-) diff --git a/Sources/Flow/Internal/Layout.swift b/Sources/Flow/Internal/Layout.swift index 01bc66b0..0bfe32e4 100644 --- a/Sources/Flow/Internal/Layout.swift +++ b/Sources/Flow/Internal/Layout.swift @@ -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) @@ -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(item: [], size: .zero) for index in start ..< end { @@ -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) { diff --git a/Sources/Flow/Internal/LineBreaking.swift b/Sources/Flow/Internal/LineBreaking.swift index dc83dce4..1590f454 100644 --- a/Sources/Flow/Internal/LineBreaking.swift +++ b/Sources/Flow/Internal/LineBreaking.swift @@ -1,57 +1,66 @@ 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 +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] = [0] + 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 } } + breakpoints.append(sizes.endIndex) + + return breakpoints } +} + +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) - return breaks + 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 + } + } + } + + 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 + } } diff --git a/Sources/Flow/Support.swift b/Sources/Flow/Support.swift index e9551e0b..4bce3975 100644 --- a/Sources/Flow/Support.swift +++ b/Sources/Flow/Support.swift @@ -10,6 +10,7 @@ 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 @@ -17,6 +18,7 @@ public enum Justification { } } + @inlinable var isStretchingSpaces: Bool { switch self { case .stretchSpaces, .stretchItemsAndSpaces: true