diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Block+Layout.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Block+Layout.swift index f7bbec9e0..3c8b6597c 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Block+Layout.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Block+Layout.swift @@ -30,6 +30,7 @@ public struct ContainerBlockLayout { let children: [ContainerBlock.Child] let gaps: [CGFloat] let layoutDirection: ContainerBlock.LayoutDirection + let layoutMode: ContainerBlock.LayoutMode let axialAlignment: Alignment let size: CGSize @@ -37,6 +38,7 @@ public struct ContainerBlockLayout { children: [ContainerBlock.Child], gaps: [CGFloat], layoutDirection: ContainerBlock.LayoutDirection, + layoutMode: ContainerBlock.LayoutMode, axialAlignment: Alignment, size: CGSize ) { @@ -44,12 +46,22 @@ public struct ContainerBlockLayout { self.children = children self.gaps = gaps self.layoutDirection = layoutDirection + self.layoutMode = layoutMode self.axialAlignment = axialAlignment self.size = size self.blockFrames = calculateBlockFrames() } private func calculateBlockFrames() -> [CGRect] { + switch layoutMode { + case .noWrap: + return calculateNoWrapLayoutFrames() + case .wrap: + return calculateWrapLayoutFrames() + } + } + + private func calculateNoWrapLayoutFrames() -> [CGRect] { var frames = [CGRect]() let gapsSize = gaps.reduce(0, +) var shift = CGPoint(x: 0, y: 0) @@ -57,10 +69,11 @@ public struct ContainerBlockLayout { case .horizontal: let horizontallyResizableBlocks = children.map { $0.content } .filter { $0.isHorizontallyResizable } - let widthOfHorizontallyNonResizableBlocks = widthsOfHorizontallyNonResizableBlocks.reduce( - 0, - + - ) + let widthOfHorizontallyNonResizableBlocks = widthsOfHorizontallyNonResizableBlocks + .reduce( + 0, + + + ) let widthAvailableForResizableBlocks = ( size.width - widthOfHorizontallyNonResizableBlocks - gapsSize ) @@ -134,6 +147,7 @@ public struct ContainerBlockLayout { } shift.y = axialAlignment.offset(forAvailableSpace: size.height, contentSize: y) } + return frames.map { let frame = $0.offset(by: shift).roundedToScreenScale precondition(frame.isValidAndFinite) @@ -141,6 +155,77 @@ public struct ContainerBlockLayout { } } + private func calculateWrapLayoutFrames() -> [CGRect] { + #if INTERNAL_BUILD + assert(gaps.allSatisfy(0), "You cannot use gaps in wrap container.") + assert( + !children + .contains { $0.content.isHorizontallyResizable || $0.content.isVerticallyResizable + }, + "You cannot use resizable blocks in wrap container." + ) + #endif + var frames = [CGRect]() + let buildingDirectionKeyPath: KeyPath + let transferDirectionKeyPath: KeyPath + + switch layoutDirection { + case .horizontal: + buildingDirectionKeyPath = \.width + transferDirectionKeyPath = \.height + case .vertical: + buildingDirectionKeyPath = \.height + transferDirectionKeyPath = \.width + } + + let groups: [[(child: ContainerBlock.Child, childSize: CGSize, lineOffset: CGFloat)]] = + children.reduce([[]]) { result, child in + let offset = (result.last?.last?.2 ?? 0) + + (result.last?.last?.1[keyPath: buildingDirectionKeyPath] ?? 0) + let childSize = child.content.size(forResizableBlockSize: .zero) + if offset + childSize[keyPath: buildingDirectionKeyPath] > + size[keyPath: buildingDirectionKeyPath] { + return result + [[(child, childSize, 0)]] + } else { + return (result.dropLast()) + [(result.last ?? []) + [(child, childSize, offset)]] + } + } + + var currentLineOffset = 0.0 + groups.forEach { + let groupHeight = $0.map(\.childSize).map(to: transferDirectionKeyPath).max() ?? 0 + let contentLength = $0.map(\.childSize).map(to: buildingDirectionKeyPath).reduce(0, +) + + $0.forEach { + let alignmentSpace = groupHeight - $0.childSize[keyPath: transferDirectionKeyPath] + let alignedLineOffset = currentLineOffset + $0.child.crossAlignment + .offset(forAvailableSpace: alignmentSpace) + let alignedElementOffset = $0.lineOffset.advanced(by: axialAlignment.offset( + forAvailableSpace: size[keyPath: buildingDirectionKeyPath], + contentSize: contentLength + ).roundedToScreenScale) + + switch layoutDirection { + case .horizontal: + frames + .append(CGRect(origin: CGPoint( + x: alignedElementOffset, + y: alignedLineOffset + ), size: $0.childSize)) + case .vertical: + frames + .append(CGRect(origin: CGPoint( + x: alignedLineOffset, + y: alignedElementOffset + ), size: $0.childSize)) + } + } + + currentLineOffset += groupHeight + } + return frames + } + public private(set) var blockFrames: [CGRect] = [] public var leftInset: CGFloat { diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift index 831540126..b3c0ddc7b 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift @@ -18,6 +18,11 @@ public final class ContainerBlock: BlockWithLayout { /// Child blocks are laid out vertically one after another case vertical } + + public enum LayoutMode { + case wrap + case noWrap + } public struct Child: Equatable { public var content: Block @@ -44,6 +49,7 @@ public final class ContainerBlock: BlockWithLayout { } public let layoutDirection: LayoutDirection + public let layoutMode: LayoutMode public let widthTrait: LayoutTrait public let heightTrait: LayoutTrait public let axialAlignment: Alignment @@ -59,6 +65,7 @@ public final class ContainerBlock: BlockWithLayout { public init( layoutDirection: LayoutDirection, + layoutMode: LayoutMode = .noWrap, widthTrait: LayoutTrait = .resizable, heightTrait: LayoutTrait = .intrinsic, axialAlignment: Alignment = .leading, @@ -80,6 +87,7 @@ public final class ContainerBlock: BlockWithLayout { } self.layoutDirection = layoutDirection + self.layoutMode = layoutMode self.widthTrait = widthTrait self.heightTrait = heightTrait self.axialAlignment = axialAlignment @@ -273,6 +281,7 @@ public final class ContainerBlock: BlockWithLayout { children: children, gaps: gaps, layoutDirection: layoutDirection, + layoutMode: layoutMode, axialAlignment: axialAlignment, size: size ) diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockExtensions.swift b/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockExtensions.swift index 11d4c7e8d..c3be779e5 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockExtensions.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockExtensions.swift @@ -6,6 +6,7 @@ import CommonCore extension ContainerBlock { public func modifying( layoutDirection: LayoutDirection? = nil, + layoutMode: LayoutMode? = nil, widthTrait: LayoutTrait? = nil, heightTrait: LayoutTrait? = nil, axialAlignment: Alignment? = nil, @@ -19,6 +20,7 @@ extension ContainerBlock { ) throws -> ContainerBlock { try ContainerBlock( layoutDirection: layoutDirection ?? self.layoutDirection, + layoutMode: layoutMode ?? self.layoutMode, widthTrait: widthTrait ?? self.widthTrait, heightTrait: heightTrait ?? self.heightTrait, axialAlignment: axialAlignment ?? self.axialAlignment, @@ -34,6 +36,7 @@ extension ContainerBlock { public convenience init( layoutDirection: LayoutDirection, + layoutMode: LayoutMode = .noWrap, widthTrait: LayoutTrait = .resizable, heightTrait: LayoutTrait = .intrinsic, horizontalChildrenAlignment: Alignment = .leading, @@ -64,6 +67,7 @@ extension ContainerBlock { try self.init( layoutDirection: layoutDirection, + layoutMode: layoutMode, widthTrait: widthTrait, heightTrait: heightTrait, axialAlignment: axialAlignment, diff --git a/client/ios/LayoutKit/LayoutKit/Blocks/ImageRenderableBlock/ContainerBlock+ImageRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/Blocks/ImageRenderableBlock/ContainerBlock+ImageRenderableBlock.swift index 565d10740..a1cf937f3 100644 --- a/client/ios/LayoutKit/LayoutKit/Blocks/ImageRenderableBlock/ContainerBlock+ImageRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/Blocks/ImageRenderableBlock/ContainerBlock+ImageRenderableBlock.swift @@ -9,6 +9,7 @@ extension ContainerBlock: ImageRenderableBlock { children: children, gaps: gaps, layoutDirection: layoutDirection, + layoutMode: layoutMode, axialAlignment: axialAlignment, size: rect.size ).blockFrames diff --git a/client/ios/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift b/client/ios/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift index 61684b445..264bd8eb4 100644 --- a/client/ios/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift +++ b/client/ios/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift @@ -26,6 +26,7 @@ extension ContainerBlock { children: children, gaps: gaps, layoutDirection: layoutDirection, + layoutMode: layoutMode, axialAlignment: axialAlignment, contentAnimation: contentAnimation, anchorPoint: anchorPoint, @@ -49,6 +50,7 @@ private final class ContainerBlockView: UIView, BlockViewProtocol, VisibleBounds let children: [ContainerBlock.Child] let gaps: [CGFloat] let layoutDirection: ContainerBlock.LayoutDirection + let layoutMode: ContainerBlock.LayoutMode let axialAlignment: Alignment let contentAnimation: BlockAnimation? let anchorPoint: AnchorPoint @@ -131,6 +133,7 @@ private final class ContainerBlockView: UIView, BlockViewProtocol, VisibleBounds children: model.children, gaps: model.gaps, layoutDirection: model.layoutDirection, + layoutMode: model.layoutMode, axialAlignment: model.axialAlignment, size: bounds.size ) diff --git a/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockTests+Layout.swift b/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockTests+Layout.swift index 61096ff3e..41c9bd861 100644 --- a/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockTests+Layout.swift +++ b/client/ios/LayoutKit/LayoutKitTests/Blocks/BlockTests+Layout.swift @@ -17,6 +17,7 @@ extension ContainerBlockLayout { children: blocks.map { .init(content: $0, crossAlignment: crossAlignment) }, gaps: gaps, layoutDirection: layoutDirection, + layoutMode: .noWrap, axialAlignment: axialAlignment, size: size )