Merge pull request #11 from phranck/feature/parameter-packs-container-cleanup

refactor: Parameter Packs, Container cleanup, precompiled ANSI Regex (A.8, A.10, H.7)
This commit is contained in:
phranck
2026-01-30 15:11:31 +01:00
10 changed files with 61 additions and 435 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ let package = Package(
// Minimum deployment targets for Apple platforms
// Linux is automatically supported (no platform specification needed)
platforms: [
.macOS(.v13)
.macOS(.v14)
],
products: [
.library(
+18 -131
View File
@@ -1,143 +1,30 @@
// swiftlint:disable large_tuple
//
// TupleViews.swift
// TUIKit
//
// Container types for multiple views in ViewBuilder.
// Container type for multiple views in ViewBuilder, using Swift Parameter Packs.
//
// MARK: - TupleView2
/// A view that contains multiple child views packed via a parameter pack.
///
/// `TupleView` replaces the previous `TupleView2` through `TupleView10`
/// types with a single generic struct using Swift Parameter Packs (SE-0393).
/// This removes the 10-child limit and eliminates ~400 lines of boilerplate.
///
/// `TupleView` is created automatically by `ViewBuilder` when multiple
/// views appear in a `@ViewBuilder` closure.
public struct TupleView<each V: View>: View {
/// The packed child views.
public let children: (repeat each V)
/// A view that contains two child views.
public struct TupleView2<V0: View, V1: View>: View {
public let value: (V0, V1)
public init(_ v0: V0, _ v1: V1) {
self.value = (v0, v1)
/// Creates a tuple view from a parameter pack of child views.
///
/// - Parameter children: The child views.
public init(_ children: repeat each V) {
self.children = (repeat each children)
}
public var body: Never {
fatalError("TupleView2 renders its children directly")
fatalError("TupleView renders its children directly")
}
}
// MARK: - TupleView3
/// A view that contains three child views.
public struct TupleView3<V0: View, V1: View, V2: View>: View {
public let value: (V0, V1, V2)
public init(_ v0: V0, _ v1: V1, _ v2: V2) {
self.value = (v0, v1, v2)
}
public var body: Never {
fatalError("TupleView3 renders its children directly")
}
}
// MARK: - TupleView4
/// A view that contains four child views.
public struct TupleView4<V0: View, V1: View, V2: View, V3: View>: View {
public let value: (V0, V1, V2, V3)
public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3) {
self.value = (v0, v1, v2, v3)
}
public var body: Never {
fatalError("TupleView4 renders its children directly")
}
}
// MARK: - TupleView5
/// A view that contains five child views.
public struct TupleView5<V0: View, V1: View, V2: View, V3: View, V4: View>: View {
public let value: (V0, V1, V2, V3, V4)
public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4) {
self.value = (v0, v1, v2, v3, v4)
}
public var body: Never {
fatalError("TupleView5 renders its children directly")
}
}
// MARK: - TupleView6
/// A view that contains six child views.
public struct TupleView6<V0: View, V1: View, V2: View, V3: View, V4: View, V5: View>: View {
public let value: (V0, V1, V2, V3, V4, V5)
public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4, _ v5: V5) {
self.value = (v0, v1, v2, v3, v4, v5)
}
public var body: Never {
fatalError("TupleView6 renders its children directly")
}
}
// MARK: - TupleView7
/// A view that contains seven child views.
public struct TupleView7<V0: View, V1: View, V2: View, V3: View, V4: View, V5: View, V6: View>: View {
public let value: (V0, V1, V2, V3, V4, V5, V6)
public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4, _ v5: V5, _ v6: V6) {
self.value = (v0, v1, v2, v3, v4, v5, v6)
}
public var body: Never {
fatalError("TupleView7 renders its children directly")
}
}
// MARK: - TupleView8
/// A view that contains eight child views.
public struct TupleView8<V0: View, V1: View, V2: View, V3: View, V4: View, V5: View, V6: View, V7: View>: View {
public let value: (V0, V1, V2, V3, V4, V5, V6, V7)
public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4, _ v5: V5, _ v6: V6, _ v7: V7) {
self.value = (v0, v1, v2, v3, v4, v5, v6, v7)
}
public var body: Never {
fatalError("TupleView8 renders its children directly")
}
}
// MARK: - TupleView9
/// A view that contains nine child views.
public struct TupleView9<V0: View, V1: View, V2: View, V3: View, V4: View, V5: View, V6: View, V7: View, V8: View>: View {
public let value: (V0, V1, V2, V3, V4, V5, V6, V7, V8)
public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4, _ v5: V5, _ v6: V6, _ v7: V7, _ v8: V8) {
self.value = (v0, v1, v2, v3, v4, v5, v6, v7, v8)
}
public var body: Never {
fatalError("TupleView9 renders its children directly")
}
}
// MARK: - TupleView10
/// A view that contains ten child views.
public struct TupleView10<V0: View, V1: View, V2: View, V3: View, V4: View, V5: View, V6: View, V7: View, V8: View, V9: View>: View {
public let value: (V0, V1, V2, V3, V4, V5, V6, V7, V8, V9)
public init(_ v0: V0, _ v1: V1, _ v2: V2, _ v3: V3, _ v4: V4, _ v5: V5, _ v6: V6, _ v7: V7, _ v8: V8, _ v9: V9) {
self.value = (v0, v1, v2, v3, v4, v5, v6, v7, v8, v9)
}
public var body: Never {
fatalError("TupleView10 renders its children directly")
}
}
// swiftlint:enable large_tuple
+10 -110
View File
@@ -1,4 +1,3 @@
// swiftlint:disable function_parameter_count
//
// ViewBuilder.swift
// TUIKit
@@ -22,7 +21,7 @@
///
/// The builder supports:
/// - Single views
/// - Multiple views (up to 10)
/// - Multiple views (unlimited, via Parameter Packs)
/// - Conditionals (`if`, `if-else`)
/// - Optional views (`if let`)
/// - Arrays of views (`for-in`)
@@ -36,114 +35,16 @@ public struct ViewBuilder {
content
}
// MARK: - Multiple Views (Tuple Views)
// MARK: - Multiple Views (Parameter Pack)
/// Builds two views into a TupleView.
public static func buildBlock<C0: View, C1: View>(
_ c0: C0,
_ c1: C1
) -> TupleView2<C0, C1> {
TupleView2(c0, c1)
}
/// Builds three views into a TupleView.
public static func buildBlock<C0: View, C1: View, C2: View>(
_ c0: C0,
_ c1: C1,
_ c2: C2
) -> TupleView3<C0, C1, C2> {
TupleView3(c0, c1, c2)
}
/// Builds four views into a TupleView.
public static func buildBlock<C0: View, C1: View, C2: View, C3: View>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3
) -> TupleView4<C0, C1, C2, C3> {
TupleView4(c0, c1, c2, c3)
}
/// Builds five views into a TupleView.
public static func buildBlock<C0: View, C1: View, C2: View, C3: View, C4: View>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4
) -> TupleView5<C0, C1, C2, C3, C4> {
TupleView5(c0, c1, c2, c3, c4)
}
/// Builds six views into a TupleView.
public static func buildBlock<C0: View, C1: View, C2: View, C3: View, C4: View, C5: View>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4,
_ c5: C5
) -> TupleView6<C0, C1, C2, C3, C4, C5> {
TupleView6(c0, c1, c2, c3, c4, c5)
}
/// Builds seven views into a TupleView.
public static func buildBlock<C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4,
_ c5: C5,
_ c6: C6
) -> TupleView7<C0, C1, C2, C3, C4, C5, C6> {
TupleView7(c0, c1, c2, c3, c4, c5, c6)
}
/// Builds eight views into a TupleView.
public static func buildBlock<C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4,
_ c5: C5,
_ c6: C6,
_ c7: C7
) -> TupleView8<C0, C1, C2, C3, C4, C5, C6, C7> {
TupleView8(c0, c1, c2, c3, c4, c5, c6, c7)
}
/// Builds nine views into a TupleView.
public static func buildBlock<C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4,
_ c5: C5,
_ c6: C6,
_ c7: C7,
_ c8: C8
) -> TupleView9<C0, C1, C2, C3, C4, C5, C6, C7, C8> {
TupleView9(c0, c1, c2, c3, c4, c5, c6, c7, c8)
}
/// Builds ten views into a TupleView.
public static func buildBlock<C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View, C9: View>(
_ c0: C0,
_ c1: C1,
_ c2: C2,
_ c3: C3,
_ c4: C4,
_ c5: C5,
_ c6: C6,
_ c7: C7,
_ c8: C8,
_ c9: C9
) -> TupleView10<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9> {
TupleView10(c0, c1, c2, c3, c4, c5, c6, c7, c8, c9)
/// Builds multiple views into a TupleView using Swift Parameter Packs.
///
/// This single overload replaces the previous 9 arity-specific `buildBlock`
/// overloads (`TupleView2` through `TupleView10`), removing the 10-child limit.
public static func buildBlock<each C: View>(
_ content: repeat each C
) -> TupleView<repeat each C> {
TupleView(repeat each content)
}
// MARK: - Conditionals
@@ -191,4 +92,3 @@ public struct ViewBuilder {
expression
}
}
// swiftlint:enable function_parameter_count
+1 -5
View File
@@ -15,11 +15,7 @@ extension String {
/// The string with all ANSI escape codes removed.
var stripped: String {
replacingOccurrences(
of: ANSIRenderer.ansiPattern,
with: "",
options: .regularExpression
)
replacing(ANSIRenderer.ansiRegex, with: "")
}
/// Pads the string to the specified visible width using spaces.
+6 -2
View File
@@ -22,8 +22,12 @@ public enum ANSIRenderer {
/// Dim/faint text style code.
public static let dim = "\(csi)2m"
/// Regex pattern that matches any ANSI escape sequence.
public static let ansiPattern = "\u{1B}\\[[0-9;]*[a-zA-Z]"
/// Precompiled regex that matches any ANSI escape sequence.
///
/// Used by `String.stripped` and `String.strippedLength` to remove
/// formatting codes for visible-width calculations. Compiling once
/// avoids per-call overhead in the hot rendering path.
public static nonisolated(unsafe) let ansiRegex = /\u{1B}\[[0-9;]*[a-zA-Z]/
// MARK: - SGR Style Codes
+4 -145
View File
@@ -227,156 +227,15 @@ extension ZStack: Renderable {
// MARK: - TupleView Rendering + ChildInfoProvider
extension TupleView2: Renderable, ChildInfoProvider {
extension TupleView: Renderable, ChildInfoProvider {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer))
}
func childInfos(context: RenderContext) -> [ChildInfo] {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
]
}
}
extension TupleView3: Renderable, ChildInfoProvider {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer))
}
func childInfos(context: RenderContext) -> [ChildInfo] {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
]
}
}
extension TupleView4: Renderable, ChildInfoProvider {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer))
}
func childInfos(context: RenderContext) -> [ChildInfo] {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.3, context: context),
]
}
}
extension TupleView5: Renderable, ChildInfoProvider {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer))
}
func childInfos(context: RenderContext) -> [ChildInfo] {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.3, context: context),
makeChildInfo(for: value.4, context: context),
]
}
}
extension TupleView6: Renderable, ChildInfoProvider {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer))
}
func childInfos(context: RenderContext) -> [ChildInfo] {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.3, context: context),
makeChildInfo(for: value.4, context: context),
makeChildInfo(for: value.5, context: context),
]
}
}
extension TupleView7: Renderable, ChildInfoProvider {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer))
}
func childInfos(context: RenderContext) -> [ChildInfo] {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.3, context: context),
makeChildInfo(for: value.4, context: context),
makeChildInfo(for: value.5, context: context),
makeChildInfo(for: value.6, context: context),
]
}
}
extension TupleView8: Renderable, ChildInfoProvider {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer))
}
func childInfos(context: RenderContext) -> [ChildInfo] {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.3, context: context),
makeChildInfo(for: value.4, context: context),
makeChildInfo(for: value.5, context: context),
makeChildInfo(for: value.6, context: context),
makeChildInfo(for: value.7, context: context),
]
}
}
extension TupleView9: Renderable, ChildInfoProvider {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer))
}
func childInfos(context: RenderContext) -> [ChildInfo] {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.3, context: context),
makeChildInfo(for: value.4, context: context),
makeChildInfo(for: value.5, context: context),
makeChildInfo(for: value.6, context: context),
makeChildInfo(for: value.7, context: context),
makeChildInfo(for: value.8, context: context),
]
}
}
extension TupleView10: Renderable, ChildInfoProvider {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
FrameBuffer(verticallyStacking: childInfos(context: context).compactMap(\.buffer))
}
func childInfos(context: RenderContext) -> [ChildInfo] {
[
makeChildInfo(for: value.0, context: context),
makeChildInfo(for: value.1, context: context),
makeChildInfo(for: value.2, context: context),
makeChildInfo(for: value.3, context: context),
makeChildInfo(for: value.4, context: context),
makeChildInfo(for: value.5, context: context),
makeChildInfo(for: value.6, context: context),
makeChildInfo(for: value.7, context: context),
makeChildInfo(for: value.8, context: context),
makeChildInfo(for: value.9, context: context),
]
var infos: [ChildInfo] = []
repeat infos.append(makeChildInfo(for: each children, context: context))
return infos
}
}
+1 -12
View File
@@ -89,20 +89,9 @@ public struct Alert<Actions: View>: View {
extension Alert: Renderable {
public func renderToBuffer(context: RenderContext) -> FrameBuffer {
let hasActions = !(actions is EmptyView)
let effectiveConfig =
hasActions
? config
: ContainerConfig(
borderStyle: config.borderStyle,
borderColor: config.borderColor,
titleColor: config.titleColor,
padding: config.padding,
showFooterSeparator: false
)
return renderContainer(
title: title,
config: effectiveConfig,
config: config,
content: Text(message),
footer: hasActions ? actions : nil,
context: context
+17 -26
View File
@@ -139,36 +139,27 @@ internal func renderContainer<Content: View, Footer: View>(
footer: Footer?,
context: RenderContext
) -> FrameBuffer {
let containerStyle = ContainerStyle(from: config)
let hasFooter = footer != nil
let style = ContainerStyle(
showHeaderSeparator: true,
showFooterSeparator: hasFooter && config.showFooterSeparator,
borderStyle: config.borderStyle,
borderColor: config.borderColor
)
if let footerView = footer {
let container = ContainerView(
title: title,
titleColor: config.titleColor,
style: containerStyle,
padding: config.padding
) {
content
} footer: {
let container = ContainerView(
title: title,
titleColor: config.titleColor,
style: style,
padding: config.padding
) {
content
} footer: {
if let footerView = footer {
footerView
}
return container.renderToBuffer(context: context)
} else {
let container = ContainerView(
title: title,
titleColor: config.titleColor,
style: ContainerStyle(
showHeaderSeparator: true,
showFooterSeparator: false,
borderStyle: config.borderStyle,
borderColor: config.borderColor
),
padding: config.padding
) {
content
}
return container.renderToBuffer(context: context)
}
return container.renderToBuffer(context: context)
}
// MARK: - Container View
+1 -1
View File
@@ -122,7 +122,7 @@ extension Panel where Footer == EmptyView {
borderColor: borderColor,
titleColor: titleColor,
padding: padding,
showFooterSeparator: true
showFooterSeparator: false
)
}
}
+2 -2
View File
@@ -76,7 +76,7 @@ struct ViewBuilderTests {
}
let views = buildViews()
#expect(views is TupleView2<Text, Text>)
#expect(views is TupleView<Text, Text>)
}
@Test("ViewBuilder with three views")
@@ -89,7 +89,7 @@ struct ViewBuilderTests {
}
let views = buildViews()
#expect(views is TupleView3<Text, Text, Text>)
#expect(views is TupleView<Text, Text, Text>)
}
@Test("VStack can contain views")