From c8e85a78385b221387efeca125ee51e36898873b Mon Sep 17 00:00:00 2001 From: Laszlo Teveli Date: Wed, 4 Dec 2024 17:42:25 +0100 Subject: [PATCH] Line breaks --- Sources/Flow/Internal/Layout.swift | 3 ++- Sources/Flow/Internal/LineBreaking.swift | 31 +++++++++++++++++++++--- Sources/Flow/Internal/Protocols.swift | 1 + Sources/Flow/Support.swift | 29 ++++++++++++++++++++++ 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/Sources/Flow/Internal/Layout.swift b/Sources/Flow/Internal/Layout.swift index bffcd7d6..67e08eff 100644 --- a/Sources/Flow/Internal/Layout.swift +++ b/Sources/Flow/Internal/Layout.swift @@ -172,6 +172,7 @@ struct FlowLayout: Sendable { let breakpoints: [Int] = lineBreaker.wrapItemsToLines( sizes: sizes.map(\.breadth), spacings: spacings, + lineBreaks: cache.subviewsCache.enumerated().filter(\.element.shouldStartInNewLine).map(\.offset), in: proposedSize.replacingUnspecifiedDimensions(by: .infinity).value(on: axis) ) @@ -181,7 +182,7 @@ struct FlowLayout: Sendable { for index in start ..< end { let subview = subviews[index] let size = sizes[index] - let spacing = index == start ? 0 : spacings[index] // Reset spacing for the first item in each line + let spacing = index == start || cache.subviewsCache[index - 1].isLineBreak ? 0 : spacings[index] // Reset spacing for the first item in each line line.append((subview, cache.subviewsCache[index]), size: size, spacing: spacing) } lines.append(line) diff --git a/Sources/Flow/Internal/LineBreaking.swift b/Sources/Flow/Internal/LineBreaking.swift index c861df14..db7a7dc3 100644 --- a/Sources/Flow/Internal/LineBreaking.swift +++ b/Sources/Flow/Internal/LineBreaking.swift @@ -3,7 +3,7 @@ import CoreFoundation @usableFromInline protocol LineBreaking { @inlinable - func wrapItemsToLines(sizes: [CGFloat], spacings: [CGFloat], in availableSpace: CGFloat) -> [Int] + func wrapItemsToLines(sizes: [CGFloat], spacings: [CGFloat], lineBreaks: [Int], in availableSpace: CGFloat) -> [Int] } @usableFromInline @@ -12,13 +12,13 @@ struct FlowLineBreaker: LineBreaking { init() {} @inlinable - func wrapItemsToLines(sizes: [CGFloat], spacings: [CGFloat], in availableSpace: CGFloat) -> [Int] { + func wrapItemsToLines(sizes: [CGFloat], spacings: [CGFloat], lineBreaks: [Int], 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 { + if currentLineSize + requiredSpace > availableSpace || lineBreaks.contains(index) { breakpoints.append(index) currentLineSize = size } else { @@ -40,8 +40,31 @@ struct KnuthPlassLineBreaker: LineBreaking { @inlinable init() {} + @inlinable + func wrapItemsToLines(sizes: [CGFloat], spacings: [CGFloat], lineBreaks: [Int], in availableSpace: CGFloat) -> [Int] { + if lineBreaks.isEmpty { + return wrapItemsToLines(sizes: sizes, spacings: spacings, in: availableSpace) + } + var result: [Int] = [] + var start: Int = 0 + for lineBreak in lineBreaks + [sizes.endIndex] { + let partial = wrapItemsToLines( + sizes: Array(sizes[start.. [Int] { + if sizes.isEmpty { + return [] + } let count = sizes.count var costs: [CGFloat] = Array(repeating: .infinity, count: count + 1) var breaks: [Int?] = Array(repeating: nil, count: count + 1) @@ -68,7 +91,7 @@ struct KnuthPlassLineBreaker: LineBreaking { } if breaks.compactMap({ $0 }).isEmpty { - return Array(0 ... sizes.endIndex) + return [0, sizes.endIndex] } var breakpoints: [Int] = [] diff --git a/Sources/Flow/Internal/Protocols.swift b/Sources/Flow/Internal/Protocols.swift index 86705df6..10ebeb3b 100644 --- a/Sources/Flow/Internal/Protocols.swift +++ b/Sources/Flow/Internal/Protocols.swift @@ -12,6 +12,7 @@ protocol Subview { func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize func dimensions(_ proposal: ProposedViewSize) -> any Dimensions func place(at position: CGPoint, anchor: UnitPoint, proposal: ProposedViewSize) + subscript(key: K.Type) -> K.Value { get } } extension LayoutSubview: Subview { diff --git a/Sources/Flow/Support.swift b/Sources/Flow/Support.swift index 1cc6ec1e..13979f3e 100644 --- a/Sources/Flow/Support.swift +++ b/Sources/Flow/Support.swift @@ -37,6 +37,8 @@ public struct FlowLayoutCache { var min: Size var ideal: Size var max: Size + var shouldStartInNewLine: Bool + var isLineBreak: Bool @usableFromInline init(_ subview: some Subview, axis: Axis) { @@ -45,6 +47,8 @@ public struct FlowLayoutCache { min = subview.dimensions(.zero).size(on: axis) ideal = subview.dimensions(.unspecified).size(on: axis) max = subview.dimensions(.infinity).size(on: axis) + shouldStartInNewLine = subview[ShouldStartInNewLine.self] + isLineBreak = subview[ShouldStartInNewLine.self] } } @@ -58,3 +62,28 @@ public struct FlowLayoutCache { } } } + +public struct LineBreak: View { + public var body: some View { + Color.clear + .frame(width: 0, height: 0) + .layoutValue(key: IsLineBreak.self, value: true) + .startInNewLine() + } + + public init() {} +} + +struct ShouldStartInNewLine: LayoutValueKey { + static let defaultValue = false +} + +struct IsLineBreak: LayoutValueKey { + static let defaultValue = false +} + +extension View { + public func startInNewLine() -> some View { + layoutValue(key: ShouldStartInNewLine.self, value: true) + } +}